codebase-snapshot 0.0.0-stage → 4.5.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 ADDED
@@ -0,0 +1,1242 @@
1
+ # Changelog
2
+
3
+ All notable changes to `codebase-snapshot`.
4
+
5
+ ---
6
+
7
+ ## 4.5.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 4.5.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 4.5.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.