codebase-snapshot 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1242 -0
- package/LICENSE +21 -0
- package/README.md +276 -2
- package/SECURITY.md +110 -0
- package/dist/bin.js +9840 -0
- package/dist/chunk-FJ4RQYY2.js +7885 -0
- package/dist/cli.cjs +9822 -0
- package/dist/cli.d.cts +124 -0
- package/dist/cli.d.ts +124 -0
- package/dist/cli.js +1931 -0
- package/dist/index.cjs +7973 -0
- package/dist/index.d.cts +710 -0
- package/dist/index.d.ts +710 -0
- package/dist/index.js +3 -0
- package/dist/types-Ca4PmkiQ.d.cts +491 -0
- package/dist/types-Ca4PmkiQ.d.ts +491 -0
- package/docs/agent.md +135 -0
- package/docs/cli.md +437 -0
- package/docs/configuration.md +424 -0
- package/docs/how-to-context-budget.md +90 -0
- package/docs/how-to-feed-a-repo-to-claude.md +122 -0
- package/docs/install.md +196 -0
- package/docs/output.md +264 -0
- package/docs/troubleshooting.md +384 -0
- package/docs/vs-code2prompt.md +66 -0
- package/docs/vs-gitingest.md +69 -0
- package/docs/vs-repomix.md +83 -0
- package/package.json +112 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `codebase-snapshot`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 0.9.0
|
|
8
|
+
|
|
9
|
+
### The package is now `codebase-snapshot`, unscoped
|
|
10
|
+
|
|
11
|
+
**Breaking:** published as `codebase-snapshot` rather than `@ankurhalder/codebase-snapshot`.
|
|
12
|
+
|
|
13
|
+
The old name is **deprecated, not unpublished**, so existing instructions keep working:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @ankurhalder/codebase-snapshot generate # still resolves, now prints a deprecation notice
|
|
17
|
+
npx codebase-snapshot generate # the supported name from 0.9.0
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Why: npm's search index ranks on text match rather than downloads — a package with a few thousand
|
|
21
|
+
weekly downloads outranks one with a hundred million when the query matches its name better — so a
|
|
22
|
+
name that *is* the phrase people search is the strongest free signal available. Doing this at
|
|
23
|
+
version 0.9.0 with no dependents costs one migration; doing it after documentation, articles and
|
|
24
|
+
tutorials all point at the scoped name costs far more.
|
|
25
|
+
|
|
26
|
+
The rename is not cosmetic. The package name is written into every ownership marker and every
|
|
27
|
+
manifest, and the output-directory guard compares it exactly, so renaming it naively would have made
|
|
28
|
+
**every snapshot written by 1.0–4.4.x unrecognisable to its own tool** — the tool would have refused
|
|
29
|
+
to update its own output. Ownership now accepts the legacy spelling and rewrites it to the current
|
|
30
|
+
name on the next run, so an existing snapshot keeps working and is upgraded in place.
|
|
31
|
+
|
|
32
|
+
### Metadata and discoverability
|
|
33
|
+
|
|
34
|
+
- `keywords` went from 9 to 20, chosen by measured position rather than intuition — phrases like
|
|
35
|
+
`codebase-to-prompt` and `repo-to-llm` that describe how people actually ask for this.
|
|
36
|
+
- The `description` now contains the word "codebase" and names the clients people search for.
|
|
37
|
+
- The README's 16 relative links became absolute. On npmjs.com — the most-linked page the package
|
|
38
|
+
has — every documentation link was resolving to a 404.
|
|
39
|
+
- The README states build provenance, which the release pipeline has been producing since 4.0.0
|
|
40
|
+
without ever mentioning.
|
|
41
|
+
|
|
42
|
+
### Documentation site
|
|
43
|
+
|
|
44
|
+
The docs now have a crawlable home at <https://codebase-snapshot.ankurhalder.com>, generated from
|
|
45
|
+
`README.md` and `docs/` in this repository, with a sitemap, its own `robots.txt`, an `llms.txt` at
|
|
46
|
+
the site root and a Markdown twin per page. GitHub's `robots.txt` blocks directory-tree and raw-file
|
|
47
|
+
paths and publishes no repository sitemap, so `docs/` had no crawlable entry point before this.
|
|
48
|
+
|
|
49
|
+
The GitHub wiki is closed. The same prose was being maintained in the wiki, `docs/` and the README
|
|
50
|
+
with no canonical copy; `Home.md` is now a single pointer and the other pages are one-line stubs so
|
|
51
|
+
inbound links keep resolving.
|
|
52
|
+
|
|
53
|
+
### New guides
|
|
54
|
+
|
|
55
|
+
- [Feeding a codebase to ChatGPT, Claude or Cursor](docs/how-to-feed-a-repo-to-claude.md)
|
|
56
|
+
- [Budgeting LLM context](docs/how-to-context-budget.md)
|
|
57
|
+
- Comparisons with [repomix](docs/vs-repomix.md), [gitingest](docs/vs-gitingest.md) and
|
|
58
|
+
[code2prompt](docs/vs-code2prompt.md), each stating where the alternative is the better choice
|
|
59
|
+
|
|
60
|
+
### Tooling
|
|
61
|
+
|
|
62
|
+
- `npm run seo-rank` measures this package's position in npm and GitHub search for a fixed query
|
|
63
|
+
set, appends to `seo-log.jsonl`, and `--check` fails when a query we hold slips. It runs weekly in
|
|
64
|
+
CI.
|
|
65
|
+
- `npm run site` renders the documentation site; `scripts/check-site-links.mjs` fails the deploy on
|
|
66
|
+
an unresolvable internal link.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4.4.1
|
|
71
|
+
|
|
72
|
+
### `setup` ran the install with npm's own environment, which could refuse it
|
|
73
|
+
|
|
74
|
+
Running `setup` through `npx` — the documented way — put the parent npm's resolved configuration
|
|
75
|
+
into `npm_config_*` variables, and the nested `npm install` inherited them. With an npm that
|
|
76
|
+
supports an `allowScripts` switch the child aborted with
|
|
77
|
+
`EALLOWSCRIPTS: --allow-scripts is not allowed in project-scoped installs`, so the install was
|
|
78
|
+
skipped and only a warning said so. The nested run now gets the caller's environment minus
|
|
79
|
+
`npm_config_*` (and npm's lifecycle variables), so it reads the project's own `.npmrc` and flags —
|
|
80
|
+
which is the point of using the project's package manager at all. A shim-based test pins it: the
|
|
81
|
+
package manager is invoked, and none of npm's config reaches it.
|
|
82
|
+
## 4.4.0
|
|
83
|
+
|
|
84
|
+
### One command that does the whole first run
|
|
85
|
+
|
|
86
|
+
`npx @ankurhalder/codebase-snapshot setup` — detect the project, write the commented config (only
|
|
87
|
+
when the repository has none; an existing one is kept byte for byte and the output says so), add
|
|
88
|
+
the npm scripts, the devDependency and the `.gitignore` entries, install dependencies with the
|
|
89
|
+
package manager the repository's lockfile names (`bun`, `pnpm`, `yarn` or npm), then generate
|
|
90
|
+
through the normal `generate` command so every guard, warning and JSON field is the same one a
|
|
91
|
+
plain run produces.
|
|
92
|
+
|
|
93
|
+
- `--no-install` skips the install: CI, an offline machine, or a lockfile the project's own tooling
|
|
94
|
+
owns.
|
|
95
|
+
- A failed install is a warning, not a dead end — the snapshot is still generated, because the
|
|
96
|
+
scripts resolve the package through `npx` until it is installed.
|
|
97
|
+
- `--dry-run` prints the plan and the config it would write, and writes nothing.
|
|
98
|
+
- `--format json` emits one document: `{ "setup": { … }, "manifest": { … } }`.
|
|
99
|
+
- The ownership guard is untouched: an `outDir` holding files this tool did not write is still
|
|
100
|
+
refused (exit 1), and the refusal names the entries.
|
|
101
|
+
|
|
102
|
+
`init` keeps its old behaviour — it refuses when a config exists and needs `--force`, because
|
|
103
|
+
someone who typed `init` should be told. `setup` is the merge-friendly path.
|
|
104
|
+
|
|
105
|
+
### An outDir that holds only empty directories is replaced
|
|
106
|
+
|
|
107
|
+
Reported from a real repository: `generate` refused with "it holds no .codebase-snapshot marker
|
|
108
|
+
(found parts, reports) — remove the directory yourself" for a directory that held **no files**, just
|
|
109
|
+
`parts/` and `reports/` from an interrupted run. Replacing a tree with no files destroys nothing, so
|
|
110
|
+
the guard now treats it as empty. The walk is iterative, a symlink counts as content, and a
|
|
111
|
+
directory holding even one file is still refused without a marker.
|
|
112
|
+
## 4.3.0
|
|
113
|
+
|
|
114
|
+
### Node 18 and 20 actually run the suite now — and one of them was broken
|
|
115
|
+
|
|
116
|
+
`engines` has said `>=18`, and until this release no run had ever executed on 18 or 20: the publish
|
|
117
|
+
workflow ran `npm run verify` on one Node version only. The gate now runs on 18, 20 and 22 for every
|
|
118
|
+
release, and the first honest run found a real defect — see the next section. Verified locally on
|
|
119
|
+
18.20.4 and 20.19.4: 498 tests, 493 pass, 0 fail, 5 version-dependent skips.
|
|
120
|
+
|
|
121
|
+
### `validate` called an unreadable file "missing from disk" on Node 18
|
|
122
|
+
|
|
123
|
+
A Windows `Deny-Read` ACE makes Node 18's `statSync` throw `EPERM` and `existsSync` answer `false`,
|
|
124
|
+
so a file the user could not read looked *deleted*: `validate` reported "no longer exists on disk"
|
|
125
|
+
for a file sitting right there. The validator now asks why a path cannot be stat'd (`ENOENT` means
|
|
126
|
+
gone, `EPERM`/`EACCES` means unreadable) instead of trusting a boolean, and both drift classes are
|
|
127
|
+
deduplicated — an unreadable file is seen by the scan loop and the manifest loop.
|
|
128
|
+
|
|
129
|
+
### A base64-wrapped private key is detected
|
|
130
|
+
|
|
131
|
+
`LS0tLS1CRUdJTi…` is the base64 of `-----BEGIN `, so a wrapped key is found by its prefix without
|
|
132
|
+
decoding anything. The new `base64-private-key` rule takes the built-in set to 30; archives and
|
|
133
|
+
other encodings remain out of scope, and `SECURITY.md` now says which is which.
|
|
134
|
+
|
|
135
|
+
### A dependency manifest is data, not documentation
|
|
136
|
+
|
|
137
|
+
`requirements.txt` and its variants were classified kind `doc` because they end in `.txt`. They are
|
|
138
|
+
machine input, like `package.json`, so they are `data` now.
|
|
139
|
+
|
|
140
|
+
### The monolith explains the one field it cannot carry
|
|
141
|
+
|
|
142
|
+
`full-codebase.json` embeds a copy of the manifest, and a file cannot record its own sha256. The
|
|
143
|
+
embedded copy keeps `artifacts.fullJson: null` and carries a `selfReference` sentence pointing at
|
|
144
|
+
`manifest.json`, which holds the real byteSize and digest, instead of leaving the reader to wonder
|
|
145
|
+
why the two manifests disagree.
|
|
146
|
+
|
|
147
|
+
### Empty and trailing-blank line accounting is documented
|
|
148
|
+
|
|
149
|
+
`includedLines` counts content lines after CRLF normalisation and without a trailing empty element:
|
|
150
|
+
an empty file records `0` while the fence shows one empty line. That is the intended meaning of the
|
|
151
|
+
field, and `docs/output.md` now states it rather than leaving it to be rediscovered.
|
|
152
|
+
|
|
153
|
+
### The audit release
|
|
154
|
+
|
|
155
|
+
Every other change here came out of a full audit of the tool against eight real repositories and an
|
|
156
|
+
adversarial review of every module. The three defects that mattered most were all in the two
|
|
157
|
+
promises this package leads with — "never leak secrets" and "machine-readable output".
|
|
158
|
+
|
|
159
|
+
### Two ways a secret kept its tail
|
|
160
|
+
|
|
161
|
+
A quoted credential containing an escaped quote was masked only up to the backslash:
|
|
162
|
+
`JSON.parse('{"password": "p\"ssw0rd"})` was emitted as
|
|
163
|
+
`"[REDACTED: potential secret-like value omitted from snapshot]"ssw0rd"` — most of the value in
|
|
164
|
+
clear, and the line left syntactically broken. An unquoted value stopped at the first character
|
|
165
|
+
outside `[A-Za-z0-9_+=/-]`, so a percent-encoded token kept its tail:
|
|
166
|
+
`?access_token=abc%2Edef&scope=read` lost `abc` and shipped `%2Edef`. Both value classes now
|
|
167
|
+
describe the value they are masking — escaped pairs are consumed by the quoted rule, and `%`,
|
|
168
|
+
`:` and `~` are token characters — and the mask replaces the whole value.
|
|
169
|
+
|
|
170
|
+
### A compressed fixture was emitted as mojibake, at twice its own size
|
|
171
|
+
|
|
172
|
+
`.br` was missing from `BINARY_EXTENSIONS`, so ripgrep's 186-byte `sherlock.br` failed UTF-8
|
|
173
|
+
decoding instead of being recognised as binary: it was emitted `full` as 341 bytes containing 78
|
|
174
|
+
U+FFFD — the one case where the artifact is larger than its source and says nothing. `.br` (and
|
|
175
|
+
`zst`, `lz4`, `lzma`) are now in the list, and a second guard catches the next unknown format:
|
|
176
|
+
at least 32 undecodable bytes **and** at least a third of the file means binary. The bar is high
|
|
177
|
+
on purpose — a Latin-1 document is legitimately full of invalid UTF-8 bytes and must stay
|
|
178
|
+
`utf-8-lossy`.
|
|
179
|
+
|
|
180
|
+
### A config that exported nothing ran on the built-in defaults
|
|
181
|
+
|
|
182
|
+
`export default null;` was erased by a nullish fallback (`imported.default ?? imported`), so the
|
|
183
|
+
module namespace stood in for the config, every setting was ignored, and the run generated with
|
|
184
|
+
the defaults at exit 0. The loader now distinguishes "no default export" from "the default
|
|
185
|
+
export is null" and refuses with the same message every other wrong export gets.
|
|
186
|
+
|
|
187
|
+
### An unknown setting was dropped in silence
|
|
188
|
+
|
|
189
|
+
`{ maxPartByte: 4096 }` generated with the default budget at exit 0 — the typo was invisible, and
|
|
190
|
+
it is the silent fallback this project forbids. A config file (module, JSON or `.snapshotrc`)
|
|
191
|
+
now rejects any key that is not a setting, naming it and pointing at the generated config as the
|
|
192
|
+
list of what exists.
|
|
193
|
+
|
|
194
|
+
### `--format json` produced nothing when the arguments were wrong
|
|
195
|
+
|
|
196
|
+
Every usage error — unknown flag, unknown command, bad value, missing `--cwd`, wrong arity —
|
|
197
|
+
exited 2 with **0 bytes** on stdout, while the docs promised one parseable document per JSON-mode
|
|
198
|
+
invocation. The format is now read from raw argv before validation, so the error document is
|
|
199
|
+
emitted for usage errors too; the human line stays on stderr.
|
|
200
|
+
|
|
201
|
+
### An async hook could append to the JSON payload
|
|
202
|
+
|
|
203
|
+
The stdout redirect that keeps `console.log` inside a hook out of the payload wrapped only the
|
|
204
|
+
synchronous call. A hook that logged after an `await` appended its text after the JSON, so
|
|
205
|
+
`--format json` output was no longer parseable while the command still exited 0. The redirect now
|
|
206
|
+
lives until the returned promise settles, and the payload is written to the real stdout exactly
|
|
207
|
+
once.
|
|
208
|
+
|
|
209
|
+
### Truncation was invisible in the audit
|
|
210
|
+
|
|
211
|
+
`reports/audit.md` printed "✅ No files exceeded the large file budget" while a third of a
|
|
212
|
+
document was missing from the snapshot, because the cut came from `maxPartBytes` rather than
|
|
213
|
+
`largeFileBytes`. It is structural for the `docs` preset, whose 100 KB part budget sits *below*
|
|
214
|
+
the 200 KB large-file threshold. Section 3 now reports truncation from either cause, with bytes,
|
|
215
|
+
lines and a `Cut by` column, and `generate` warns when `maxPartBytes < largeFileBytes`.
|
|
216
|
+
|
|
217
|
+
### A part-budget cut carried no marker
|
|
218
|
+
|
|
219
|
+
When the part budget was the binding constraint the block simply stopped — no
|
|
220
|
+
`[N lines omitted]` anywhere, and the manifest said `truncated: true` while the artifact read as
|
|
221
|
+
complete. Every budget-forced cut now carries a marker naming the budget
|
|
222
|
+
(`… [19940 lines omitted: part budget 1887 B] …`), recomputed correctly when a second shrink pass
|
|
223
|
+
runs.
|
|
224
|
+
|
|
225
|
+
### The preview tail window lost lines on newline-terminated files
|
|
226
|
+
|
|
227
|
+
`content.split("\n")` yields a trailing empty element, which consumed a tail slot: a 10-line file
|
|
228
|
+
at `previewHeadLines: 2`, `previewTailLines: 2` emitted `line 1`, `line 2`, an omission marker and
|
|
229
|
+
**no tail at all**, while `includedLines` claimed one more line than the block carried. The tail
|
|
230
|
+
window and the omission count now describe what is emitted.
|
|
231
|
+
|
|
232
|
+
### A filename could inject a link into `reports/navigation.md`
|
|
233
|
+
|
|
234
|
+
The link label escaped `|` and control characters but not brackets, so a file named `b](b).ts`
|
|
235
|
+
produced `[src/b](b).ts](…)` — a live Markdown link to `b` — and `x](/evil).ts` linked to `/evil`.
|
|
236
|
+
The label is escaped; the link target and anchor are unchanged.
|
|
237
|
+
|
|
238
|
+
### A deep directory tree crashed the run
|
|
239
|
+
|
|
240
|
+
The project-tree walker recursed per directory, so a ~6,000-level chain overflowed the stack after
|
|
241
|
+
76 s and wrote **no snapshot**. The walk is iterative, the output order is unchanged, and the tree
|
|
242
|
+
is capped at 512 levels with a marker and a warning.
|
|
243
|
+
|
|
244
|
+
### One oversized text file aborted the whole run
|
|
245
|
+
|
|
246
|
+
A file over ~512 MiB of *text* threw `Cannot create a string longer than 0x1fffffe8 characters`
|
|
247
|
+
from inside the run, with no `skipped[]` record; the equivalent *binary* file was skipped
|
|
248
|
+
correctly. A decode failure is now a per-file `read-error` skip and the run continues.
|
|
249
|
+
|
|
250
|
+
### `validate`, `report` and `diff` now agree on what a snapshot is
|
|
251
|
+
|
|
252
|
+
`validate` called a manifest "valid and up to date" with `fingerprint` deleted, `report` printed
|
|
253
|
+
`undefined`, and `diff` rejected the same file. One shared guard now requires `generatedAt`,
|
|
254
|
+
`schemaVersion`, a 64-character lowercase `fingerprint`, `counts`, `totals`, `layers` and the
|
|
255
|
+
`files`/`skipped`/`parts` arrays, and all three commands use it.
|
|
256
|
+
|
|
257
|
+
### `diff` notices a schema or a parts change
|
|
258
|
+
|
|
259
|
+
A copy with `schemaVersion: "9.9.9"`, a deleted `parts` array, or a changed part digest was
|
|
260
|
+
reported as `+0 -0 ~0 =N unchanged`. `diff` now reports a schema mismatch as a difference and
|
|
261
|
+
compares part metadata by filename.
|
|
262
|
+
|
|
263
|
+
### `--discovery walk` now honours `.gitignore` escapes
|
|
264
|
+
|
|
265
|
+
`\#hashfile.ts`, `\!bangfile.ts` and `spaced\ .ts` were all *included* in walk mode while git
|
|
266
|
+
ignored them — the parser trimmed and sniffed first characters, and the matcher normalised `\` to
|
|
267
|
+
`/`. The parser follows gitignore(5)'s line grammar and an escape-aware matcher handles patterns
|
|
268
|
+
containing a backslash; plain `!` negation is unchanged.
|
|
269
|
+
|
|
270
|
+
### `init` writes the repository's real branch
|
|
271
|
+
|
|
272
|
+
The generated config always said `branch: "main"`, so on any repository whose default branch is
|
|
273
|
+
`master` or `vNext` the first `validate` printed a branch-mismatch warning the user never caused.
|
|
274
|
+
It now records the branch `getGitInfo` reports and falls back to `"main"` only when git cannot
|
|
275
|
+
report one.
|
|
276
|
+
|
|
277
|
+
### The contract publishes `check` and `--mode`, and stops over-promising `--option=value`
|
|
278
|
+
|
|
279
|
+
`check` is documented as an alias of `validate`, the hidden `--mode` selector is in
|
|
280
|
+
`describe.options` with its values and contradictions, and the help sentence now says
|
|
281
|
+
"value-taking options" — booleans never accepted `=true`.
|
|
282
|
+
|
|
283
|
+
### Documentation
|
|
284
|
+
|
|
285
|
+
The complete list of corrections is in the wiki and `docs/`: option counts, the removed `agents`
|
|
286
|
+
skill target, `--tmp-dir` (a config key, not a flag), the `--watch` message, the shipped README's
|
|
287
|
+
404 links (`CHANGELOG.md` and `SECURITY.md` now ship; the rest are absolute URLs), and
|
|
288
|
+
measurement-backed statements for determinism, text-mode output streams, monolith byte counts and
|
|
289
|
+
ignore granularity.
|
|
290
|
+
|
|
291
|
+
### The generated config was a lint warning in every Next.js project
|
|
292
|
+
|
|
293
|
+
`init` wrote `export default { … }`. `import/no-anonymous-default-export`, which
|
|
294
|
+
`eslint-config-next` enables by default, reports that inline form — so in the framework this tool
|
|
295
|
+
detects most often, the one file we tell people to keep and edit arrived flagged the moment it was
|
|
296
|
+
created. The object is now assigned to `config` and exported by name:
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
const config = { /* every setting */ };
|
|
300
|
+
|
|
301
|
+
export default config;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Both forms load identically; only one is quiet. The reason is in the file itself, so deleting the
|
|
305
|
+
comment does not invite the warning back.
|
|
306
|
+
|
|
307
|
+
### A clean repository reported `Warnings: 156`
|
|
308
|
+
|
|
309
|
+
Every masked line was appended to `manifest.warnings` as `<rule> at <path>:<line>`, so a
|
|
310
|
+
repository with nothing wrong with it reported 156 "warnings" — the first thing a new user sees
|
|
311
|
+
after `init` + `generate`, and the reason a 583-file project looked broken. Worse, the summary
|
|
312
|
+
printed ten of them under that count with nothing to say the other 146 existed.
|
|
313
|
+
|
|
314
|
+
Redaction is not a warning: a masked value is the tool doing its job, while a warning says the
|
|
315
|
+
snapshot may surprise you. The two are now separate — the summary prints
|
|
316
|
+
`Redacted: N line(s) — values masked; see reports/audit.md` and keeps `Warnings:` for the things
|
|
317
|
+
worth acting on, and a warnings list longer than the ten-row preview ends with
|
|
318
|
+
`… and N more in manifest.warnings[]`. When `reports/audit.md` is disabled the manifest says the
|
|
319
|
+
per-line detail was not written instead of losing it silently. `manifest.totals.redactedLines`
|
|
320
|
+
carries the count for machines.
|
|
321
|
+
|
|
322
|
+
### One line of code was counted twice
|
|
323
|
+
|
|
324
|
+
`scanAndRedactSecrets` pushed a finding per (line, rule) pair, so an unquoted
|
|
325
|
+
`JWT_SECRET=<value>` — which matches both `credential-assignment-unquoted` and
|
|
326
|
+
`env-secret-assignment` — appeared twice in `reports/audit.md` on the same file and line and
|
|
327
|
+
inflated the total, making a 78-site repository report 156 findings. A finding is now one *line*
|
|
328
|
+
with `rules[]` naming every rule that matched it; the audit table has one row per line and a
|
|
329
|
+
`Rules` column that lists them.
|
|
330
|
+
|
|
331
|
+
### Artifact counts followed the generating machine's locale
|
|
332
|
+
|
|
333
|
+
`reports/audit.md` and the snapshot `README.md` called `toLocaleString()` with no locale, so the
|
|
334
|
+
same repository reported `1,24,976` lines on an `en-IN` machine and `124,976` on an `en-US` one —
|
|
335
|
+
different bytes from the same commit, which defeats diffing two snapshots or caching one. Both
|
|
336
|
+
generators now share `formatNumber()` with the CLI, which already pinned `"en-US"`.
|
|
337
|
+
|
|
338
|
+
## 4.1.1
|
|
339
|
+
|
|
340
|
+
### `init` left the package undeclared, so nothing reached `node_modules`
|
|
341
|
+
|
|
342
|
+
`init` wrote `npx @ankurhalder/codebase-snapshot generate` into `package.json` and no dependency
|
|
343
|
+
at all. `npm install` therefore installed nothing, every `npm run snapshot` resolved `@latest`
|
|
344
|
+
from the registry instead of a pinned copy, and the `npm install && npm run snapshot` it printed
|
|
345
|
+
as the next step could not have installed it. `init` now records
|
|
346
|
+
`devDependencies["@ankurhalder/codebase-snapshot"]` in the same single write as the scripts, and
|
|
347
|
+
leaves a range that is already declared — including a deliberate pin — untouched.
|
|
348
|
+
|
|
349
|
+
### `--minimal` wrote a config that documented nothing
|
|
350
|
+
|
|
351
|
+
The flag wrote only the settings that differed from the defaults, so a Next.js project received
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
export default { preset: "nextjs" };
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
— 179 bytes listing none of the twenty-six settings, in the one file a user opens to change
|
|
358
|
+
them. The flag is gone: `init` always writes the complete commented config, and `describe` no
|
|
359
|
+
longer advertises an option the parser rejects.
|
|
360
|
+
|
|
361
|
+
### `skill --install` wrote a tool's documentation into the project's `AGENTS.md`
|
|
362
|
+
|
|
363
|
+
`AGENTS.md` is a repository's own agent contract — build commands, conventions, house rules —
|
|
364
|
+
not a registry for third-party skills. The install created it when missing, which meant a file
|
|
365
|
+
whose name claims the repository documents itself to agents, holding only this tool's text; and
|
|
366
|
+
it merged a marked section into it when present, editing a tracked file on the user's behalf.
|
|
367
|
+
It now writes only the two registries keyed by skill name —
|
|
368
|
+
`.claude/skills/codebase-snapshot/SKILL.md` and `.cursor/rules/codebase-snapshot.mdc` — and
|
|
369
|
+
appends exactly what it *creates* to `.gitignore`, so an install no longer turns a one-line
|
|
370
|
+
command into a decision about which untracked files to commit. A file that already existed is
|
|
371
|
+
never ignored, and a section left behind by 4.1.0 is reported rather than rewritten. To put the
|
|
372
|
+
skill in `AGENTS.md`, print it and paste it: `codebase-snapshot skill >> AGENTS.md`.
|
|
373
|
+
|
|
374
|
+
`init` also stops printing the tip that led there: an install's whole footprint is now the
|
|
375
|
+
config, `package.json` and `.gitignore`.
|
|
376
|
+
|
|
377
|
+
### A file's recorded bytes and lines described the file, not the block
|
|
378
|
+
|
|
379
|
+
`includedBytes` counted the final newline that the closing fence swallows — 6 290 of 6 344 files in
|
|
380
|
+
a real repository were off by exactly one byte — and a CRLF checkout, which is written as LF, was
|
|
381
|
+
over-counted by one byte per line. A bounded preview reported `head + tail + 1` lines, one more than
|
|
382
|
+
its block carried. Both numbers are now computed once, from the body after normalisation and after
|
|
383
|
+
the closing newline is dropped, and an invariant test compares every `### FILE:` block in every part
|
|
384
|
+
against the manifest entry.
|
|
385
|
+
|
|
386
|
+
### A large file was parsed for a summary the snapshot could not carry
|
|
387
|
+
|
|
388
|
+
A file above `largeFileBytes` is emitted as a head/tail excerpt, yet it was still parsed into an AST
|
|
389
|
+
and had every identifier masked so a whole-file summary could be attached — a summary describing
|
|
390
|
+
lines no artifact contains, which the partitioner then discarded. On a 1.9 MB source file that was
|
|
391
|
+
70% of the wall clock: 1.8 s / 283 MB peak RSS became 0.5 s / 105 MB. The manifest names every
|
|
392
|
+
skipped preview in one grouped warning; `summaryOmitted` is deliberately not reused for them,
|
|
393
|
+
because it means "dropped for budget" and the part prints that reason.
|
|
394
|
+
|
|
395
|
+
### `--format json` wrote nothing at all when a command failed
|
|
396
|
+
|
|
397
|
+
Progress goes to stderr and payloads to stdout, which meant a failed JSON-mode run produced an empty
|
|
398
|
+
stdout: an agent had to notice the empty string, then parse stderr prose and guess. Every failure now
|
|
399
|
+
writes one parseable document — `{ "error": { "message": …, "exitCode": … } }` — while the human line
|
|
400
|
+
stays on stderr and the exit code is unchanged. A successful run never carries an `error` key.
|
|
401
|
+
|
|
402
|
+
### Smaller corrections
|
|
403
|
+
|
|
404
|
+
- Masking no longer walks left through a URL path: `https://example.com/v1/<key>` was emitted as
|
|
405
|
+
`https://example.[REDACTED]`, contradicting "only the matched span is masked".
|
|
406
|
+
- `explain` answers a file inside an ignored directory with the record its directory carries,
|
|
407
|
+
naming the ancestor, instead of the weaker `not-discovered`.
|
|
408
|
+
- `package.json#snapshot` as an array is reported like any other wrong type instead of being
|
|
409
|
+
silently ignored, and an unreadable config file names itself in the error.
|
|
410
|
+
- `init` no longer fails with a raw `EISDIR` when `.gitignore` is a directory — after the config had
|
|
411
|
+
already been written. The `.gitignore` line is a convenience, so it is a note, and `init` finishes.
|
|
412
|
+
- `--format`'s scope is the same machine-readable eight-command list as every other option, and a
|
|
413
|
+
test now fails if any option's scope regresses to prose or names a command that does not exist.
|
|
414
|
+
- The hook documentation says what "not awaited" does not mean: Node keeps the process alive while
|
|
415
|
+
the hook's own timers and sockets are outstanding, so `afterGenerate` should stay short.
|
|
416
|
+
- The generators agree with each other: `full-codebase.md` escapes control and bidi characters like
|
|
417
|
+
every other artifact, the snapshot `README.md` links only to reports that were actually written,
|
|
418
|
+
and a layer or kind name containing `|` or a backtick can no longer break the table or the inline
|
|
419
|
+
code span it is listed in.
|
|
420
|
+
- The wizard no longer treats `package.json` `"snapshot": []` as an existing configuration.- The README and `llms.txt` describe what 4.1.0 actually guarantees — the eleven skip reasons, the
|
|
421
|
+
`encoding` field, discovery modes, the emitted-byte accounting — instead of the feature list from
|
|
422
|
+
before the release.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 4.1.0
|
|
427
|
+
|
|
428
|
+
### A UTF-16 source file was invisible, and a Latin-1 one was quietly mangled
|
|
429
|
+
|
|
430
|
+
Two encoding decisions were silent, and one was wrong in both directions. `generate` decided
|
|
431
|
+
"binary" from a NUL in the first 8 MB and then decoded with `buffer.toString("utf8")`, so a UTF-16
|
|
432
|
+
source file — which is half NUL bytes — was skipped as `binary` and vanished from the snapshot,
|
|
433
|
+
while a Latin-1 file was emitted with U+FFFD in place of every non-ASCII byte, and `validate` then
|
|
434
|
+
reported the snapshot as current because the hash covers the raw bytes.
|
|
435
|
+
|
|
436
|
+
Decoding is now explicit. A BOM, or the NUL padding of a BOM-less UTF-16 file, is recognised and
|
|
437
|
+
the file is transcoded; a file that is not valid UTF-8 is kept, decoded lossily, and labelled
|
|
438
|
+
`encoding: "utf-8-lossy"` with the count of undecodable bytes in `warnings`; a NUL anywhere in the
|
|
439
|
+
file — including past the 8 MB head scan, which used to be the end of the check — marks it binary
|
|
440
|
+
and `skipped[]` records the offset. `files[].encoding` is present only when a file was not plain
|
|
441
|
+
UTF-8, so its absence keeps meaning "clean UTF-8". `explain` answers the same question the same
|
|
442
|
+
way. Verified: 12 new tests covering both endiannesses, BOM-less detection, five classes of
|
|
443
|
+
invalid sequence, a NUL at byte 8388608, and hash stability across two runs.
|
|
444
|
+
|
|
445
|
+
### A file the tool did not write could be deleted without a word
|
|
446
|
+
|
|
447
|
+
Ownership checking had a hole with a nice name. `.gitkeep` and `.gitignore` were treated as
|
|
448
|
+
placeholders and subtracted from the set of entries belonging to somebody else, so a snapshot
|
|
449
|
+
directory this generator owned and that a user then added a `.gitignore` to was replaced — atomically,
|
|
450
|
+
exit 0, `manifest.warnings` empty — and the file was gone. A directory holding nothing but
|
|
451
|
+
`.gitkeep`, or nothing but a `.gitignore` (`*`, `!.gitignore`), was replaced without any ownership
|
|
452
|
+
evidence at all, because the placeholder shortcut fired before the marker was ever read.
|
|
453
|
+
|
|
454
|
+
Both are files a user creates; the generator never writes either into an output directory. They are
|
|
455
|
+
foreign entries now, so the run refuses and names them, exactly as it already did for `notes.md`.
|
|
456
|
+
`.DS_Store` remains the only tolerated entry, because Finder writes it and it carries no intent.
|
|
457
|
+
Verified: 6 new tests — owned directory plus `.gitignore`, bare `.gitkeep`, bare `.gitignore`, the
|
|
458
|
+
refusal naming the entry, `.DS_Store` still discarded, and a genuine snapshot still replaced.
|
|
459
|
+
|
|
460
|
+
### A directory named `snapshot` disappeared, and the manifest said nothing
|
|
461
|
+
|
|
462
|
+
The output directory was excluded from the scan by name, at every depth: `snapshot`,
|
|
463
|
+
`snapshot.tmp` and `snapshot.backup-*` were dropped before the branch that records a `skipped[]`
|
|
464
|
+
entry, so a directory the user committed under one of those names was missing from the snapshot
|
|
465
|
+
with no record anywhere — the exact omission `skipped[]` exists to prevent, and it contradicted
|
|
466
|
+
the documented promise that every discovered path is accounted for. `generate` and `explain` also
|
|
467
|
+
disagreed: one dropped the path, the other called it `not-discovered`.
|
|
468
|
+
|
|
469
|
+
Exclusion is now evidence-based. A directory is this tool's output when it carries the
|
|
470
|
+
`.codebase-snapshot` marker, and only the exact `outDir`/`tmpDir` paths a run is about to write are
|
|
471
|
+
excluded by name — so a first run still does not record its own output (two identical runs keep
|
|
472
|
+
producing identical manifests) while a user's directory with the same name is scanned like any
|
|
473
|
+
other. `snapshot` and `snapshot.tmp` also left the built-in ignore list, where they had been
|
|
474
|
+
second-layer protection for the same mistake. Verified: 5 new tests, including the same fixture
|
|
475
|
+
through the git and the walking discovery paths, a full generate that now lists
|
|
476
|
+
`snapshot/keep.md`, and the determinism guard that the second run does not record the first.
|
|
477
|
+
|
|
478
|
+
### A hook could kill a run that had already succeeded
|
|
479
|
+
|
|
480
|
+
`hooks.afterGenerate` ran after the snapshot was swapped into place. A hook that returned a
|
|
481
|
+
rejected promise was never awaited and never caught, so Node turned it into an unhandled rejection:
|
|
482
|
+
the process exited 1 *after* printing "Snapshot generated … Integrity: passed" and writing its
|
|
483
|
+
`--format json` payload, and nothing about the failure reached `manifest.json`. A hook's own files
|
|
484
|
+
were also missing from the ownership inventory, so the next run refused a directory the tool had
|
|
485
|
+
written itself, and a `console.log` inside a hook landed on stdout in the middle of the JSON
|
|
486
|
+
payload.
|
|
487
|
+
|
|
488
|
+
A returned promise is now tracked rather than awaited — this is a synchronous API — and its
|
|
489
|
+
rejection is recorded as a warning on the manifest and in the marker, which are re-shipped when it
|
|
490
|
+
lands. The marker is written after the hook for the same reason it now includes the hook's files.
|
|
491
|
+
stdout is redirected to stderr for the duration of the hook and restored afterwards, so the payload
|
|
492
|
+
channel stays pure. Verified: 6 end-to-end tests through the built CLI, including a rejected async
|
|
493
|
+
hook that must exit 0 with a parseable payload and a recorded warning.
|
|
494
|
+
|
|
495
|
+
### `validate` would read any file the manifest named
|
|
496
|
+
|
|
497
|
+
A manifest is an input, and `validate` treated it as trustworthy. It joined `parts[].filename`
|
|
498
|
+
onto `snapshot/parts` with `path.join`, which accepts `../../../some/file` and absolute paths, so a
|
|
499
|
+
manifest naming a file outside the snapshot made `validate` read that file and then report its byte
|
|
500
|
+
size — a read primitive, with a size oracle for the file it named, reachable by anyone who can
|
|
501
|
+
write a manifest. Part names are bare names by construction, so a name that does not stay inside
|
|
502
|
+
`snapshot/parts` is now a drift error and is never read. `explain`'s text form also re-spells
|
|
503
|
+
control and bidi characters in paths, layer names, kind names and part names, because a committed
|
|
504
|
+
`snapshot.config.*` can put a U+202E in a layer name and the terminal renders it.
|
|
505
|
+
|
|
506
|
+
Verified: 5 new tests — a traversal name, an absolute name, the byte-size oracle specifically
|
|
507
|
+
proven closed, a legitimate snapshot still current, and control characters escaped while accented
|
|
508
|
+
text passes through untouched.
|
|
509
|
+
|
|
510
|
+
### Also in this release
|
|
511
|
+
|
|
512
|
+
- **`--watch` reloads a module config.** Node's ESM loader caches by URL, so `snapshot.config.ts`
|
|
513
|
+
(exactly what `init` writes) was never re-read: an edited config was ignored and a broken one
|
|
514
|
+
was invisible while the run printed `passed`. The loader now evicts the CJS cache and imports
|
|
515
|
+
with a per-load token. 6 tests.
|
|
516
|
+
- **`--watch` never drops an edit.** A change during a run, a change inside the post-run quiet
|
|
517
|
+
window, and a source directory whose name matched the outDir basename were all lost silently
|
|
518
|
+
while the manifest kept reporting `Integrity: passed`. 6 tests plus live repros.
|
|
519
|
+
- **JWTs are redacted.** Only three-segment JWTs with long signatures were masked; two-segment,
|
|
520
|
+
padded, empty-signature and short-signature forms reached the parts verbatim. 8 tests.
|
|
521
|
+
- **Display hygiene.** Control, C1 and bidi characters in file and layer names are escaped wherever
|
|
522
|
+
a path is displayed (parts, tree, navigation, audit, explain), raw bytes are kept in
|
|
523
|
+
`manifest.json` and file bodies, and the snapshot README and part header say that contents are
|
|
524
|
+
untrusted repository data. 12 tests.
|
|
525
|
+
- **The hint after `init` is runnable.** It printed `npm run snapshot` with no `package.json`, and
|
|
526
|
+
a bare package name on re-runs. 4 tests. The scaffolder now records the tool in
|
|
527
|
+
`devDependencies` at its own version instead of resolving it from the registry on first run.
|
|
528
|
+
7 tests.
|
|
529
|
+
- **The linter ignores prose.** Content rules matched comments, so documenting why `console` is
|
|
530
|
+
banned failed the lint that bans it.
|
|
531
|
+
|
|
532
|
+
### `validate` and `report` could be talked into saying the state was fine
|
|
533
|
+
|
|
534
|
+
Four ways the state checks answered confidently and wrongly.
|
|
535
|
+
|
|
536
|
+
A run killed mid-write left the output directory holding a marker and no manifest, and `validate`
|
|
537
|
+
answered "run generate first" for ever — an instruction that could never resolve, because
|
|
538
|
+
generation had already run and failed. The marker records the state, the run id and the start time,
|
|
539
|
+
so the message now names them and says whether to wait or regenerate.
|
|
540
|
+
|
|
541
|
+
A manifest with no `config.digest` fell back to comparing a handful of fields, so a changed
|
|
542
|
+
`maxMonolithBytes`, `layers`, `kinds` or custom secret rule passed as "valid and up to date". A
|
|
543
|
+
configuration that cannot be verified is not a match: the absence of the digest is itself reported
|
|
544
|
+
as drift.
|
|
545
|
+
|
|
546
|
+
A file that existed but could not be read was reported as "no longer exist on disk", sending the
|
|
547
|
+
reader to look for a file that was right there. Unreadable and deleted are separate findings now,
|
|
548
|
+
and the summary says which.
|
|
549
|
+
|
|
550
|
+
`report` printed whatever numbers the manifest contained — `-5 files included`,
|
|
551
|
+
`Total lines: 1.5`, `Total size: Infinity MB` — and both `report` and `validate` accepted
|
|
552
|
+
`fingerprint: "12345"`. Counts must be non-negative integers and a fingerprint must be a sha256
|
|
553
|
+
digest, or the file is refused as not being a snapshot manifest.
|
|
554
|
+
|
|
555
|
+
Verified: 17 new tests across `tests/snapshot-incomplete.test.js`,
|
|
556
|
+
`tests/validate-drift-classes.test.js` and the guards suite, including a real Windows ACL deny for
|
|
557
|
+
the unreadable case and a control that an untouched snapshot still reports no drift at all.
|
|
558
|
+
|
|
559
|
+
### `validate` called a binary file a new file
|
|
560
|
+
|
|
561
|
+
`generate` decides "binary" twice: from the extension and the 8 MB head scan, and again on the
|
|
562
|
+
bytes it reads, where a control-byte payload or a NUL past the head scan only reveals itself.
|
|
563
|
+
`validate` re-evaluated skipped paths with the cheap pre-read question alone, so a file `generate`
|
|
564
|
+
had just skipped came back as `new file` and the first `validate` after a `generate` failed on an
|
|
565
|
+
untouched tree. Both commands decide the same way now.
|
|
566
|
+
|
|
567
|
+
### `--discovery` chooses how files are found
|
|
568
|
+
|
|
569
|
+
A `.git` directory that exists but cannot be listed made discovery refuse, and there was no
|
|
570
|
+
override — the only way forward was to repair the repository or copy the tree out of it. The
|
|
571
|
+
refusal also justified itself with a gap that no longer exists (a walk does honour nested
|
|
572
|
+
`.gitignore` files now), so it names the differences that remain: `info/exclude`, the global
|
|
573
|
+
excludes file, tracked files that a `.gitignore` now matches, and a submodule as one entry.
|
|
574
|
+
|
|
575
|
+
`--discovery auto|git|walk` (config `discovery`) makes that choice explicit. `auto` keeps the old
|
|
576
|
+
behaviour, `git` requires git, `walk` scans the tree and reads `.gitignore` directly. The mode is
|
|
577
|
+
validated, scoped to the commands that scan, published in the contract, written to the init
|
|
578
|
+
template, and recorded in `manifest.config`, so changing it is config drift. 10 tests.
|
|
579
|
+
|
|
580
|
+
### A workspace is classified by what its packages are
|
|
581
|
+
|
|
582
|
+
Detection read only the repository root, so a Next.js app under `apps/web` in a pnpm workspace
|
|
583
|
+
detected as `monorepo` and a docs repository whose markdown lived under `docs/` detected as
|
|
584
|
+
`generic` — both contradicting the published preset table. Detection now probes a bounded number
|
|
585
|
+
of workspace packages and markdown directories, so a workspace is classified by the frameworks its
|
|
586
|
+
packages use. The bounds keep it honest: a bare `apps/` without a workspace declaration does not
|
|
587
|
+
trigger it, and a framework two levels down still detects as `monorepo`. 8 tests.
|
|
588
|
+
|
|
589
|
+
### `skill --install` could write outside the repository
|
|
590
|
+
|
|
591
|
+
The installer built its three targets from `--cwd` and wrote them without checking where they
|
|
592
|
+
landed, so a pre-existing junction (`.claude` itself, or `.claude/skills/codebase-snapshot`) or a
|
|
593
|
+
hard-linked `AGENTS.md` made it write outside the repository. Targets are resolved and refused
|
|
594
|
+
when they leave it. The section merge also repairs malformed marker layouts — duplicated blocks,
|
|
595
|
+
an inverted pair, a missing end marker — instead of appending another one, while a well-formed
|
|
596
|
+
file stays byte-identical on a rerun. 11 tests.
|
|
597
|
+
|
|
598
|
+
### `diff` saw no change when the payload changed
|
|
599
|
+
|
|
600
|
+
`diff` compared `relativePath` plus the source hash, so two snapshots of identical sources with
|
|
601
|
+
different `--max-part-bytes` or `--large-file-bytes` — a 4 KB preview against a 36 KB full body,
|
|
602
|
+
275 KB of parts against 307 KB — reported "0 added / 0 removed / 0 changed". The per-file answer
|
|
603
|
+
stays the same, and a payload block now says the configuration differs and why (the config digest,
|
|
604
|
+
the generator version, part and layer totals), which is what "what changed in the context we hand
|
|
605
|
+
to the model?" was asking. 5 tests.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## 4.0.9
|
|
610
|
+
|
|
611
|
+
### The tool executed a config it had been told to ignore
|
|
612
|
+
|
|
613
|
+
`generate --watch --no-config` printed "config file ignored" and then reloaded the repository
|
|
614
|
+
config on every regeneration, running its side effects and applying its include filter (4 tracked
|
|
615
|
+
files became 1). The watch loop now honours the flag.
|
|
616
|
+
|
|
617
|
+
`diff` also loaded the repository config, so comparing two manifests executed repository code —
|
|
618
|
+
and `--no-config` was rejected for that command, leaving no way to opt out. `diff` reads two
|
|
619
|
+
manifests and nothing else now; it never loads a config, and the flag is accepted there for
|
|
620
|
+
consistency. Verified: a config that writes a file on import no longer does so during `diff`, and
|
|
621
|
+
still does during `generate`.
|
|
622
|
+
|
|
623
|
+
### Accepted risk: the ownership marker is evidence, not authentication
|
|
624
|
+
|
|
625
|
+
A hand-written marker plus a stub manifest with a matching digest can still pass the ownership
|
|
626
|
+
check, because there is no secret involved — anyone who can write those two files can also delete
|
|
627
|
+
the directory directly. The check exists to stop the tool from deleting a directory *it* did not
|
|
628
|
+
create, not to resist an attacker with write access. `SECURITY.md` now says so.
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## 4.0.8
|
|
633
|
+
|
|
634
|
+
### A dry run could write and delete
|
|
635
|
+
`--dry-run` printed "nothing was written" and then ran `hooks.afterGenerate`. A hook can write
|
|
636
|
+
files anywhere and delete parts of the live snapshot — one did exactly that during a dry run. The
|
|
637
|
+
hook no longer runs when nothing is written.
|
|
638
|
+
|
|
639
|
+
### Two runs to one output directory both claimed success
|
|
640
|
+
Runs were serialised on `tmpDir` only, so two invocations with different `tmpDir`s but the same
|
|
641
|
+
`outDir` each took their own lock, both exited 0, and one snapshot was silently lost. Both keys are
|
|
642
|
+
locked now, in a fixed order so two runs cannot deadlock: one succeeds, the other refuses.
|
|
643
|
+
|
|
644
|
+
### A git failure no longer changes what gets scanned
|
|
645
|
+
When `git ls-files` failed (corrupt index, git missing) the scan fell back to a directory walk,
|
|
646
|
+
which honours only the root `.gitignore` and ignores nested ones — a gitignored secrets file
|
|
647
|
+
entered `files[]` with no warning, and the snapshot looked clean. A git checkout whose files git
|
|
648
|
+
cannot list is now an error: fix the repository rather than silently scanning something different.
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## 4.0.7
|
|
652
|
+
|
|
653
|
+
Wave-2 findings: nine independent critics, one dimension each.
|
|
654
|
+
|
|
655
|
+
### Credentials
|
|
656
|
+
- **A branch name is user input.** `git checkout -b "release2/<ghp_…>"` copied the token raw into
|
|
657
|
+
`manifest.git.branch`, the README and the audit report — a channel that never passed through
|
|
658
|
+
redaction. It is masked now.
|
|
659
|
+
- **Azure had no rules.** DevOps PATs, AD client secrets (`…8Q~…`) and SAS `sig=` values reached
|
|
660
|
+
parts and both monoliths verbatim. Added.
|
|
661
|
+
- **Missing prefixes** in otherwise exhaustive alternations: `ABIA` for AWS bearer keys, `glrt-`
|
|
662
|
+
(GitLab runner), `hvb.` (Vault batch), `1//0…` (GCP refresh), `sk_test_` (Stripe test).
|
|
663
|
+
- **`exclude` in a config** is the flag's spelling of `excludePatterns` and used to be ignored
|
|
664
|
+
silently, so a directory the user excluded was still emitted. It is accepted as an alias now.
|
|
665
|
+
|
|
666
|
+
### The documented programmatic flow scanned the wrong project
|
|
667
|
+
`resolveConfig(await loadConfigFile(dir))` ignored `dir` and generated into `process.cwd()`, exit 0.
|
|
668
|
+
The loaded config now carries the directory it came from (non-enumerable), so the documented flow
|
|
669
|
+
scans the project the config describes.
|
|
670
|
+
|
|
671
|
+
### Small lies in the CLI
|
|
672
|
+
- `--mode` was discarded when a command word was present: `generate --mode validate` wrote a
|
|
673
|
+
snapshot. A contradiction is now exit 2, and an unknown mode is an error either way.
|
|
674
|
+
- `--only-artifacts monolith` wrote nothing but the marker, because the artifact was off. Naming
|
|
675
|
+
the monolith now enables it.
|
|
676
|
+
- `init` wrote `"snapshot": "codebase-snapshot generate"` into `package.json` without installing the
|
|
677
|
+
package, so the very next line it printed (`npm run snapshot`) failed with "not recognized".
|
|
678
|
+
The scripts are `npx @ankurhalder/codebase-snapshot …` now, which works either way.
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## 4.0.6
|
|
682
|
+
|
|
683
|
+
### A NUL byte past the first 8 KB
|
|
684
|
+
Binary detection sampled 8 KB, so a file with a NUL at byte 9000 was emitted as text into a part.
|
|
685
|
+
The whole file is scanned now, up to 8 MB, in 64 KB chunks. Verified: a 20 KB file with one NUL at
|
|
686
|
+
offset 9000 is skipped as `binary`.
|
|
687
|
+
|
|
688
|
+
### Layer and kind names are masked wherever they are derived
|
|
689
|
+
Masking `config.layers` was not enough: the *derived* values were raw — `manifest.layers` keys,
|
|
690
|
+
`files[].layer`, part headers, the README parts table, the audit layer table, navigation, report and
|
|
691
|
+
`config.budgets.byLayer` keys. Eleven sinks, one root. The name is now masked where it is assigned,
|
|
692
|
+
so every derived field inherits the mask. Verified with a `ghp_…` layer name: zero occurrences in
|
|
693
|
+
any artifact.
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## 4.0.5
|
|
697
|
+
|
|
698
|
+
### `validate` now notices every configuration change
|
|
699
|
+
|
|
700
|
+
It compared a handful of settings field by field and silently ignored the rest: `layers`, `kinds`,
|
|
701
|
+
`secrets.customPatterns`, `disableDefaultPatterns`, `maxMonolithBytes`. With a config that is not
|
|
702
|
+
itself in the snapshot (`include: ["src/**"]`), changing classification or redaction rules still
|
|
703
|
+
produced "✅ valid and up to date" on a stale snapshot — the "config changes are drift" promise
|
|
704
|
+
was false for exactly the rules that decide what goes in.
|
|
705
|
+
|
|
706
|
+
The manifest now records a digest of the effective settings, computed from the raw values, and
|
|
707
|
+
`validate` compares it. That covers every setting, including the ones no field comparison lists,
|
|
708
|
+
and it works where masked values cannot: two different credential-shaped exclude patterns both
|
|
709
|
+
mask to the same placeholder, so a masked comparison would call them equal.
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
$ validate # same config
|
|
713
|
+
✅ Snapshot is valid and up to date with disk.
|
|
714
|
+
|
|
715
|
+
$ validate # after adding a layer rule
|
|
716
|
+
❌ config: configuration: the settings differ from the ones this snapshot was generated with (run generate)
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Still open, recorded rather than hidden
|
|
720
|
+
100k files take about 67 s (I/O bound); parallel runs with different `outDir`s can still hit the
|
|
721
|
+
staging lock and exit 1 instead of waiting; `--watch`'s quiet window can defer an edit made during
|
|
722
|
+
a run; layer and kind names supplied in the config are still emitted raw in derived fields; binary
|
|
723
|
+
detection samples the first 8 KB; the monolith is built before `--max-monolith-bytes` rejects it.
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## 4.0.4
|
|
727
|
+
|
|
728
|
+
Five defects found by driving the published package from another agent's environment.
|
|
729
|
+
|
|
730
|
+
### A stale shim exited 0 without doing anything
|
|
731
|
+
`dist/cli.js` exported `main()` and never called it. `dist/bin.js` is the shipped entry point, but a
|
|
732
|
+
`node_modules/.bin` shim left by an earlier install can point at `cli.js`, which then imported, did
|
|
733
|
+
nothing and exited 0 — indistinguishable from a successful run with empty output. `cli.js` now runs
|
|
734
|
+
itself when it is the entry file, and only then.
|
|
735
|
+
|
|
736
|
+
### Redaction was breaking the parser
|
|
737
|
+
Structural summaries were built from the **redacted** text, so masking an identifier such as
|
|
738
|
+
`const secret = previousSecrets[i]` produced source the TypeScript parser could not read: 969
|
|
739
|
+
"parse errors" on a codebase that has none, and incomplete summaries everywhere. The parser now
|
|
740
|
+
reads the original source, and every string that leaves a summary — exports, variables, functions,
|
|
741
|
+
classes, headings, JSON keys — is masked before it is written. Structure stays correct; nothing raw
|
|
742
|
+
reaches an artifact.
|
|
743
|
+
|
|
744
|
+
### Audit diagnostics
|
|
745
|
+
`reports/audit.md` gained a **Parse & AST Diagnostics** section listing every file whose summary
|
|
746
|
+
was incomplete, with its parse-error count. The CLI warned about these; the artifact an auditor
|
|
747
|
+
reads did not.
|
|
748
|
+
|
|
749
|
+
### Formatting
|
|
750
|
+
- Summary lines no longer mix separators (`snapshot\parts` instead of `snapshot\parts/`).
|
|
751
|
+
- Line counts use `en-US` grouping rather than the host locale, so a CI runner and a developer
|
|
752
|
+
machine print the same number.
|
|
753
|
+
|
|
754
|
+
### Known gaps, not fixed here
|
|
755
|
+
`validate` does not compare `layers`, `kinds` or `secrets.customPatterns`, so changing those does
|
|
756
|
+
not register as drift; a changed credential-shaped value compares equal because both sides mask;
|
|
757
|
+
layer and kind **names** from your config are still emitted raw in derived fields; the ownership
|
|
758
|
+
marker can record a hook-written file whose name contains a token; and binary detection samples the
|
|
759
|
+
first 8 KB only.
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## 4.0.3
|
|
763
|
+
|
|
764
|
+
### A snapshot could fail its own `validate`
|
|
765
|
+
|
|
766
|
+
4.0.2 masks config values in the manifest, which is right — an `excludePatterns` entry or an
|
|
767
|
+
`include` glob can contain a credential-shaped string. But `validate` compared that masked value
|
|
768
|
+
against the **raw** current config, so a repository that generated cleanly failed validation one
|
|
769
|
+
second later, and the mismatch line printed the very token the mask was hiding.
|
|
770
|
+
|
|
771
|
+
Both sides are now masked before comparing, and the mismatch text uses the masked form. Verified:
|
|
772
|
+
`generate` then `validate` with `excludePatterns: ["ghp_…"]` exits 0, and the token appears in no
|
|
773
|
+
file under `snapshot/`.
|
|
774
|
+
|
|
775
|
+
Known and unchanged: a layer or kind **name** supplied in the config is still emitted raw in the
|
|
776
|
+
derived fields (`manifest.layers` keys, `files[].layer`, part headers). It is your own string
|
|
777
|
+
rather than a discovered secret, but it is the same class and will be fixed with the next batch.
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
## 4.0.2
|
|
781
|
+
|
|
782
|
+
### The last place a path secret could reach an artifact
|
|
783
|
+
|
|
784
|
+
4.0.1 masked the path field of every skip record. It did not mask the **detail** beside it, and
|
|
785
|
+
that text names the link target or the exclude pattern: a symlink to `real-ghp_…` and an
|
|
786
|
+
`excludePatterns: ["ghp_…"]` both printed the token in `manifest.skipped[].detail`, the audit
|
|
787
|
+
table and `explain`. Config values recorded for drift detection (`include`, `excludePatterns`,
|
|
788
|
+
layer and kind names) had the same problem. All of them now go through the same rules.
|
|
789
|
+
|
|
790
|
+
### `--watch` idles again
|
|
791
|
+
|
|
792
|
+
After 4.0.1's staging changes the watcher fed itself: 15 regenerations in 8 seconds with no edits,
|
|
793
|
+
because Windows reports its own writes with paths the segment filter did not recognise. The
|
|
794
|
+
watcher now also stays quiet for a short window after each run. Measured: one regeneration in nine
|
|
795
|
+
idle seconds, and an edit still triggers exactly one more.
|
|
796
|
+
|
|
797
|
+
### Honest answers where there used to be silence
|
|
798
|
+
|
|
799
|
+
- `loadConfigFile()` returned `null` for a config that existed but could not be parsed, so
|
|
800
|
+
`resolveConfig(await loadConfigFile(cwd))` — the documented example — silently generated on
|
|
801
|
+
defaults. It now throws, and `loadConfigFileDetailed` already reported the reason.
|
|
802
|
+
- Custom secret rules are no longer stored in a module-level variable: a `collectFiles` call for
|
|
803
|
+
one project applied the previous project's rules. Every caller passes its own.
|
|
804
|
+
- `describe` lists `.snapshotrc.json` before `.snapshotrc`, matching the real precedence.
|
|
805
|
+
|
|
806
|
+
### Measured this round
|
|
807
|
+
|
|
808
|
+
100k files: 56.6 s (was 336.4 s in 4.0.0), 20k: 11.8 s, 100k `validate`: 50.3 s, 100k `report`:
|
|
809
|
+
1.28 s, 50 MB single TypeScript file: 1.33 s. About 60% of the remaining time is Windows file I/O
|
|
810
|
+
— read, stat and open — not parsing or redaction.
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
## 4.0.1
|
|
814
|
+
|
|
815
|
+
### A credential in a file name no longer reaches the snapshot
|
|
816
|
+
|
|
817
|
+
A path that contained a token was masked only when the file was skipped *for that reason*. If it
|
|
818
|
+
was skipped earlier — as a credential-bearing file (`certs/AKIA….key`), by `.gitignore`, by a user
|
|
819
|
+
exclude or by the built-in ignore list — the raw path was written into `manifest.skipped[].path`
|
|
820
|
+
and the audit table, and a binary file whose name carried a token still reached the manifest as
|
|
821
|
+
`binary`. Custom `secrets.customPatterns` were not applied to paths at all, so a file named
|
|
822
|
+
`src/CORP-<32>.ts` was *included* and the token landed in the parts, the navigation index and the
|
|
823
|
+
project tree — the payload written to be pasted into a model.
|
|
824
|
+
|
|
825
|
+
Path masking now runs over every skip record regardless of reason, and the included-file check
|
|
826
|
+
uses the default **and** custom rules, so a custom-rule hit becomes a `sensitive-path` skip with a
|
|
827
|
+
masked path. A tested fixture with all four shapes produces zero occurrences of either token in
|
|
828
|
+
any file under `snapshot/`.
|
|
829
|
+
|
|
830
|
+
### `report` no longer re-scans the repository
|
|
831
|
+
|
|
832
|
+
`report` printed an integrity verdict by calling the validator, which re-reads every file: 4.5 s
|
|
833
|
+
on a 20 000-file repository to print a summary already on disk. It now reads `manifest.json` and
|
|
834
|
+
nothing else — **0.54 s** on the same repository — and says plainly that drift is `validate`'s job.
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## 4.0.0
|
|
838
|
+
|
|
839
|
+
### `snap` is back
|
|
840
|
+
|
|
841
|
+
3.0.0 removed the `snap` alias after I misdiagnosed why `npx @ankurhalder/codebase-snapshot`
|
|
842
|
+
printed nothing. **The removal was unnecessary and this release reverses it** — both
|
|
843
|
+
`codebase-snapshot` and `snap` are published again.
|
|
844
|
+
|
|
845
|
+
The real cause, confirmed by reading npm's own resolver
|
|
846
|
+
(`libnpmexec/lib/get-bin-from-manifest.js`): npm resolves a package from its `bin` entries, and
|
|
847
|
+
when every entry points at the same file it treats them as aliases and resolves one. Two
|
|
848
|
+
aliases were always fine. What actually broke the bare command was a **stale global install on
|
|
849
|
+
the test machine** — a leftover `npm link` back to the development checkout, which made `npx`
|
|
850
|
+
run that instead of the published package. A clean profile runs the same command correctly, with
|
|
851
|
+
either one or two aliases.
|
|
852
|
+
|
|
853
|
+
If you ever hit a silent exit 0:
|
|
854
|
+
|
|
855
|
+
```bash
|
|
856
|
+
npm ls -g --depth=0 | grep codebase-snapshot
|
|
857
|
+
npm rm -g @ankurhalder/codebase-snapshot
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Security: the redaction layer was rebuilt
|
|
861
|
+
|
|
862
|
+
Independent adversarial review found credentials reaching `parts/*.md` while `audit.md` reported
|
|
863
|
+
them as redacted. That is the worst failure this tool can have — its output is written to be
|
|
864
|
+
pasted into a model — so the layer was rewritten rather than patched.
|
|
865
|
+
|
|
866
|
+
Leaks that were real and are now closed, each with a regression test:
|
|
867
|
+
|
|
868
|
+
- **CRLF checkouts.** Line-oriented rules ended in `$`, and a `\r` before the newline stopped the
|
|
869
|
+
match, so `export TOKEN=…` in a Windows checkout printed in the clear while the byte-identical
|
|
870
|
+
LF file was redacted. Scanning now splits on `\r\n`, `\n` and `\r` and preserves line numbers.
|
|
871
|
+
- **Trailing comments and trailing spaces.** `export TOKEN=abc # rotate quarterly` leaked because
|
|
872
|
+
the value had to be the last thing on the line. The rule now matches the value, not the tail.
|
|
873
|
+
- **camelCase keys and short values.** `awsSecret`, `dbPassword: "hunter2"` and similar were never
|
|
874
|
+
matched because only snake_case spellings were. Credential **names** now drive matching, and the
|
|
875
|
+
value is masked regardless of length.
|
|
876
|
+
- **Unquoted YAML / docker-compose.** `POSTGRES_PASSWORD: hunter2` is now covered, while
|
|
877
|
+
expressions (`password: req.body.password`, `getToken()`, `process.env.SECRET`) are explicitly
|
|
878
|
+
not.
|
|
879
|
+
- **Sensitive file names.** `.env~`, `.env.swp`, `.npmrc ` (trailing space), `.vault-token`,
|
|
880
|
+
`prod.env`, `secrets.toml` and friends are denied; deny-list matching normalises the name
|
|
881
|
+
instead of anchoring on the exact spelling.
|
|
882
|
+
- **Secrets in a path.** A filename containing a token is skipped as `sensitive-path` with the
|
|
883
|
+
path span-masked, instead of being printed in the tree, the navigation index and the manifest.
|
|
884
|
+
- **Authorization headers, and nine vendor prefixes** (`glpat-`, `hvs.`, `s.`, `xoxc/xoxd`,
|
|
885
|
+
`whsec_`, `dckr_pat_`, `hf_`, `ya29.`) are now detected.
|
|
886
|
+
|
|
887
|
+
Masking is **span-only**: `dbPassword: "[REDACTED…]"` keeps the key and the rest of the line. That
|
|
888
|
+
also fixed the opposite defect — over-redaction that corrupted ordinary code (`id-token: write`,
|
|
889
|
+
`{ pass: 3 }`, and an unrelated string on a line that also contained a credential) is now
|
|
890
|
+
explicitly negative-tested.
|
|
891
|
+
|
|
892
|
+
The built-in rule count is now **29** (was 21).
|
|
893
|
+
|
|
894
|
+
**Known limitations, stated rather than hidden** (see [SECURITY.md](SECURITY.md)): detection is
|
|
895
|
+
pattern-based, so a secret deliberately split across two string literals is not caught, and
|
|
896
|
+
unusual credential shapes can still be missed. Do not treat a clean audit as a guarantee.
|
|
897
|
+
|
|
898
|
+
### The ownership marker is now evidence, not a filename
|
|
899
|
+
|
|
900
|
+
`generate` deletes the previous snapshot, and it decides whether it is allowed to by checking
|
|
901
|
+
whether it wrote that directory. The check was forgeable: an empty `.codebase-snapshot` file, or a
|
|
902
|
+
three-key `manifest.json`, was enough to make the tool delete a directory full of someone else's
|
|
903
|
+
files.
|
|
904
|
+
|
|
905
|
+
`.codebase-snapshot` is now a JSON record — generator name, schema version, a random run id, a
|
|
906
|
+
state, a digest of the manifest and an inventory of everything written. Replacement requires the
|
|
907
|
+
marker to be complete and every file in the directory to be in that inventory; anything else
|
|
908
|
+
blocks the run and is named in the message. The same evidence rule now applies to `tmpDir`, which
|
|
909
|
+
used to be deleted wholesale. Concurrent runs take a per-`outDir` lock, so four simultaneous
|
|
910
|
+
`generate` invocations leave one complete snapshot rather than a manifest describing parts that
|
|
911
|
+
are gone.
|
|
912
|
+
|
|
913
|
+
### Correctness
|
|
914
|
+
|
|
915
|
+
- **JSONC comments in strings.** A valid `package.json` containing `//` anywhere in a string
|
|
916
|
+
(`"build": "tsc && echo done//ok"`) made every command fail with "the config could not be
|
|
917
|
+
read". The stripper is now a state machine that respects strings and escapes.
|
|
918
|
+
- **Configs that are not objects.** `export default Promise.resolve({…})` was silently treated as
|
|
919
|
+
an empty config; a thenable, array or primitive is now a hard error naming what it received.
|
|
920
|
+
- **Wrong-typed settings.** `excludePatterns: "src/**"` and `previewHeadLines: 0` were silently
|
|
921
|
+
ignored. Every setting is now validated, and the error names the setting, the expected shape and
|
|
922
|
+
what arrived.
|
|
923
|
+
- **`layers` rejected the documented shape.** `{ name: "api", match: … }` — the shape the type
|
|
924
|
+
declares and the generated config recommends — crashed `generate` with a `TypeError`. Both
|
|
925
|
+
`layers` and `kinds` are validated up front, `name` is canonical for layers with `layer` as an
|
|
926
|
+
alias, and a bad rule is an error naming the index rather than a crash.
|
|
927
|
+
- **`preset: "detected"`**, which `describe` published as the default, was rejected as an unknown
|
|
928
|
+
preset. `detected` and `auto` are now accepted.
|
|
929
|
+
- **Budget precedence.** `--max-part-bytes` lost to `budgets.default`, so the banner said "Part
|
|
930
|
+
budget: 4 KB" while 151 KB parts were written. The order is now CLI > config `maxPartBytes` >
|
|
931
|
+
`budgets.default` > 300 KB, and `budgets.byLayer` is capped by the chosen budget.
|
|
932
|
+
- **The output directory was scanned into the next snapshot.** `--out-dir .context` produced a
|
|
933
|
+
snapshot that included its own previous manifest, so the fingerprint changed on every run and
|
|
934
|
+
`validate` failed immediately after `generate`. The resolved `outDir`/`tmpDir` are excluded as
|
|
935
|
+
tool-owned paths, silently and identically on the first run.
|
|
936
|
+
- **The monolith contained previews.** `full-codebase.md` was supposed to be the whole repository
|
|
937
|
+
in one file, but a 400 KB file contributed about 3 KB — its head/tail preview. The monolith now
|
|
938
|
+
carries full redacted bodies; `largeFileBytes` governs the parts only.
|
|
939
|
+
- **A failing hook disappeared.** `hooks.afterGenerate` throwing printed a warning, but the
|
|
940
|
+
manifest had already been written, so the warning was not in it. The manifest is now rewritten
|
|
941
|
+
with the final warning list (the hook still runs once).
|
|
942
|
+
- **`--out-dir` with a missing parent** failed with a raw `ENOENT` from the rename; parents are
|
|
943
|
+
now created, an ancestor that is a file is refused cleanly, and a failed run no longer leaves
|
|
944
|
+
`snapshot.tmp` behind.
|
|
945
|
+
- **`validate` exempted skipped files forever.** A file skipped as binary, then rewritten as
|
|
946
|
+
source, was never reported as new. Skips are re-evaluated.
|
|
947
|
+
- **`--dry-run` skipped the output guard**, claiming it would write into a directory the real run
|
|
948
|
+
refuses. A dry run now runs the same guard and exits the same way.
|
|
949
|
+
- **`diff` reported `before undefined undefined file(s)` and exited 0** on a manifest that was not
|
|
950
|
+
a snapshot. Both sides are validated and the malformed one is named.
|
|
951
|
+
- **`reactComponents` listed plain classes.** A component now requires JSX/`createElement` or a
|
|
952
|
+
React base class.
|
|
953
|
+
- **A filename could escape a code fence.** A file named ``` ``` ``` closed the `## Files` fence
|
|
954
|
+
and let the next filename be rendered as live markdown — a prompt-injection vector in a file
|
|
955
|
+
built for LLMs. Fences are now chosen from their content.
|
|
956
|
+
- **`--watch` never reloaded the config.** Editing the config while watching appeared to work
|
|
957
|
+
while the old settings stayed in force; a broken config regenerated silently instead of
|
|
958
|
+
reporting the error.
|
|
959
|
+
- **`reports/audit.md` said "Redacted Content Bytes"** when it meant emitted bytes. Renamed
|
|
960
|
+
"Emitted Content Bytes".
|
|
961
|
+
|
|
962
|
+
### CLI honesty
|
|
963
|
+
|
|
964
|
+
- **Options that do not apply to a command are now rejected** (exit 2) instead of being parsed and
|
|
965
|
+
ignored: `--watch` is generate-only, `--dry-run` is generate/init, config and size options do
|
|
966
|
+
not apply to `diff`/`describe`/`skill`. The published `appliesTo` column is now generated from
|
|
967
|
+
the same table the parser enforces, so the contract cannot drift from the CLI again.
|
|
968
|
+
- **`--no-config`** ignores `snapshot.config.*` and runs on the built-in defaults — useful in a
|
|
969
|
+
checkout you do not trust, because a `.ts`/`.js`/`.mjs`/`.cjs` config is imported and executed.
|
|
970
|
+
- **`explain` always exits 0** when it can answer; the verdict is `included`/`reason`. Exit 1 now
|
|
971
|
+
only means the command could not run.
|
|
972
|
+
- **`skill --format json`** emits JSON instead of markdown.
|
|
973
|
+
- **`init` writes `includeMonolith`** (the file now lists every setting, as it claims), honours
|
|
974
|
+
`--out-dir`, refuses to silently override a `package.json#snapshot` config, and no longer says
|
|
975
|
+
`Next: npm run snapshot` in a directory with no `package.json`.
|
|
976
|
+
|
|
977
|
+
### Upgrading from 3.0.0
|
|
978
|
+
|
|
979
|
+
- **Old snapshots are refused, not replaced.** A snapshot written by 3.0.0 carries a marker in the
|
|
980
|
+
old format, so `generate` will stop and ask you to remove the directory or point `--out-dir`
|
|
981
|
+
somewhere else. This is deliberate: the old marker could not prove the directory was ours.
|
|
982
|
+
- `explain` no longer exits 1 for a file that would be skipped.
|
|
983
|
+
- Attention-only flags (`--watch` on `validate`, for example) are now usage errors.
|
|
984
|
+
- With `--include-monolith`, the monolith is much larger, because it now actually contains the
|
|
985
|
+
code.
|
|
986
|
+
|
|
987
|
+
### Documentation and packaging
|
|
988
|
+
|
|
989
|
+
Every document was rewritten against the current behaviour rather than patched:
|
|
990
|
+
|
|
991
|
+
- **New** [docs/cli.md](docs/cli.md) — every command, every option with its default and which
|
|
992
|
+
commands it applies to, exit codes, output streams, worked examples.
|
|
993
|
+
- **New** [docs/output.md](docs/output.md) — the snapshot layout and the complete
|
|
994
|
+
`manifest.json` schema, field by field, including how to read `skipped[]`.
|
|
995
|
+
- **New** [docs/troubleshooting.md](docs/troubleshooting.md) — every message the tool prints,
|
|
996
|
+
its cause and its fix.
|
|
997
|
+
- **Rewritten** README, [docs/install.md](docs/install.md),
|
|
998
|
+
[docs/configuration.md](docs/configuration.md) and [docs/agent.md](docs/agent.md).
|
|
999
|
+
- **New** [AGENTS.md](AGENTS.md) for agents working in the repository and
|
|
1000
|
+
[llms.txt](llms.txt) for AI discovery.
|
|
1001
|
+
- **New** CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, issue forms and a pull request
|
|
1002
|
+
template.
|
|
1003
|
+
|
|
1004
|
+
Corrections worth calling out, because the old text was wrong:
|
|
1005
|
+
|
|
1006
|
+
- The generated config was documented as importing `defineConfig`; it has been a plain object
|
|
1007
|
+
with no imports since 2.2.0, so that it loads when the package is only used through `npx`.
|
|
1008
|
+
- The "bin-less placeholder" explanation for the bare `npx` failure has been removed. The
|
|
1009
|
+
placeholder version is unrelated and harmless.
|
|
1010
|
+
- `snap init` writes an `.mjs` config for a CommonJS project, not a `.ts` one — the
|
|
1011
|
+
documentation now states the rule.
|
|
1012
|
+
|
|
1013
|
+
Packaging: `prepare` was replaced with `prepack`, so installing the package no longer triggers an
|
|
1014
|
+
install-script warning, and the published tarball is still built from source. The test suite now
|
|
1015
|
+
includes an end-to-end agent simulation that follows the six workflow steps published by
|
|
1016
|
+
`describe` and asserts the resulting snapshot.
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## 3.0.0
|
|
1021
|
+
|
|
1022
|
+
### BREAKING: the `snap` executable is gone; use `codebase-snapshot`
|
|
1023
|
+
|
|
1024
|
+
The package now ships **exactly one** executable, `codebase-snapshot`.
|
|
1025
|
+
|
|
1026
|
+
This is what makes the bare form work. `npx @ankurhalder/codebase-snapshot generate`
|
|
1027
|
+
used to exit 0 printing nothing, on every npm version and every OS, because of how npm
|
|
1028
|
+
picks a binary: it matches the command you typed against the package's `bin` keys, and no
|
|
1029
|
+
key can contain an `@scope/`. With two executables (`snap` and `codebase-snapshot`)
|
|
1030
|
+
nothing matched, so nothing ran. With one, npm uses it regardless of its name.
|
|
1031
|
+
|
|
1032
|
+
Verify the rule against packages you know: `@sentry/cli`, `@babel/cli` and
|
|
1033
|
+
`@changesets/cli` all ship one executable and run fine by bare name;
|
|
1034
|
+
`@swc/cli` ships three and fails with "could not determine executable to run";
|
|
1035
|
+
`npm-check-updates` ships two but is unscoped, so its name matches one of its bins.
|
|
1036
|
+
|
|
1037
|
+
Migration:
|
|
1038
|
+
|
|
1039
|
+
| Before | After |
|
|
1040
|
+
| :--- | :--- |
|
|
1041
|
+
| `snap generate` | `codebase-snapshot generate` |
|
|
1042
|
+
| `npx snap` | `npx codebase-snapshot` |
|
|
1043
|
+
| `npm run snapshot` (generated by `init`) | unchanged — it already used `codebase-snapshot` |
|
|
1044
|
+
|
|
1045
|
+
Nothing else in the API changed: the library exports, the config format, the manifest
|
|
1046
|
+
schema and every command and option are identical.
|
|
1047
|
+
|
|
1048
|
+
### Changed
|
|
1049
|
+
|
|
1050
|
+
- The agent contract no longer claims the bare spec is unusable. It documents one
|
|
1051
|
+
executable and explains why, so an agent does not add a pin it does not need.
|
|
1052
|
+
- `docs/install.md` explains the single-executable rule instead of the previous
|
|
1053
|
+
(incorrect) "bin-less placeholder" theory.
|
|
1054
|
+
|
|
1055
|
+
### Note on `0.0.0-stage`
|
|
1056
|
+
|
|
1057
|
+
Earlier notes suggested unpublishing a `0.0.0-stage` placeholder to fix the bare form.
|
|
1058
|
+
That diagnosis was wrong — the placeholder is unrelated and can stay. The npm behaviour
|
|
1059
|
+
above is the real cause, and the single-executable change fixes it for everyone with no
|
|
1060
|
+
registry surgery.
|
|
1061
|
+
|
|
1062
|
+
---
|
|
1063
|
+
|
|
1064
|
+
Built for AI agents. Everything an agent needs is now available as data, and the tool can
|
|
1065
|
+
install its own skill where agents look for it.
|
|
1066
|
+
|
|
1067
|
+
### Added
|
|
1068
|
+
|
|
1069
|
+
- **snap describe** — a machine-readable contract of the whole tool: the invocation that
|
|
1070
|
+
works, an ordered workflow array, every command, every option with its default, the
|
|
1071
|
+
exit-code contract, all 25 presets, the artifact list, config file names, output layout,
|
|
1072
|
+
safety rules and a troubleshooting table. --format json for machines, text for humans.
|
|
1073
|
+
Generated from the same code the CLI runs; a test compares its documented defaults
|
|
1074
|
+
against a real `generate --dry-run` manifest so it cannot drift into lying.
|
|
1075
|
+
- **snap skill** — prints the bundled agent skill (to stdout, so it can be redirected),
|
|
1076
|
+
or with `--install` writes it to `AGENTS.md`, `.claude/skills/codebase-snapshot/SKILL.md`
|
|
1077
|
+
and `.cursor/rules/codebase-snapshot.mdc`. `--target claude|agents|cursor|all`.
|
|
1078
|
+
The install is idempotent and replaces only its own marked block, so an existing
|
|
1079
|
+
`AGENTS.md` keeps its conventions.
|
|
1080
|
+
- `snap init` now points at `skill --install`.
|
|
1081
|
+
- [docs/agent.md](docs/agent.md) explains the contract and the skill for humans.
|
|
1082
|
+
|
|
1083
|
+
### Why
|
|
1084
|
+
|
|
1085
|
+
`--help` is prose for a person. An agent told "use npx @ankurhalder/codebase-snapshot"
|
|
1086
|
+
had to guess option names and failure modes. `describe` removes the guessing, and the
|
|
1087
|
+
skill removes the discovery problem: once installed, an agent finds the tool without being
|
|
1088
|
+
told it exists.
|
|
1089
|
+
|
|
1090
|
+
### Tests
|
|
1091
|
+
|
|
1092
|
+
138 pass (130 before): the contract's shape and defaults, skill output and frontmatter,
|
|
1093
|
+
installation to all three targets, idempotency, and that an existing `AGENTS.md` is
|
|
1094
|
+
preserved.
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
## 2.1.1
|
|
1099
|
+
|
|
1100
|
+
A correctness and contract pass driven by an independent CLI audit of 2.1.0. No new
|
|
1101
|
+
features; every item below was reproduced against the released build first.
|
|
1102
|
+
|
|
1103
|
+
### Fixed
|
|
1104
|
+
|
|
1105
|
+
- **`validate` never printed the bookkeeping mismatches it counted.** It reported
|
|
1106
|
+
"1 manifest bookkeeping value(s) disagree" without saying which one, while
|
|
1107
|
+
`--format json` carried the detail. The text output now names them
|
|
1108
|
+
(`counts.totalFiles: manifest 999 vs actual 5`).
|
|
1109
|
+
- **`generate` claimed artifacts it had not written.** `--only-artifacts parts` still
|
|
1110
|
+
printed a `Reports:` line and a `Manifest:` line; `--skip-artifacts manifest` still
|
|
1111
|
+
printed `Manifest:`. The summary now reflects the selection, and an all-disabled run
|
|
1112
|
+
says so instead of listing files that do not exist.
|
|
1113
|
+
- **`diff` leaked raw Node errors** — `ENOENT: no such file or directory, stat …` for a
|
|
1114
|
+
missing path, and `Cannot read properties of undefined (reading 'totalFiles')` for a
|
|
1115
|
+
parseable but foreign manifest. Both are actionable messages now.
|
|
1116
|
+
- **`validate` double-counted a tracked-but-deleted file**: it appeared in
|
|
1117
|
+
`missingOnDisk` twice, so the text said "2 file(s) no longer exist" for one file.
|
|
1118
|
+
- **A bad `--cwd` exited 1** although the documented contract reserves 2 for usage
|
|
1119
|
+
errors. A missing directory or a file passed as `--cwd` is now exit 2.
|
|
1120
|
+
- **`--max-part-bytes -5`** produced the parser's "option argument is ambiguous" text;
|
|
1121
|
+
it now says the value looks like another flag and points at the `--option=value` form.
|
|
1122
|
+
- **Empty option values silently meant "use the default"**, so `--out-dir "$UNSET"`
|
|
1123
|
+
wrote to `./snapshot` instead of failing. Every string option now rejects an empty
|
|
1124
|
+
value with a usage error.
|
|
1125
|
+
- **A detached HEAD was recorded as branch `"HEAD"`**, then `validate` warned about it
|
|
1126
|
+
forever. A detached HEAD cannot name a branch, so the configured fallback is used.
|
|
1127
|
+
- **A read-only output directory produced a bare `EPERM`**. It now says the directory is
|
|
1128
|
+
not writable and suggests `--out-dir`; the previous snapshot still survives.
|
|
1129
|
+
- **A wrong-typed `"snapshot"` key in `package.json` was silently dropped.** It now
|
|
1130
|
+
warns. It stays non-fatal on purpose — other tools use that key too.
|
|
1131
|
+
- The all-artifacts-disabled warning claimed the directory "will be empty", which
|
|
1132
|
+
stopped being true once the ownership marker was introduced.
|
|
1133
|
+
|
|
1134
|
+
### Tests
|
|
1135
|
+
|
|
1136
|
+
130 pass (119 before), including a POSIX-only check that a read-only output directory
|
|
1137
|
+
fails with a clear message.
|
|
1138
|
+
|
|
1139
|
+
---
|
|
1140
|
+
|
|
1141
|
+
## 2.1.0
|
|
1142
|
+
|
|
1143
|
+
Multi-project support and an interactive setup.
|
|
1144
|
+
|
|
1145
|
+
### Added
|
|
1146
|
+
|
|
1147
|
+
- **Interactive setup.** `npm create codebase-snapshot` (or `npx @ankurhalder/codebase-snapshot init`)
|
|
1148
|
+
detects the project, walks every tunable parameter, and writes the config. Every
|
|
1149
|
+
prompt has a flag, so it works non-interactively in CI.
|
|
1150
|
+
- **A self-documenting config.** The generated `snapshot.config.mjs` lists *every*
|
|
1151
|
+
setting at its current value, commented and grouped — not just the ones you
|
|
1152
|
+
changed. Deleting any line is safe. `--minimal` writes the terse variant.
|
|
1153
|
+
- **25 presets**, up from 4: nextjs, nuxt, sveltekit, remix, astro, angular,
|
|
1154
|
+
solid, vue-vite, react-vite, nest, node-express, node-lib, python-django,
|
|
1155
|
+
python-fastapi, python-lib, go, rust, java-spring, dotnet, php-laravel,
|
|
1156
|
+
ruby-rails, monorepo, infra, docs, generic.
|
|
1157
|
+
- **Per-ecosystem ignores** contributed by each preset — `__pycache__`, `.venv`,
|
|
1158
|
+
`.mypy_cache` (Python), `target` (Rust), `.gradle` (JVM), `bin`/`obj` (.NET),
|
|
1159
|
+
`vendor`/`storage` (PHP), `.svelte-kit`/`.angular`/`.astro` (JS frameworks) —
|
|
1160
|
+
merged with `excludePatterns` and still recorded per file.
|
|
1161
|
+
- **Structural summaries for non-TypeScript languages.** Python, Go, Rust,
|
|
1162
|
+
Java/Kotlin/Scala, C#, PHP, Ruby and C/C++ now get imports, declarations and
|
|
1163
|
+
exports. Vue/Svelte/Astro components have their `<script>` block handed to the
|
|
1164
|
+
real TypeScript compiler.
|
|
1165
|
+
- **`summaryQuality`** (`ast` / `structural` / `heuristic`) recorded per file and
|
|
1166
|
+
rendered in every part section, so a line-scan is never presented as a parse.
|
|
1167
|
+
- **`preset`** option to force a preset by name; an unknown name is an error with
|
|
1168
|
+
the full list rather than a silent fallback.
|
|
1169
|
+
|
|
1170
|
+
### Changed
|
|
1171
|
+
|
|
1172
|
+
- Preset detection now reads the repository once per run and shares that view
|
|
1173
|
+
with every detector. `peerDependencies` and `optionalDependencies` count.
|
|
1174
|
+
- A bare `package.json` ranks below any definitive language manifest, so a Go or
|
|
1175
|
+
Rust repo with a `package.json` for tooling is not mistaken for a Node library.
|
|
1176
|
+
|
|
1177
|
+
### Fixed
|
|
1178
|
+
|
|
1179
|
+
- **`outDir`/`tmpDir` pointing at an existing directory could destroy it.** A
|
|
1180
|
+
config with `outDir: "src"` or `tmpDir: "src"` deleted the user's source tree
|
|
1181
|
+
and exited 0: the scratch directory was removed outright, and the atomic swap
|
|
1182
|
+
renamed the directory aside, moved the snapshot in, then deleted the backup. A
|
|
1183
|
+
regular file named `snapshot` was destroyed the same way. Both directories are
|
|
1184
|
+
now refused unless they are empty, hold only placeholder dotfiles, or carry
|
|
1185
|
+
proof that a previous run wrote them (a `.codebase-snapshot` marker or a
|
|
1186
|
+
parseable `manifest.json`). Filenames alone are never treated as proof, so an
|
|
1187
|
+
existing `docs/README.md` is safe and hook-written artifacts do not block
|
|
1188
|
+
reruns.
|
|
1189
|
+
- **A config that could not be parsed was silently ignored** — a typo produced a
|
|
1190
|
+
snapshot with none of the developer's settings and exit 0. Unparseable configs
|
|
1191
|
+
are now a hard error (exit 1, nothing written). A `.ts` config on a Node without
|
|
1192
|
+
type stripping is still only a warning.
|
|
1193
|
+
- **Absolute `--out-dir` was honoured by `generate` but not by the readers.**
|
|
1194
|
+
`validate`, `report` and `explain` joined instead of resolving the path, so they
|
|
1195
|
+
looked for `<cwd>\<absolute path>` and reported a missing manifest immediately
|
|
1196
|
+
after a successful generate.
|
|
1197
|
+
- **`--preset`/`--include`/`--exclude`/`--yes`/`--minimal`/`--force` were accepted
|
|
1198
|
+
and ignored outside `init`.** `snap generate --preset nextjs` silently used the
|
|
1199
|
+
detected preset. These are now a usage error (exit 2) on any other command.
|
|
1200
|
+
- **`init --preset <typo>` silently fell back to detection**, writing a config for
|
|
1201
|
+
a preset nobody asked for. It is now a usage error listing every valid preset.
|
|
1202
|
+
- **Astro components were summarized as markup.** The leading `---` fence — where
|
|
1203
|
+
the TypeScript actually lives — was not parsed, so the common case degraded to
|
|
1204
|
+
`heuristic` quality. It now goes through the compiler like Vue and Svelte.
|
|
1205
|
+
- Summaries for `.jsonc` strip comments instead of reporting `invalid-json`.
|
|
1206
|
+
- `export default function Foo` records both `Foo` and `default`.
|
|
1207
|
+
- Parser diagnostics are counted and surfaced as warnings.
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
## 2.0.0
|
|
1212
|
+
|
|
1213
|
+
Configurable snapshots, `explain`/`diff` commands and correctness fixes.
|
|
1214
|
+
|
|
1215
|
+
### Added
|
|
1216
|
+
|
|
1217
|
+
- `artifacts`: switch each of the seven artifacts off individually, with
|
|
1218
|
+
`--skip-artifacts` and `--only-artifacts`.
|
|
1219
|
+
- `include`: allow-list globs, applied before `excludePatterns`.
|
|
1220
|
+
- `budgets.byLayer`: per-layer part budgets.
|
|
1221
|
+
- `layersMode`: user layers merge with the detected preset by default.
|
|
1222
|
+
- `hooks.afterGenerate`.
|
|
1223
|
+
- `snap explain <path>` and `snap diff <a> <b>`.
|
|
1224
|
+
- `generate --watch`.
|
|
1225
|
+
- Byte budgets accept human sizes (`120k`, `1.5mb`) from CLI and config.
|
|
1226
|
+
- Globs support brace groups and character classes (`*.{ts,tsx}`, `[abc]`).
|
|
1227
|
+
|
|
1228
|
+
### Fixed
|
|
1229
|
+
|
|
1230
|
+
- The monolith is built from pre-partition content, so `full-codebase.*` really is
|
|
1231
|
+
the whole codebase even when a part truncates a file.
|
|
1232
|
+
- The part byte bound is enforced after the split-pass cap instead of being
|
|
1233
|
+
silently exceeded.
|
|
1234
|
+
- Default-ignored files are recorded with `reason: "default-ignore"` instead of
|
|
1235
|
+
vanishing, including leftover `snapshot.backup-*` directories.
|
|
1236
|
+
- Watch mode no longer retriggers itself on its own writes.
|
|
1237
|
+
|
|
1238
|
+
---
|
|
1239
|
+
|
|
1240
|
+
## 1.0.0
|
|
1241
|
+
|
|
1242
|
+
Initial release.
|