codebase-snapshot 0.9.8 → 0.9.12

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 CHANGED
@@ -21,6 +21,542 @@ Two versions exist on the registry that are not releases, and both are deprecate
21
21
  Deleting them requires account-level rights that the release pipeline does not have, so they are
22
22
  deprecated with an explanatory message instead. `latest` has pointed at 0.9.0 since it existed.
23
23
 
24
+ ---
25
+
26
+ ## 0.9.12
27
+
28
+ A third audit, this time of what the snapshot *costs* as well as what it gets wrong. Two of the
29
+ defects were circular — `validate` demanded a flag it refused to accept — and one of them meant a
30
+ config file could not turn a setting on at all. Every item below has a test that fails without it.
31
+
32
+ ### The parts were 32 000 tokens of duplicated metadata
33
+
34
+ Each file section carried nine metadata bullets and, for a TS/JSON/Markdown file, a
35
+ pretty-printed JSON summary of code that followed three lines later. On this repository — 234
36
+ files — that was **195 619 bytes, about 32 600 tokens, 12 % of the snapshot**, spent on a
37
+ `SHA-256:` line per file and on listing every variable and export immediately above the code that
38
+ declares it.
39
+
40
+ A section is now an anchor, a heading and one line:
41
+
42
+ ```text
43
+ - Meta: src/other | typescript | full | 3 lines (63 B)
44
+ ```
45
+
46
+ - The per-file `SHA-256` is **gone from the parts** and stays in `manifest.json`, which is the file
47
+ a machine reads. `snapshot_read` used to fall back to it when a path was too long for the
48
+ heading; it now matches the `<a id>` anchor, which is a digest of the exact `relativePath` and was
49
+ already there — exact even for the 300-character paths the heading caps.
50
+ - The structural summary is **not printed for a `full` file**: `manifest.json` keeps it, and
51
+ `preview` and `signatures` sections keep it, because there the body is not the file. A one-line
52
+ string summary survives either way.
53
+ - `- Content Fingerprint:` stays in the part header (~22 tokens per part), which anchors an answer
54
+ to a verifiable state of the repository.
55
+
56
+ Nothing else about the format moved, so `snapshot_read` and both structural parsers still read it.
57
+
58
+ ### `validate` refused the flag its own comparison needed
59
+
60
+ `manifest.config.digest` hashes `skeleton`, `sarif`, `maxPartTokens`, `changedSince`, `staged` and
61
+ `withDependents`, and `validate` compared that digest — but `COMMAND_OPTION_SCOPE` allowed those six
62
+ flags on `generate` only. So a snapshot generated with `--skeleton`:
63
+
64
+ 1. failed `validate` with "the settings differ from the ones this snapshot was generated with", and
65
+ 2. failed `validate --skeleton` with "`--skeleton` does not apply to validate".
66
+
67
+ There was no way to validate it. `validate` now accepts all six, `explain` accepts the three that
68
+ decide its answer, and `report` accepts none: it reads the manifest and prints what is there, so a
69
+ generation setting would change nothing.
70
+
71
+ ### A config file could not turn these four settings on
72
+
73
+ `parseArgs` returned `skeleton: values.skeleton === true`, so a run that did not pass the flag sent
74
+ **`false`** — and `resolveConfig` reads these as `cliOptions.x ?? config.x`, where `false` is an
75
+ explicit override. `skeleton: true` in `snapshot.config.ts` was silently turned off by every run
76
+ that did not repeat the flag on the command line. The same applied to `staged`, `withDependents`
77
+ and `sarif`. All four are now `undefined` when the flag is absent.
78
+
79
+ ### `explain` and `validate` ignored `--with-dependents`
80
+
81
+ The generator widened a changed-files scope by one hop of importers; both readers compared the raw
82
+ path set. A file that was **in** the snapshot because it imported something that changed was
83
+ explained as `included: false, reason: "unchanged"`, and `validate` called the snapshot current
84
+ while a file it should contain was missing from it. The import graph moved to
85
+ `src/scanner/dependents.ts` so all three apply one rule, and `validate` now re-parses a recorded
86
+ `unchanged` skip only when the manifest says the run used `--with-dependents`.
87
+
88
+ ### `setup` wrote a config its own snapshot did not match
89
+
90
+ `setup --discovery walk --large-file-bytes 100000` forwarded both flags to the generation it runs
91
+ and wrote the defaults into the config beside it, so a brand-new project's one-command setup
92
+ finished with a snapshot that `validate` immediately called drifted — three configuration
93
+ mismatches on the first run. `--max-part-tokens`, `--skeleton` and `--sarif` are now accepted by
94
+ `setup` too, and every one of them is written into the generated config as well as forwarded.
95
+
96
+ ### Redaction that corrupted what it protected
97
+
98
+ Three shapes were found in this repository's own snapshot, where the tool rewrote its own
99
+ documentation and tests:
100
+
101
+ - **Prose.** `credential-assignment-unquoted` masked any word after `secret: ` or `token: `, so
102
+ `indistinguishable from a secret: a` became `…from a secret: [REDACTED…]`. An unquoted value in a
103
+ line-oriented format never continues after a space (`PASSWORD: hunter2` ends the line; `secret: a
104
+ value` is a sentence), and a one-character bare value is not a credential. Quoted values keep
105
+ their one-character floor — quotes are evidence somebody meant to write a value.
106
+ - **Template literals.** `` `… an empty token: "${appliesTo}"` `` had the interpolation masked,
107
+ because `startsInsideStringLiteral` only looks at the character before the identifier. A value
108
+ that is entirely `${…}` is an expression, so it is not masked.
109
+ - **Doubly-escaped quotes.** `JSON.parse('{"password": "p@\\"ssw0rdINVENTED"}')` — how every
110
+ fixture that embeds JSON is written — emitted `"[REDACTED…]"ssw0rdINVENTED"`: a fragment of the
111
+ credential in clear and a line that no longer parses. A quote after `\\` that is followed by a
112
+ value character is an escaped quote in a deeper literal, and the value now runs to the real close.
113
+
114
+ ### An async `afterGenerate` hook's files were not recorded
115
+
116
+ The hook is documented as the way to write your own artifact into the snapshot directory, and that
117
+ directory is only replaceable while `.codebase-snapshot` accounts for every file in it. Only the
118
+ *rejection* path re-shipped the marker, so an async hook that succeeded left its file unrecorded
119
+ and the next `generate` refused the directory as foreign. The marker is now re-shipped when the
120
+ hook settles either way, and only while it still records this run — the lock is released when
121
+ `generateSnapshot` returns, so a slow hook can outlive the snapshot it belongs to.
122
+ `generateSnapshotAsync` now awaits the hook, so a caller of the async API gets control back when
123
+ the snapshot is actually finished.
124
+
125
+ ### Smaller fixes
126
+
127
+ - **`getGitInfo` used Node's 1 MB `maxBuffer`** where every sibling call passed 32 or 50 MB. On a
128
+ repository with enough dirty entries, `git status` overflowed, threw, and the `catch` returned
129
+ `isDirty: false` — reporting the dirtiest possible tree as clean. The limit is raised and shared,
130
+ and an overflow now reports `isDirty: true`, because a buffer that large only fills when there is
131
+ a great deal to report. All three git invocations use `execFileSync` instead of a shell string.
132
+ - **`.gitignore` character classes** were escaped as literals, so `*.[oa]`, `[Tt]humbs.db` and
133
+ `log[0-9].txt` matched nothing in `--discovery walk` while working in `git` mode.
134
+ - **A layer or kind rule** was glob-translated only when it contained `*` or `?`, so
135
+ `src/{api,web}/index.ts` fell through to a substring search and classified nothing; and a plain
136
+ pattern like `test` used `String.includes`, so it matched `src/contest/a.ts`. Both now go through
137
+ `matchesExclude`, which is the single implementation of "does this pattern select this path".
138
+ - **`export { handler as GET }`** and `export { GET } from "./handlers"` recorded the export but no
139
+ route method, so those App Router routes had no `methods` in `manifest.indexes.routes`.
140
+ `export default () => <div />` recorded no component for the same reason.
141
+ - **The last-resort clip** reserved room for its own truncation marker but not for the token count
142
+ rewritten afterwards, so a part clipped to exactly its budget came back over it. `rehashPart` also
143
+ estimated tokens from text that still carried the *old* number, unlike every other part.
144
+ - **Seven MCP tools ignored `outDir`**, so a snapshot generated into a custom directory could be
145
+ written by `snapshot_generate` and inspected by nothing.
146
+ - **`snapshot_read`** replaced the line range with the preview note instead of adding to it, so a
147
+ window into a preview came back with no `Lines X–Y of Z` at all; `signatures` was treated as a
148
+ full file.
149
+ - **Every published file ended with a `sourceMappingURL`** pointing at a `.map` that
150
+ `package.json#files` deliberately does not ship. The build no longer emits them.
151
+ - **`--watch` crashed on Node 18 on Linux**, where `fs.watch(cwd, { recursive: true })` does not
152
+ exist before Node 20.13. The package declares `engines: >=18`, and the run wrote a snapshot,
153
+ printed "Watching for changes", and *then* threw `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`. It is now
154
+ refused up front, with exit `2` and a message naming the version that works — and before anything
155
+ is written. A watcher that cannot start for any other reason (`ENOSPC`, the Linux inotify limit)
156
+ reports it as a message instead of a stack trace, and the snapshot it already wrote stays valid.
157
+ Found by the release's own Node 18 job: 0.9.10 and 0.9.11 never ran one.
158
+
159
+ ---
160
+
161
+ ## 0.9.11
162
+
163
+ A second audit, run against 0.9.10's own output, found that several defects the previous release
164
+ believed it had fixed were still reachable, that the changed-files scope was applied one layer too
165
+ late, and that this repository's documentation had been quietly corrupted. Every item below is
166
+ fixed with a test that fails without it.
167
+
168
+ ### The redactor was still editing the program
169
+
170
+ `credential-assignment-unquoted` exists for `.env`, YAML and compose files, where everything after
171
+ `NAME=` is a literal. In a source file the same shape is an **expression**. The bug is visible in
172
+ the 0.9.10 snapshot itself, in `parts/part-005.md`:
173
+
174
+ ```
175
+ const SINGLE_TOKEN = [REDACTED: potential secret-like value omitted from snapshot][^\sA-Za-z0-9_]/g;
176
+ ```
177
+
178
+ The regex literal's opening `/` was a legal match for `UNQUOTED_LITERAL`, so the pattern was
179
+ deleted and the line no longer parses — data loss in the artifact whose whole purpose is to
180
+ describe the code. Three changes close the class rather than the instance:
181
+
182
+ - an unquoted value must **begin** with a word character, so `/`, `+`, `-` and `%` can only
183
+ continue one;
184
+ - a value followed by `[`, `{` or `(` is refused, because `const accessToken = tokens[0]` is a
185
+ reference;
186
+ - a value that sits in a **declaration** is never a literal. `const`, `let`, `var`, `readonly`,
187
+ `private`, `protected`, `static`, `declare`, `export` and `using` are recognised, walking back to
188
+ the start of the identifier so the tail match (`TOKEN` inside `SINGLE_TOKEN`) is covered too.
189
+
190
+ The guard is bounded to a 40-character window: an unbounded version made a 236 KB line with 4,000
191
+ assignments cost 2.7 s, which the existing quadratic-scan test caught.
192
+
193
+ ### A changed-files scope was applied after the scan instead of before it
194
+
195
+ `--changed-since` and `--staged` read, decoded, secret-scanned and TypeScript-parsed every file in
196
+ the repository and then threw all but a handful away. Because every aggregate was bumped inside
197
+ that scan, the snapshot inherited the repository's numbers:
198
+
199
+ - **`validate` failed on every scoped snapshot.** Unchanged files are recorded in `skipped[]` with
200
+ reason `unchanged`, and `validate` then asked `wouldNowBeIncluded` about them — a question that
201
+ only answers "is this text?" — so every unchanged source file was reported as `new file:` and
202
+ the exit code was 1.
203
+ - **`manifest.layers`, `manifest.kinds`, `counts.largeFileCount`, `totals.redactedLines`** and the
204
+ encoding/preview/parse warnings all described files the snapshot did not contain. A two-file
205
+ snapshot reported the layer distribution and the secret count of the whole repository.
206
+ - **`explain` ignored the scope entirely** and answered `included: true` for exactly the files
207
+ `generate` was skipping.
208
+
209
+ The scope is now resolved once, before the scan, and applied to the candidate list whenever
210
+ `--with-dependents` is not in play. The dependents case still has to scan first — it needs every
211
+ file's imports — so it narrows afterwards and recomputes the counts, the findings and the warnings
212
+ from what survived.
213
+
214
+ ### `--with-dependents` walked the import graph transitively
215
+
216
+ The closure tested `keep.has(resolved)` while adding to `keep`, so `a.ts` changed pulled in
217
+ `b.ts` (which imports it) and then `c.ts` (which imports `b.ts`). Whether a two-hop file was
218
+ included depended on whether its name sorted after `b.ts`: the same repository produced different
219
+ snapshots depending on file names. The test is now made against the changed set, which makes the
220
+ closure exactly one hop.
221
+
222
+ ### Root-anchored `.gitignore` rules never matched in `walk` mode
223
+
224
+ `readGitignoreRules` keeps the leading `/`, and the matcher it was handed compares against a path
225
+ with no leading slash — so `/build` could not equal `build`, could not prefix `build/x`, and could
226
+ not appear between two slashes. `/secret-notes.txt` silently kept its file in the snapshot.
227
+
228
+ The same delegation made the opposite mistake one segment down: gitignore(5) anchors a pattern
229
+ containing a non-trailing `/` to the directory holding the `.gitignore`, so `doc/frotz` ignores the
230
+ top-level one only, while `matchesExclude` matched `a/doc/frotz` as well. Both are fixed by using
231
+ the gitignore matcher (which already implemented `anchored || body.includes("/")`) for every rule
232
+ rather than only for rules containing a backslash. `matchesExclude` is left to configuration
233
+ excludes, where "match at any depth" is the documented behaviour.
234
+
235
+ ### Spreading a resolved config with a new `maxPartBytes` changed nothing
236
+
237
+ `{ ...resolved, maxPartBytes: 64 * 1024 }` still satisfies `isResolvedConfig`, and
238
+ `normalizeResolvedConfig` kept the **old** `budgets` object. The partitioner reads
239
+ `budgets.default`, so the run produced 300 KB parts and the manifest recorded the 64 KB that was
240
+ ignored. `normalizeResolvedConfig` now normalises `budgets` against `maxPartBytes`, capping
241
+ per-layer budgets rather than replacing them.
242
+
243
+ A related inconsistency is fixed too: a config that sets only `budgets: { default: "100k" }`
244
+ recorded `maxPartBytes: 300k`, so one snapshot described two different budgets.
245
+
246
+ ### Smaller fixes
247
+
248
+ - **The MCP worker URL was built with string interpolation.** `file://${path}` turns a Windows path
249
+ into `file://C:/…` whose *host* is the drive letter, reads a `#` as a fragment and a `%` as an
250
+ escape. It uses `pathToFileURL` now, through an exported `workerEntry()`.
251
+ - **`--sarif` and the monolith disagreed.** The log was rendered in the write phase, so
252
+ `full-codebase.json` embedded a manifest with no `artifacts.sarif` while `manifest.json` had one.
253
+ The log is rendered before the monolith is serialised.
254
+ - **`validate` never checked `reports/audit.sarif`** against its recorded digest. It does now: a
255
+ security report uploaded to a third party is exactly the artifact that must be verified.
256
+ - **`--sarif` was missing from `configDigest`**, so two runs that write different directories
257
+ compared equal.
258
+ - **`--skip-artifacts … --sarif` claimed nothing was written** while writing `reports/audit.sarif`.
259
+ - **SARIF rule ids were positional.** They were handed out by position in the sorted set of rule
260
+ names a run happened to use, so adding one custom rule that sorts before the built-ins
261
+ (`acme-key` does) shifted every built-in id by one and re-pointed every open alert. Built-in
262
+ rules now take a fixed id from their declaration order and custom rules get an id hashed from
263
+ their own name. This renumbers the built-ins once, relative to 0.9.10, and never again.
264
+ - **`generate --watch` generated twice on startup** — `runCommand` generated, then the loop
265
+ generated again before any file changed.
266
+ - **`--watch` froze `outDir`/`tmpDir` at startup.** After the config moved them, the watcher saw
267
+ its own output as user content and regenerated without end.
268
+ - **The pre-commit hook hardcoded `snapshot/`**, so a project with `outDir: ".context"` got a hook
269
+ that printed "no snapshot found, skipping" and exited 0 on every commit — a gate that never ran.
270
+ The directory is baked in at install time, and the guard tests `manifest.json` rather than the
271
+ directory (a directory left by a failed run exists but holds no snapshot).
272
+ - **`--max-part-tokens 200` was silently raised** to the 1024-token floor. It is a usage error
273
+ now, and the message talks about tokens rather than bytes.
274
+ - **The MCP line cap was bypassed** by an oversized line that arrived complete with its terminator,
275
+ because the guard required the buffer to hold no `\n` at all. The cap now applies to every
276
+ complete line.
277
+ - **`snapshot_locate` searched only half the summary.** `variables` (where a `const`, a Python
278
+ constant, a Go `var`, a Rust `mod`, a C `#define` and a Terraform `variable` all land),
279
+ `directives`, `frontmatterKeys` and JSON `keys` are searched too.
280
+ - **`snapshot_read` printed `Lines 1–0 of 1` for an empty file** and returned the *last* line when
281
+ `fromLine` was past the end. Both are stated instead.
282
+ - **`.TSX` was parsed as `ScriptKind.TS`** — `isSkeletonizable` lowered the name and
283
+ `skeletonizeTypeScript` did not.
284
+ - **Markdown summaries:** an unclosed `---` at line 0 swallowed every heading, a shorter fence
285
+ inside a longer one closed it early, and a closing fence with an info string was accepted. All
286
+ three follow CommonMark now.
287
+ - **Destructured exports were invisible.** `export const { a, b } = obj` and
288
+ `export const [first] = pair` declare real exports and were dropped by an
289
+ `isIdentifier` check.
290
+ - **`rehashPart` left `estimatedTokens` at its pre-clip value** after `enforceBudget` cut a part,
291
+ and the part's own header kept stating the old number. Both are recomputed.
292
+ - **`setup --preset <unknown>` exited 1** where `init` exits 2, because the parse-time check was
293
+ written for `init` only.
294
+ - **`describe` omitted all seven 0.9.10 flags and six config keys.** `describe` is the contract an
295
+ agent reads instead of the docs, so this is the failure that matters most: the options simply did
296
+ not exist as far as a machine consumer was concerned. A test now compares `CLI_OPTIONS` and
297
+ `CONFIG_KEYS` against the contract in both directions.
298
+ - **`--changed-since` was passed to git as a positional revision.** A ref beginning with `-` is now
299
+ refused before git sees it, and `--` separates revisions from paths.
300
+ - **`--skeleton --include-monolith` labelled a body-stripped file `inclusionMode: "full"`.** The
301
+ monolith now keeps the full redacted body, so the label is true.
302
+
303
+ ### Documentation and repository hygiene
304
+
305
+ - **Three sentences had letters eaten by interpreted escapes**: `\v` in "Validate", `\r` in
306
+ "reports/", `\a` in "audit.md". A test now fails on any control byte in any tracked text file.
307
+ - **Four documents claimed the MCP server serves seven tools.** It serves eight. The count is now
308
+ compared against the live tool list.
309
+ - **`publish:create` could not run.** `npm publish packages/create` is parsed by npm as the GitHub
310
+ shorthand `ssh://git@github.com/packages/create.git` and fails with exit 128; it names
311
+ `./packages/create`.
312
+ - **`check-site-links` accepted a sibling directory.** `resolved.startsWith(SITE)` is true for
313
+ `../site-other`; the check requires the path separator.
314
+
315
+ ---
316
+
317
+ ## 0.9.10
318
+
319
+ An audit of this codebase, run against its own snapshot, found twenty defects, six performance
320
+ problems and seven missing capabilities. This release is all of them: every defect, every
321
+ performance problem and every capability named in that audit is in the tree, with a test that
322
+ fails without it.
323
+
324
+ ### Redaction was corrupting the code it exists to preserve
325
+
326
+ The worst defects were in the secret scanner, and the evidence was in this repository's own
327
+ snapshot. Four false-positive classes rewrote valid statements:
328
+
329
+ - `[:=]` matched the first `=` of `===`, and the unquoted-value rules then read the remaining `==`
330
+ as a credential. `clause.token === ts.SyntaxKind…` shipped as `clause.token =[REDACTED…]`, and so
331
+ did `if (token === "--")`.
332
+ - `env-secret-assignment` carried `/i`, so an indented reassignment matched: `key =
333
+ fs.realpathSync(stagingRoot)` was emitted with the call masked.
334
+ - Credential names are matched as a *tail* — that is what catches `dbPassword` — so a name inside a
335
+ string literal matched, and the string's closing quote was taken as the value's opening quote.
336
+ `["awsSecret:", "apiKey:"]` came out with two list elements welded together.
337
+ - `progressToken: "progressToken"` was masked as a credential. A camelCase value that spells its
338
+ own key is a symbol-table entry, not a secret.
339
+
340
+ Reserved words are no longer credential literals either: `token = await refreshToken(client)` was
341
+ emitted as `token = [REDACTED] refreshToken(client)`.
342
+
343
+ ### Scanning no longer depends on state outside the tree
344
+
345
+ - A `.gitignore` rule naming a nested directory (`public/build/`) never matched in `walk` mode: the
346
+ candidate was reduced to its first path segment, which cannot equal a two-segment rule. The same
347
+ bug dropped a regular *file* named `build` for a `build/` rule.
348
+ - How files are enumerated was held in a module-level variable written by `resolveConfig`, so two
349
+ scans in one process could disagree, and the second inherited the first one's mode.
350
+ - `validate` did not exclude the tool's own output directory, so it counted `snapshot/` as files
351
+ missing from the snapshot and reported a correct snapshot as drifting.
352
+ - `findSecretInPath` compiled thirty-plus `RegExp` objects per call, and it runs on every path and
353
+ every string in every summary.
354
+ - `getGitInfo` spawns one `git status` instead of three `git` processes.
355
+
356
+ ### The config digest now covers what decides a snapshot
357
+
358
+ Layer and kind *names* were hashed, not their matchers, so moving a layer from `src/api/**` to
359
+ `src/v2/**` produced an identical digest and `validate` said "valid and up to date" for a snapshot
360
+ whose files had all been reclassified. A rule's `flags` and `valueGroup` were missing too, and so
361
+ was `discovery`. `package.json`'s `"snapshot"` object now gets the unknown-key guard every other
362
+ config file already had, and `secretRules` is accepted in a config file as it already was in code.
363
+
364
+ ### `setup` stops dropping the flags it was given
365
+
366
+ `setup` builds the inner `generate` command line by hand, and five settings were never copied into
367
+ it, so the run reported one configuration and generated with another. It also refused
368
+ `--discovery`, `--large-file-bytes`, `--max-monolith-bytes` and `--include-monolith` outright.
369
+ `--only-artifacts monolith` enabled *nothing*: it turns the six default artifacts off and relied on
370
+ the monolith switch to turn the seventh on, and neither `init` nor `setup` passed that switch along.
371
+
372
+ ### Part budgets say what they enforce
373
+
374
+ - Every part records the budget it was built for. With per-layer budgets, verification compared
375
+ each part with the *largest* budget in the run, so a part twice over its own layer budget passed.
376
+ - `renumberParts` rewrites headers, so the byte bound is re-applied after renumbering.
377
+ - A body cut twice reported only the second cut: a 1,000-line file reduced to a preview and then to
378
+ ten lines said "11 lines omitted", because the count came from the preview instead of the file.
379
+ - Every part states its size in **tokens**, and `--max-part-tokens 32k` sets a ceiling a context
380
+ window can be measured against. The estimate's method is documented, not hidden.
381
+
382
+ ### Crash-proof locks, byte-accurate reads
383
+
384
+ - A run killed between creating its lock file and writing its pid leaves a zero-byte lock that
385
+ named nobody, so `lockOwnerIsGone` answered "not gone" forever and every later run refused to
386
+ start until the file was deleted by hand. Age is now the fallback evidence.
387
+ - `snapshot_read` truncated to its byte limit by slicing UTF-16 code units, returning up to three
388
+ times the limit for multibyte text and able to split a surrogate pair.
389
+ - `snapshot_locate` could not find a symbol a file does not export — most of the code in most files
390
+ — and could not find a Markdown heading at all. Layer and kind matched by exact equality while
391
+ every other category used a substring.
392
+ - `findSection` compared a heading with the manifest's raw path, which differ once the heading has
393
+ been capped or escaped; such a file became unreadable whenever another file shared its content.
394
+ - A watch loop had no `error` listener on `fs.watch`, so an `EPERM` or an inotify limit killed the
395
+ process instead of being reported.
396
+ - The update check skipped itself when any argument contained "json", so `explain package.json`
397
+ never asked.
398
+
399
+ ### Fewer syscalls
400
+
401
+ `generate` read every included file twice — a chunked NUL scan, then a full read and a second scan
402
+ of the same bytes. Below the 8 MB scan cap the decode is the complete check on bytes that had to be
403
+ read anyway, so the pre-scan is kept only above it. `explain` and `validate` had the same double
404
+ read.
405
+
406
+ ### New structure for six more languages
407
+
408
+ Swift, Dart, SQL, GraphQL, Protobuf and Terraform now have profiles. `src/scanner/language.ts`
409
+ already knew four of those extensions by name and no profile claimed them, so a repository in one of
410
+ those languages got a list of files and no idea what was inside them.
411
+
412
+ ### The site keeps its own links
413
+
414
+ `README.md` writes absolute GitHub URLs so the same text works on npmjs.com; the site builder only
415
+ rewrote *relative* links, so a reader on the docs site who clicked a guide link on the homepage was
416
+ handed back to GitHub. In-repo links to published pages are rewritten now; other repositories and
417
+ files the site does not publish are left alone.
418
+
419
+ ### Also
420
+
421
+ - `configDigest`, `classifyLayer`, `classifyKind`, `displayPath`, `lockPathFor`, `estimateTokens`,
422
+ `findSecretInPath` and `getGitInfo` are exported.
423
+ - `manifest.config` records `maxMonolithBytes` and `maxPartTokens`.
424
+ - `scripts/sync-docs.mjs` propagates `package.json#version` to `src/version.ts` and
425
+ `packages/create/package.json`, so a release cannot drift by forgetting a file. `--check` covers
426
+ them.
427
+ - `scripts/pack-check.mjs` requires the MCP build outputs, and the three wizard test files share one
428
+ fake-TTY harness.
429
+
430
+ ### `snapshot_dependents`: the import graph, in both directions
431
+
432
+ `manifest.indexes.imports` already held enough to answer "what uses this file" —
433
+ `byFile` records every specifier, `centralFiles` ranks the most-imported files — but nothing
434
+ an agent could ask. So the one question that decides what to read next, *what breaks if I
435
+ change this?*, was answered by reading parts until the picture appeared.
436
+
437
+ The eighth MCP tool takes a path and a direction and returns both edge sets with the part each
438
+ neighbour lives in, plus the specifiers that resolve to nothing inside the snapshot
439
+ (`node:fs`, a package name) rather than silently dropping them. Resolution runs through the
440
+ generator's own `buildImportResolver`, so the tool and `centralFiles` cannot describe different
441
+ graphs — a test asserts the counts agree.
442
+
443
+ ### `--install-hook` gates commits on `validate`
444
+
445
+ `docs/stale-context-is-your-bug.md` argues a snapshot is only trustworthy when something gates
446
+ on it, and `validate` is that gate — but wiring it into `.git/hooks` was thirty seconds of
447
+ manual work, which is the step a project skips. `init --install-hook` and `setup --install-hook`
448
+ write `.git/hooks/pre-commit`, which refuses a commit whose snapshot is stale and never installs
449
+ anything while doing it.
450
+
451
+ A `pre-commit` this tool did not write is left byte-for-byte alone, and the output names the line
452
+ to add instead: there is deliberately no flag to force the overwrite. A `.git` that is a *file*
453
+ (a worktree or submodule) is followed to the real git directory rather than failing with
454
+ `ENOTDIR`.
455
+
456
+ ### `--changed-since` and `--staged`: review packs the diff
457
+
458
+ Reviewing a pull request with a model is the case this exists for: the diff is small, the
459
+ repository is not, and packing all of it costs a context window to answer a question about four
460
+ files.
461
+
462
+ `--changed-since <ref>` is the working tree against that ref — committed, staged and unstaged
463
+ changes all count, because all three are what a reviewer is looking at — **plus untracked
464
+ files**, which `git diff` never lists and which are exactly what a new feature starts with.
465
+ `--staged` is the index against `HEAD`, and `--with-dependents` adds the one-hop importers,
466
+ which is the context needed to change a file safely rather than merely the file that moved.
467
+
468
+ Files outside the scope are recorded in `skipped[]` with a new reason, `unchanged` —
469
+ deliberately not `excluded`, which means the user's own glob rejected the path. Every failure is
470
+ loud: an unknown ref, a directory that is not a git repository, and `--with-dependents` without
471
+ a scope are all errors naming the way out, because a ref typo that produced an empty snapshot
472
+ would read as "this change touched nothing".
473
+
474
+ ### `--skeleton`: signatures instead of bodies
475
+
476
+ `docs/vs-repomix.md` concedes that a compression mode beats byte budgeting for a repository too
477
+ large to fit at all. This is that mode, built on the compiler that is already the one runtime
478
+ dependency: each function, method, constructor, accessor and arrow body becomes `{ /* ... */ }`
479
+ in place.
480
+
481
+ The rewrite is span-based rather than a `ts.createPrinter()` re-print, and that is the whole
482
+ feature — a printer normalises quotes, blank lines and comment positions, so a skeleton would
483
+ differ from its source everywhere instead of only where bodies were removed. Affected entries
484
+ carry `inclusionMode: "signatures"`, and the structural summary still describes the whole file.
485
+
486
+ ### `--sarif`: the findings a CI gate can read
487
+
488
+ `reports/audit.md` tells a human what was masked, and nothing could *gate* on it: a CI job that
489
+ wanted to fail a build over a leaked key had to scrape markdown. `--sarif` writes
490
+ `reports/audit.sarif` in SARIF 2.1.0, which is what GitHub Code Scanning reads.
491
+
492
+ The artefact is deliberately austere: path, line, level and rule — and **never the value**,
493
+ because it is meant to be uploaded to a third party and a second copy of a secret in someone
494
+ else's retention policy is a new problem rather than a report on the old one. Rule ids are
495
+ stable across runs so an alert is not orphaned, and each result carries a
496
+ `partialFingerprint` so Code Scanning keeps an alert open instead of closing and reopening it
497
+ on every run.
498
+
499
+ ### `snapshot_generate` runs off the JSON-RPC thread
500
+
501
+ The 0.9.4 measurement stands: a synchronous generate blocks the stdio server for about 7.5
502
+ seconds on a 3,000-file repository. In that window the process cannot answer `ping` or
503
+ acknowledge `notifications/cancelled`, and a client with a socket timeout reports a working tool
504
+ call as a dead server.
505
+
506
+ The work moves to a worker thread. A resolved config cannot cross a thread boundary —
507
+ `layers[].match` may be a function and `hooks.afterGenerate` always is — so the worker loads
508
+ the config file and applies the same CLI options the calling thread did, rather than receiving
509
+ values that would all look like explicit overrides. A worker that fails to start, exits without
510
+ posting, or throws is reported as a tool failure naming the reason; there is no quiet retry on the
511
+ calling thread, because that would hide exactly the condition this exists to avoid.
512
+
513
+ One consequence is documented rather than discovered: `hooks.afterGenerate` does not run for a
514
+ generation started from MCP, only for a CLI `generate`.
515
+
516
+ ---
517
+
518
+ ## 0.9.9
519
+
520
+ ### `snapshot -version` now answers the question it was asked
521
+
522
+ Reported from a real terminal, by someone trying to find out which version they had:
523
+
524
+ ```
525
+ > snapshot -version
526
+ ❌ Unknown option '-e'. To specify a positional argument starting with a '-', place it at the
527
+ end of the command after '--', as in '-- "-e"'
528
+ ```
529
+
530
+ That message came from `node:util.parseArgs`, which reads `-version` as the short-flag cluster
531
+ `-v -e -r -s -i -o -n` and objects to the third character. It names something the caller never
532
+ typed, and it buries the actual mistake — one dash where two belong — which is exactly the
533
+ information a usage error exists to give.
534
+
535
+ Every token of that shape is now caught before the parser sees it and answered as a spelling
536
+ problem:
537
+
538
+ ```
539
+ ❌ Unknown option '-version'. Options start with two dashes — did you mean '--version'?
540
+ ```
541
+
542
+ `-cwd`, `-help`, `-format` and `-dry-run` get the same treatment with their own long form
543
+ suggested. A token that is not an option at all (`-frobnicate`) is told to go after `--` instead of
544
+ being pointed at a `--frobnicate` that does not exist, and a cluster of short flags that *are* real
545
+ (`-vh`) still works.
546
+
547
+ ### The update notice suggests the command that fits this copy
548
+
549
+ It said `npm install -g codebase-snapshot` to everybody, which is wrong for two of the three ways
550
+ people run this. `setup` deliberately adds the package as a **devDependency**, where `-g` installs a
551
+ second global copy that shadows the pinned one; an `npx` invocation is a cache entry that the next
552
+ `@latest` run replaces by itself.
553
+
554
+ The suggested command is now derived from the path Node is running: `-g` for a global install,
555
+ `npm install codebase-snapshot@latest` for a dependency of the project being worked in (npm rewrites
556
+ the entry where it already is, so a devDependency stays one), and `npx -y codebase-snapshot@latest`
557
+ for npx. The classification is `_npx` in the path for the cache, and `node_modules` under the
558
+ working directory for a local dependency — evidence rather than a guess.
559
+
24
560
  ## 0.9.8
25
561
 
26
562
  ### A wrapped menu no longer changes height under the cursor
package/README.md CHANGED
@@ -200,13 +200,13 @@ Everything an agent needs is available as data, and the tool can install its own
200
200
  ```bash
201
201
  npx codebase-snapshot describe --format json # contract: workflow, options, exit codes
202
202
  npx codebase-snapshot skill --install # into .claude/skills + .cursor/rules
203
- npx -y codebase-snapshot@0.9.8 mcp # or serve it over MCP
203
+ npx -y codebase-snapshot@0.9.12 mcp # or serve it over MCP
204
204
  ```
205
205
 
206
206
  An agent told only "use `npx codebase-snapshot`" can read the contract, follow its ordered
207
207
  workflow, and produce a validated snapshot unaided — that path is covered by a test.
208
208
 
209
- The MCP server exposes seven tools — status, plan, generate, validate, locate, read, explain — plus
209
+ The MCP server exposes eight tools — status, plan, generate, validate, locate, dependents, read, explain — plus
210
210
  `snapshot://` resources and two prompts. Its reads come from the snapshot rather than the working
211
211
  tree, so an excluded file stays excluded and a redacted value stays redacted, and
212
212
  `snapshot_validate` answers the question an agent otherwise cannot ask: is the context I was given
@@ -260,7 +260,7 @@ others.
260
260
  | [Configuration](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/configuration.md) | Every setting, all 25 presets, budgets, hooks, secrets |
261
261
  | [Snapshot output](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/output.md) | Directory layout and the full manifest schema |
262
262
  | [AI agents](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/agent.md) | The contract, the skill, what an agent should avoid |
263
- | [MCP server](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/mcp.md) | Seven tools, resources, both protocol eras |
263
+ | [MCP server](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/mcp.md) | Eight tools, resources, both protocol eras |
264
264
  | [Troubleshooting](https://github.com/ankurhalder/codebase-snapshot/blob/main/docs/troubleshooting.md) | Every message and what to do about it |
265
265
  | [Changelog](https://github.com/ankurhalder/codebase-snapshot/blob/main/CHANGELOG.md) | Release history and breaking changes |
266
266