diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bd24c..27d2a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project are documented here, following [Keep a Changelog](https://keepachangelog.com/) and semantic versioning. +## [0.1.19] - 2026-09-23 + +### Fixed + +- Classify loose `.snap` files as generated trim candidates, alongside existing snapshot-directory handling. + ## [0.1.18] - 2026-09-23 ### Fixed diff --git a/README.md b/README.md index 768356a..59332a9 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ ctxtrim · my-repo · 412 files Known binary media, including AVIF, HEIC/HEIF, APNG, WebM, Ogg, FLAC and M4A, are not read as text and contribute no text tokens. They remain explicit ignore candidates. +Loose `.snap` files are treated as generated output and suggested for trimming, just like files inside `__snapshots__/` directories. + Estimates use the widely-cited ~4-chars-per-token rule (great for ranking and relative savings; pass `--price` to match your model). Vendored dependency and build output directories (`node_modules`, `dist`, `build`, ...) are reported as a single entry keyed by directory name — sized from a cheap stat walk without reading the files inside. Directory-level categories are decided by the directory name, not by reading its contents. diff --git a/package-lock.json b/package-lock.json index e9fe25e..125adc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctxtrim", - "version": "0.1.18", + "version": "0.1.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctxtrim", - "version": "0.1.18", + "version": "0.1.19", "license": "MIT", "bin": { "ctxtrim": "bin/ctxtrim.js" diff --git a/package.json b/package.json index f5ec970..2a851ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctxtrim", - "version": "0.1.18", + "version": "0.1.19", "description": "Trim what bloats your AI coding context. Scan a repo, find the high-cost/low-value files ballooning your Claude Code / Cursor / Codex context, and write ignore files to cut token cost. Zero dependencies.", "type": "module", "bin": { diff --git a/src/classify.js b/src/classify.js index 6859387..8900dec 100644 --- a/src/classify.js +++ b/src/classify.js @@ -52,6 +52,7 @@ export function classifyPath(rel) { if (parts.some((p) => VENDOR_DIRS.includes(p))) return { category: "vendored", trim: true, binary: false, reason: "vendored dependency directory" }; if (parts.some((p) => BUILD_DIRS.includes(p))) return { category: "build", trim: true, binary: false, reason: "build / generated output directory" }; if (LOCKFILES.has(name)) return { category: "lockfile", trim: true, binary: false, reason: "dependency lockfile" }; + if (ext === ".snap") return { category: "generated", trim: true, binary: false, reason: "generated snapshot" }; if (MINIFIED.test(name)) return { category: "minified", trim: true, binary: false, reason: "minified / bundled / sourcemap" }; if (DATA_EXT.has(ext)) return { category: "data", trim: true, binary: false, reason: "data file" }; return null; diff --git a/test/loose-snapshots.test.js b/test/loose-snapshots.test.js new file mode 100644 index 0000000..266120a --- /dev/null +++ b/test/loose-snapshots.test.js @@ -0,0 +1,32 @@ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { classify } from "../src/classify.js"; +import { scanRepo } from "../src/scan.js"; + +test("loose snapshot files are generated trim candidates", () => { + for (const name of ["a.snap", "nested/result.SNAP"]) { + const result = classify(name, { tokens: 10, sample: "expected output" }); + assert.equal(result.category, "generated", name); + assert.equal(result.trim, true, name); + assert.equal(result.binary, false, name); + } + assert.equal(classify("test/__snapshots__/a.snap", {}).category, "build"); + assert.equal(classify("src/snapshot.js", { tokens: 10 }).trim, false); +}); + +test("scan includes loose snapshots in token totals and ignore patterns", (t) => { + const root = mkdtempSync(join(tmpdir(), "ctxtrim-loose-snap-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + mkdirSync(join(root, "test")); + writeFileSync(join(root, "test", "result.snap"), "expected output\n".repeat(20)); + writeFileSync(join(root, "source.js"), "export const value = 1;\n"); + const result = scanRepo(root); + const snapshot = result.files.find((file) => file.rel === "test/result.snap"); + assert.equal(snapshot.category, "generated"); + assert.ok(snapshot.tokens > 0); + assert.equal(result.totals.trimTokens, snapshot.tokens); + assert.deepEqual(result.patterns, ["test/result.snap"]); +});