codebase-snapshot 0.9.9 → 0.9.14

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.
@@ -21,7 +21,7 @@ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
21
21
  * `full` – the complete file body was emitted.
22
22
  * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
23
  */
24
- type InclusionMode = "full" | "preview";
24
+ type InclusionMode = "full" | "preview" | "signatures";
25
25
  interface SourceSummary {
26
26
  directives: string[];
27
27
  imports: string[];
@@ -88,7 +88,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
88
88
  * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
89
  * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
90
  */
91
- | "nested-repository";
91
+ | "nested-repository"
92
+ /**
93
+ * Not in the set that `--changed-since` or `--staged` asked for.
94
+ *
95
+ * Deliberately distinct from `excluded`, which means the user's own glob rejected the path.
96
+ * Here the file is perfectly includable and simply did not change; reporting it as
97
+ * `excluded` would send a reader looking for an exclude pattern they never wrote.
98
+ */
99
+ | "unchanged";
92
100
  interface SkipRecord {
93
101
  /** Path as it may be shown in an artifact: secret spans are masked. */
94
102
  path: string;
@@ -144,6 +152,23 @@ interface PartMetadata {
144
152
  filename: string;
145
153
  /** Exact UTF-8 byte length of the emitted part file. */
146
154
  byteSize: number;
155
+ /**
156
+ * Estimated tokens in this part.
157
+ *
158
+ * `byteSize` answers "will this fit the byte budget"; this answers the question a caller
159
+ * actually has, which is "will this fit my context window". An estimate, and labelled as
160
+ * one: `estimateTokens` documents exactly what it counts.
161
+ */
162
+ estimatedTokens: number;
163
+ /**
164
+ * The byte budget this part was partitioned against.
165
+ *
166
+ * With per-layer budgets a snapshot has several budgets at once, so "within budget"
167
+ * cannot be answered without knowing which one applied. Verification compared every
168
+ * part against the *largest* budget in the run, which passed a part that was over its
169
+ * own.
170
+ */
171
+ budgetBytes: number;
147
172
  sha256: string;
148
173
  lines: number;
149
174
  fileCount: number;
@@ -198,6 +223,14 @@ interface ManifestTotals {
198
223
  skippedFiles: number;
199
224
  totalSourceBytes: number;
200
225
  totalContentBytes: number;
226
+ /**
227
+ * Estimated tokens in the emitted parts.
228
+ *
229
+ * Optional because a manifest written before this was recorded has no such key. The number
230
+ * is an estimate — see `estimateTokens` for the method — and it exists because the snapshot
231
+ * is pasted into a context window, which is measured in tokens rather than bytes.
232
+ */
233
+ estimatedTokens?: number;
201
234
  partCount: number;
202
235
  warningCount: number;
203
236
  /**
@@ -256,13 +289,34 @@ interface ManifestConfigSnapshot {
256
289
  /** Digest of the effective settings; see `configDigest`. */
257
290
  digest?: string;
258
291
  maxPartBytes: number;
292
+ /**
293
+ * The token ceiling the run was given, or `undefined` when only bytes were named.
294
+ *
295
+ * Recorded so the manifest states the budget the user chose rather than only the byte
296
+ * value it was converted to.
297
+ */
298
+ maxPartTokens?: number;
259
299
  largeFileBytes: number;
260
300
  previewHeadLines: number;
261
301
  previewTailLines: number;
262
302
  includeMonolith: boolean;
303
+ /**
304
+ * The ceiling above which the monolith is not written; `null` for no ceiling.
305
+ *
306
+ * Optional because a manifest written before this was recorded has no such key.
307
+ */
308
+ maxMonolithBytes?: number | null;
263
309
  includeSensitiveFiles: boolean;
264
310
  /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
311
  discovery?: DiscoveryMode;
312
+ /** The changed-files scope the snapshot was taken over, when one was used. */
313
+ changedSince?: string;
314
+ staged?: boolean;
315
+ withDependents?: boolean;
316
+ /** True when bodies were replaced by signatures. */
317
+ skeleton?: boolean;
318
+ /** True when `reports/audit.sarif` was written. */
319
+ sarif?: boolean;
266
320
  artifacts: ResolvedArtifacts;
267
321
  include: string[];
268
322
  preset: string;
@@ -315,6 +369,14 @@ interface Manifest {
315
369
  artifacts: {
316
370
  fullJson: ManifestArtifact | null;
317
371
  fullMarkdown: ManifestArtifact | null;
372
+ /**
373
+ * `reports/audit.sarif`, when `--sarif` asked for it.
374
+ *
375
+ * Optional because most runs do not write one, and a manifest written before this
376
+ * existed has no such key at all — the two are indistinguishable to a reader, which is
377
+ * why the presence of the file is also recorded in `config`.
378
+ */
379
+ sarif?: ManifestArtifact;
318
380
  };
319
381
  layers: Record<string, number>;
320
382
  kinds: Record<string, number>;
@@ -382,6 +444,15 @@ interface SnapshotConfig {
382
444
  tmpDir?: string;
383
445
  /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
446
  maxPartBytes?: ByteSize;
447
+ /**
448
+ * A per-part ceiling in *tokens* rather than bytes.
449
+ *
450
+ * The snapshot is pasted into a context window, and a context window is measured in tokens;
451
+ * bytes are what the partitioner writes. Naming a token budget sets `maxPartBytes` to the
452
+ * byte count that corresponds to it, so the byte bound — the thing the partitioner actually
453
+ * enforces — never stops being true. The smaller of the two ceilings wins.
454
+ */
455
+ maxPartTokens?: ByteSize;
385
456
  largeFileBytes?: ByteSize;
386
457
  previewHeadLines?: number;
387
458
  previewTailLines?: number;
@@ -393,6 +464,31 @@ interface SnapshotConfig {
393
464
  include?: string[];
394
465
  /** Force a preset by name instead of auto-detecting one. */
395
466
  preset?: string;
467
+ /**
468
+ * Only include files changed since this git ref.
469
+ *
470
+ * The pull-request case: the diff is small and the repository is not. Unchanged files are
471
+ * recorded in `skipped[]` with reason `unchanged`, so the manifest accounts for them.
472
+ */
473
+ changedSince?: string;
474
+ /** Only include files staged for the next commit. */
475
+ staged?: boolean;
476
+ /**
477
+ * Emit signatures instead of bodies for TypeScript and TSX files.
478
+ *
479
+ * Matches `inclusionMode: "signatures"` on the entries it applies to, so a reader can tell a
480
+ * compressed file from a whole one without diffing it against the source.
481
+ */
482
+ skeleton?: boolean;
483
+ /** Also write `reports/audit.sarif` so a CI security gate can read the findings. */
484
+ sarif?: boolean;
485
+ /**
486
+ * With `changedSince` or `staged`, also include the files that import a changed one.
487
+ *
488
+ * A one-hop closure: the file you changed and the files that reference it are the context
489
+ * needed to change it safely.
490
+ */
491
+ withDependents?: boolean;
396
492
  /** Switch individual snapshot artifacts off. Everything is on by default. */
397
493
  artifacts?: ArtifactSelection;
398
494
  /**
@@ -418,6 +514,8 @@ interface ResolvedSnapshotConfig {
418
514
  outDir: string;
419
515
  tmpDir: string;
420
516
  maxPartBytes: number;
517
+ /** The token ceiling the user asked for, before it was turned into `maxPartBytes`. */
518
+ maxPartTokens?: number;
421
519
  largeFileBytes: number;
422
520
  previewHeadLines: number;
423
521
  previewTailLines: number;
@@ -430,6 +528,21 @@ interface ResolvedSnapshotConfig {
430
528
  * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
529
  */
432
530
  discovery?: DiscoveryMode;
531
+ /** Only files changed since this git ref; `undefined` scans everything. */
532
+ changedSince?: string;
533
+ /** Only files staged for the next commit. */
534
+ staged?: boolean;
535
+ /** Emit signatures instead of bodies for TypeScript and TSX files. */
536
+ skeleton?: boolean;
537
+ /**
538
+ * Also write `reports/audit.sarif`, the findings in SARIF 2.1.0.
539
+ *
540
+ * For a CI security gate: GitHub Code Scanning reads SARIF, and a markdown report it cannot
541
+ * ingest is a finding nobody's build can act on.
542
+ */
543
+ sarif?: boolean;
544
+ /** Also include the files that import a changed one. */
545
+ withDependents?: boolean;
433
546
  /** Empty means "no allow-list"; every discovered file is a candidate. */
434
547
  include: string[];
435
548
  /** The preset that was resolved (detected or forced). */
@@ -455,9 +568,24 @@ interface CliOptions {
455
568
  includeSensitiveFiles?: boolean;
456
569
  maxMonolithBytes?: number | null;
457
570
  maxPartBytes?: number;
571
+ maxPartTokens?: ByteSize | null;
458
572
  largeFileBytes?: number;
459
573
  /** Artifact names to suppress for this invocation (`--skip-artifacts`). */
460
574
  skipArtifacts?: string[];
575
+ /**
576
+ * Artifact names to keep, with everything else off (`--only-artifacts`).
577
+ *
578
+ * Separate from the derived `skipArtifacts` because the two are not interchangeable: "only
579
+ * these" is a statement about the whole selection, and the suppression list cannot express it
580
+ * when the configuration has already turned one of the named artifacts off.
581
+ */
582
+ onlyArtifacts?: string[];
583
+ discovery?: DiscoveryMode;
584
+ changedSince?: string;
585
+ staged?: boolean;
586
+ withDependents?: boolean;
587
+ skeleton?: boolean;
588
+ sarif?: boolean;
461
589
  }
462
590
  interface ValidationDrift {
463
591
  missingInSnapshot: string[];
@@ -21,7 +21,7 @@ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
21
21
  * `full` – the complete file body was emitted.
22
22
  * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
23
  */
24
- type InclusionMode = "full" | "preview";
24
+ type InclusionMode = "full" | "preview" | "signatures";
25
25
  interface SourceSummary {
26
26
  directives: string[];
27
27
  imports: string[];
@@ -88,7 +88,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
88
88
  * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
89
  * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
90
  */
91
- | "nested-repository";
91
+ | "nested-repository"
92
+ /**
93
+ * Not in the set that `--changed-since` or `--staged` asked for.
94
+ *
95
+ * Deliberately distinct from `excluded`, which means the user's own glob rejected the path.
96
+ * Here the file is perfectly includable and simply did not change; reporting it as
97
+ * `excluded` would send a reader looking for an exclude pattern they never wrote.
98
+ */
99
+ | "unchanged";
92
100
  interface SkipRecord {
93
101
  /** Path as it may be shown in an artifact: secret spans are masked. */
94
102
  path: string;
@@ -144,6 +152,23 @@ interface PartMetadata {
144
152
  filename: string;
145
153
  /** Exact UTF-8 byte length of the emitted part file. */
146
154
  byteSize: number;
155
+ /**
156
+ * Estimated tokens in this part.
157
+ *
158
+ * `byteSize` answers "will this fit the byte budget"; this answers the question a caller
159
+ * actually has, which is "will this fit my context window". An estimate, and labelled as
160
+ * one: `estimateTokens` documents exactly what it counts.
161
+ */
162
+ estimatedTokens: number;
163
+ /**
164
+ * The byte budget this part was partitioned against.
165
+ *
166
+ * With per-layer budgets a snapshot has several budgets at once, so "within budget"
167
+ * cannot be answered without knowing which one applied. Verification compared every
168
+ * part against the *largest* budget in the run, which passed a part that was over its
169
+ * own.
170
+ */
171
+ budgetBytes: number;
147
172
  sha256: string;
148
173
  lines: number;
149
174
  fileCount: number;
@@ -198,6 +223,14 @@ interface ManifestTotals {
198
223
  skippedFiles: number;
199
224
  totalSourceBytes: number;
200
225
  totalContentBytes: number;
226
+ /**
227
+ * Estimated tokens in the emitted parts.
228
+ *
229
+ * Optional because a manifest written before this was recorded has no such key. The number
230
+ * is an estimate — see `estimateTokens` for the method — and it exists because the snapshot
231
+ * is pasted into a context window, which is measured in tokens rather than bytes.
232
+ */
233
+ estimatedTokens?: number;
201
234
  partCount: number;
202
235
  warningCount: number;
203
236
  /**
@@ -256,13 +289,34 @@ interface ManifestConfigSnapshot {
256
289
  /** Digest of the effective settings; see `configDigest`. */
257
290
  digest?: string;
258
291
  maxPartBytes: number;
292
+ /**
293
+ * The token ceiling the run was given, or `undefined` when only bytes were named.
294
+ *
295
+ * Recorded so the manifest states the budget the user chose rather than only the byte
296
+ * value it was converted to.
297
+ */
298
+ maxPartTokens?: number;
259
299
  largeFileBytes: number;
260
300
  previewHeadLines: number;
261
301
  previewTailLines: number;
262
302
  includeMonolith: boolean;
303
+ /**
304
+ * The ceiling above which the monolith is not written; `null` for no ceiling.
305
+ *
306
+ * Optional because a manifest written before this was recorded has no such key.
307
+ */
308
+ maxMonolithBytes?: number | null;
263
309
  includeSensitiveFiles: boolean;
264
310
  /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
311
  discovery?: DiscoveryMode;
312
+ /** The changed-files scope the snapshot was taken over, when one was used. */
313
+ changedSince?: string;
314
+ staged?: boolean;
315
+ withDependents?: boolean;
316
+ /** True when bodies were replaced by signatures. */
317
+ skeleton?: boolean;
318
+ /** True when `reports/audit.sarif` was written. */
319
+ sarif?: boolean;
266
320
  artifacts: ResolvedArtifacts;
267
321
  include: string[];
268
322
  preset: string;
@@ -315,6 +369,14 @@ interface Manifest {
315
369
  artifacts: {
316
370
  fullJson: ManifestArtifact | null;
317
371
  fullMarkdown: ManifestArtifact | null;
372
+ /**
373
+ * `reports/audit.sarif`, when `--sarif` asked for it.
374
+ *
375
+ * Optional because most runs do not write one, and a manifest written before this
376
+ * existed has no such key at all — the two are indistinguishable to a reader, which is
377
+ * why the presence of the file is also recorded in `config`.
378
+ */
379
+ sarif?: ManifestArtifact;
318
380
  };
319
381
  layers: Record<string, number>;
320
382
  kinds: Record<string, number>;
@@ -382,6 +444,15 @@ interface SnapshotConfig {
382
444
  tmpDir?: string;
383
445
  /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
446
  maxPartBytes?: ByteSize;
447
+ /**
448
+ * A per-part ceiling in *tokens* rather than bytes.
449
+ *
450
+ * The snapshot is pasted into a context window, and a context window is measured in tokens;
451
+ * bytes are what the partitioner writes. Naming a token budget sets `maxPartBytes` to the
452
+ * byte count that corresponds to it, so the byte bound — the thing the partitioner actually
453
+ * enforces — never stops being true. The smaller of the two ceilings wins.
454
+ */
455
+ maxPartTokens?: ByteSize;
385
456
  largeFileBytes?: ByteSize;
386
457
  previewHeadLines?: number;
387
458
  previewTailLines?: number;
@@ -393,6 +464,31 @@ interface SnapshotConfig {
393
464
  include?: string[];
394
465
  /** Force a preset by name instead of auto-detecting one. */
395
466
  preset?: string;
467
+ /**
468
+ * Only include files changed since this git ref.
469
+ *
470
+ * The pull-request case: the diff is small and the repository is not. Unchanged files are
471
+ * recorded in `skipped[]` with reason `unchanged`, so the manifest accounts for them.
472
+ */
473
+ changedSince?: string;
474
+ /** Only include files staged for the next commit. */
475
+ staged?: boolean;
476
+ /**
477
+ * Emit signatures instead of bodies for TypeScript and TSX files.
478
+ *
479
+ * Matches `inclusionMode: "signatures"` on the entries it applies to, so a reader can tell a
480
+ * compressed file from a whole one without diffing it against the source.
481
+ */
482
+ skeleton?: boolean;
483
+ /** Also write `reports/audit.sarif` so a CI security gate can read the findings. */
484
+ sarif?: boolean;
485
+ /**
486
+ * With `changedSince` or `staged`, also include the files that import a changed one.
487
+ *
488
+ * A one-hop closure: the file you changed and the files that reference it are the context
489
+ * needed to change it safely.
490
+ */
491
+ withDependents?: boolean;
396
492
  /** Switch individual snapshot artifacts off. Everything is on by default. */
397
493
  artifacts?: ArtifactSelection;
398
494
  /**
@@ -418,6 +514,8 @@ interface ResolvedSnapshotConfig {
418
514
  outDir: string;
419
515
  tmpDir: string;
420
516
  maxPartBytes: number;
517
+ /** The token ceiling the user asked for, before it was turned into `maxPartBytes`. */
518
+ maxPartTokens?: number;
421
519
  largeFileBytes: number;
422
520
  previewHeadLines: number;
423
521
  previewTailLines: number;
@@ -430,6 +528,21 @@ interface ResolvedSnapshotConfig {
430
528
  * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
529
  */
432
530
  discovery?: DiscoveryMode;
531
+ /** Only files changed since this git ref; `undefined` scans everything. */
532
+ changedSince?: string;
533
+ /** Only files staged for the next commit. */
534
+ staged?: boolean;
535
+ /** Emit signatures instead of bodies for TypeScript and TSX files. */
536
+ skeleton?: boolean;
537
+ /**
538
+ * Also write `reports/audit.sarif`, the findings in SARIF 2.1.0.
539
+ *
540
+ * For a CI security gate: GitHub Code Scanning reads SARIF, and a markdown report it cannot
541
+ * ingest is a finding nobody's build can act on.
542
+ */
543
+ sarif?: boolean;
544
+ /** Also include the files that import a changed one. */
545
+ withDependents?: boolean;
433
546
  /** Empty means "no allow-list"; every discovered file is a candidate. */
434
547
  include: string[];
435
548
  /** The preset that was resolved (detected or forced). */
@@ -455,9 +568,24 @@ interface CliOptions {
455
568
  includeSensitiveFiles?: boolean;
456
569
  maxMonolithBytes?: number | null;
457
570
  maxPartBytes?: number;
571
+ maxPartTokens?: ByteSize | null;
458
572
  largeFileBytes?: number;
459
573
  /** Artifact names to suppress for this invocation (`--skip-artifacts`). */
460
574
  skipArtifacts?: string[];
575
+ /**
576
+ * Artifact names to keep, with everything else off (`--only-artifacts`).
577
+ *
578
+ * Separate from the derived `skipArtifacts` because the two are not interchangeable: "only
579
+ * these" is a statement about the whole selection, and the suppression list cannot express it
580
+ * when the configuration has already turned one of the named artifacts off.
581
+ */
582
+ onlyArtifacts?: string[];
583
+ discovery?: DiscoveryMode;
584
+ changedSince?: string;
585
+ staged?: boolean;
586
+ withDependents?: boolean;
587
+ skeleton?: boolean;
588
+ sarif?: boolean;
461
589
  }
462
590
  interface ValidationDrift {
463
591
  missingInSnapshot: string[];
package/docs/agent.md CHANGED
@@ -115,7 +115,7 @@ devDependency and the `.gitignore` entries for the snapshot directory, and nothi
115
115
 
116
116
  ## When the client speaks MCP
117
117
 
118
- `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: seven
118
+ `codebase-snapshot mcp` serves the repository to a Model Context Protocol client over stdio: eight
119
119
  tools, `snapshot://` resources and two prompts. Prefer it over shelling out when the agent already
120
120
  has an MCP connection, for two reasons that are properties of the design rather than of the wiring:
121
121
 
package/docs/cli.md CHANGED
@@ -174,7 +174,7 @@ The whole first run in one command, with defaults and no prompts. This is the sa
174
174
  🚀 Setup for: /path/to/repo
175
175
  ✅ Created snapshot.config.ts (preset: nextjs)
176
176
  Added npm scripts: snapshot, snapshot:validate
177
- Added devDependency: codebase-snapshot ^0.9.9
177
+ Added devDependency: codebase-snapshot ^0.9.14
178
178
  Added to .gitignore: snapshot/, snapshot.tmp/
179
179
  ⏳ Installing dependencies…
180
180
  ✅ Installed dependencies (npm install, 2.1s)
@@ -192,7 +192,9 @@ The whole first run in one command, with defaults and no prompts. This is the sa
192
192
  `generate` and `init`.
193
193
  - `--format json` emits **one** document:
194
194
  `{ "setup": { configPath, configReused, preset, scripts, dependency, gitignoreEntries, install, notes }, "manifest": { … } }`,
195
- where `manifest` is the same object `generate --format json` writes.
195
+ where `manifest` is the same object `generate --format json` writes. If that generate fails, the
196
+ document is `{ "error": { … }, "setup": { … }, "manifest": null }` and the exit code is the
197
+ generate's: one document per invocation, a payload or an error, never a payload holding an error.
196
198
  - If `outDir` holds files this tool did not write, `setup` still refuses to replace it (exit 1,
197
199
  naming the entries) after writing the config — the guard is not weakened by the convenience.
198
200
  ### `init`
@@ -211,7 +213,7 @@ It also has three side effects on files you own, and it says so when it does the
211
213
  ```
212
214
  ✅ Created snapshot.config.ts (preset: node-lib)
213
215
  Added npm scripts: snapshot, snapshot:validate
214
- Added devDependency: codebase-snapshot ^0.9.9
216
+ Added devDependency: codebase-snapshot ^0.9.14
215
217
  Added to .gitignore: snapshot/, snapshot.tmp/
216
218
  ```
217
219
 
@@ -378,7 +380,7 @@ codebase-snapshot mcp # serve the current directory over stdio
378
380
  codebase-snapshot mcp --cwd <path> # serve another repository
379
381
  ```
380
382
 
381
- Serves the repository to a Model Context Protocol client over stdio: seven tools, `snapshot://`
383
+ Serves the repository to a Model Context Protocol client over stdio: eight tools, `snapshot://`
382
384
  resources and two prompts. Stdout carries JSON-RPC messages and nothing else; every note goes to
383
385
  stderr. It speaks both protocol eras — the legacy `initialize` handshake and the stateless
384
386
  `2026-07-28` revision — and answers whichever the client starts with.
@@ -401,15 +403,22 @@ narrowest cases of the same rule.
401
403
  | `--cwd <path>` | path | current directory | generate, validate, report, explain, init, skill | Repository to operate on. Must exist and be a directory. `diff` and `describe` take no `--cwd`. |
402
404
  | `--out-dir <path>` | path | `snapshot` | generate, validate, report, explain, init | Where the snapshot is written or read. May be absolute. |
403
405
  | `--max-part-bytes <n>` | size | `307200` (300 KB) | generate, validate, report, explain, init | Hard ceiling per markdown part. Accepts `120k`, `1.5mb`. Minimum 4096. |
406
+ | `--max-part-tokens <n>` | size | unset | generate, validate, setup | A per-part ceiling in **tokens**, for a context window rather than a disk. Narrows `--max-part-bytes` to the equivalent byte count (`tokens × 4`); the smaller of the two ceilings always wins, and both are recorded in the manifest. Accepts `32k`. Minimum 1024 — below that it is a usage error, not a value quietly raised to the floor. |
404
407
  | `--large-file-bytes <n>` | size | `204800` (200 KB) | generate, validate, report, explain | Above this, a file becomes a head/tail preview. |
405
408
  | `--max-monolith-bytes <n>` | size | unset | generate, validate, report, explain | Skip the single-file monolith when it would exceed this. |
406
409
  | `--include-monolith` | flag | off | generate, validate, report, explain | Also write `full-codebase.json` and `full-codebase.md`. |
407
410
  | `--include-sensitive-files` | flag | off | generate, validate, report, explain, init | Emit `.env`, `*.pem`, `.npmrc` and similar. Off by default; they are reported as skipped. |
408
411
  | `--skip-artifacts <names>` | list | unset | generate, validate, report, explain, init | Comma-separated artifacts to omit. |
409
- | `--only-artifacts <names>` | list | unset | generate, validate, report, explain, init | Write only these. Cannot be combined with `--skip-artifacts`. |
412
+ | `--only-artifacts <names>` | list | unset | generate, validate, report, explain, init | Write *only* these, and **turn on** any this names: a config with `artifacts: { audit: false }` still gets `reports/audit.md` from `--only-artifacts audit`, because "only these" is a statement about the whole selection rather than a suppression list. Cannot be combined with `--skip-artifacts`. |
410
413
  | `--format <text\|json>` | enum | `text` | generate, validate, report, explain, init, setup, diff, describe, skill | `json` writes a pure JSON payload to stdout. Every command honours it, and `describe` reports the same nine-command list. |
414
+ | `--sarif` | flag | off | generate, validate, setup | Also write `reports/audit.sarif`: the redaction findings as SARIF 2.1.0, for upload to GitHub Code Scanning. The artefact carries the path, the line and the rule — never the value. Works with `--skip-artifacts audit`, because a CI gate does not read markdown. Rule ids are fixed per rule name, so adding a custom rule never re-points an existing alert. |
415
+ | `--skeleton` | flag | off | generate, validate, setup | Emit signatures instead of bodies: each function or method body becomes `{ /* ... */ }` in place, so formatting and comments around it are untouched. Covers `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs` and `.cjs`; `.d.ts` files are left alone because they are already declarations. The summary still describes the whole file, and affected entries carry `inclusionMode: "signatures"`. `--include-monolith` still writes the whole body, so the monolith's `inclusionMode: "full"` stays true. |
416
+ | `--changed-since <ref>` | git ref | unset | generate, validate, explain | Include only files changed since this ref, plus untracked ones. Every other file is recorded in `manifest.skipped[]` with reason `unchanged` and is never opened. `validate` re-resolves the scope: a skipped file that has changed since is reported as drift, and one that has not is left alone — and with `--with-dependents`, so is a file that now imports one that changed. `explain` answers with the same rule. A ref beginning with `-` is refused. |
417
+ | `--staged` | flag | off | generate, validate, explain | Include only files staged for the next commit. |
418
+ | `--with-dependents` | flag | off | generate, validate, explain | With either of the two above, also include the files that import a changed one — a one-hop closure, not a transitive walk. Needs a scope; alone it is a usage error. |
419
+ | `--install-hook` | flag | off | init, setup | Write `.git/hooks/pre-commit` so a commit is refused while the snapshot is out of date. The hook checks the `outDir` the config names, so it works for a project that does not use `snapshot/`. An existing hook is never replaced; the note says what to add to it instead. |
411
420
  | `--dry-run` | flag | off | generate, init, setup | Compute everything, print it, write nothing. |
412
- | `--watch` | flag | off | generate | Regenerate on every change until interrupted. |
421
+ | `--watch` | flag | off | generate | Regenerate on every change until interrupted. Redaction and parsing are cached per file across runs in the same process, keyed on the content hash, so a one-file edit regenerates from that one file instead of from the whole repository. The cache is memory only and is dropped when the process exits. |
413
422
  | `--autopilot` | flag | off | generate | Set the project up without asking — config, `.gitignore`, npm scripts, devDependency, install — then generate. Never overwrites an existing config; refuses an empty directory (exit `1`); contradicts `--no-config` (exit `2`). |
414
423
  | `--no-install` | flag | off | setup, generate `--autopilot` | Skip the dependency install. Anywhere else it is a usage error, because it would change nothing. |
415
424
  | `--no-config` | flag | off | generate, validate, report, explain, diff | Ignore every `snapshot.config.*` file and run on the built-in defaults. `diff` never loads a config anyway and accepts the flag for consistency. |
@@ -430,6 +439,17 @@ with and the comparison is like for like. `validate --skip-artifacts parts` on a
430
439
  built without parts therefore agrees, while the same flag on a full snapshot reports a
431
440
  config mismatch.
432
441
 
442
+ The same rule applies to the compression and scope options. `--skeleton`, `--sarif` and
443
+ `--max-part-tokens` are recorded in `manifest.config.digest`, so `validate` accepts them too —
444
+ without that, a snapshot generated with `--skeleton` could not be validated at all, and passing
445
+ the flag to `validate` was rejected as a flag that "does not apply to validate". `--changed-since`,
446
+ `--staged` and `--with-dependents` are accepted by `validate` and `explain` because they decide
447
+ which recorded `unchanged` skips are re-checked and what `explain` answers about one file.
448
+
449
+ `report` does not take any of them: it reads the manifest and prints what is there, so a
450
+ generation setting would change nothing. A flag that parses and does nothing is the silent
451
+ fallback this table exists to prevent.
452
+
433
453
  Every option also accepts the `--option=value` form. That form is required for negative
434
454
  numbers, which otherwise look like flags.
435
455
 
@@ -188,6 +188,11 @@ Classification drives per-layer budgets and the `Layers:` breakdown in `report`.
188
188
  ```js
189
189
  export default {
190
190
  maxPartBytes: "300k", // hard ceiling per part; default 307200, minimum 4096
191
+ // maxPartTokens: 32000, // a per-part ceiling in tokens; narrows maxPartBytes (tokens x 4)
192
+ // changedSince: "main", // only files changed since this git ref
193
+ // staged: true, // only files staged for the next commit
194
+ // withDependents: true, // ...and the files that import them
195
+ // skeleton: true, // signatures instead of bodies (--skeleton); TS, JS, TSX and JSX
191
196
  largeFileBytes: "200k", // above this a file becomes a head/tail preview
192
197
  previewHeadLines: 80,
193
198
  previewTailLines: 40,
@@ -224,8 +229,9 @@ export default {
224
229
  ```
225
230
 
226
231
  Everything except `monolith` is on by default; `monolith` follows `includeMonolith`. Turn
227
- artifacts off with the config, `--skip-artifacts a,b`, or `--only-artifacts a,b` (the two
228
- cannot be combined).
232
+ artifacts off with the config or `--skip-artifacts a,b`. `--only-artifacts a,b` is different: it
233
+ *writes only* those and turns each named artifact **on**, so `--only-artifacts audit` still produces
234
+ `reports/audit.md` in a project whose config says `audit: false`. The two flags cannot be combined.
229
235
 
230
236
  Two combinations warn because they produce a snapshot that contradicts itself: `manifest` off
231
237
  means `validate` and `report` cannot inspect the snapshot, and `parts` off with `navigation`
@@ -261,19 +267,52 @@ makes an audit entry mean something.
261
267
  matched span is masked, never the whole line, and no excerpt of a matched line reaches any
262
268
  artifact. Multi-line PEM keys are masked as a block.
263
269
  - `customPatterns` add rules; `disableDefaultPatterns` removes the built-in set — it does not
264
- disable redaction itself.
270
+ disable redaction itself, and it applies to **paths** as well as to content, so a
271
+ credential-shaped file name is no longer skipped by a rule you switched off.
265
272
 
266
273
  Redaction is **fail-closed**, so a placeholder is indistinguishable from a secret: a
267
274
  `.env.example` whose line reads `JWT_SECRET=replace-with-base64url-48-bytes` is masked like a
268
275
  live key would be, and a test fixture literal such as `process.env.X_SECRET = "test-value"` is
269
- masked too. The same holds for an environment reference written as a quoted value —
270
- `password: "${DB_PASSWORD}"` is masked exactly like a literal password, because the rule sees a
271
- credential-shaped key followed by a string and a `${…}` reference cannot be told apart from a
272
- secret by shape alone. That is the intended trade-off — the alternative is guessing which values
273
- are throwaway — and it is worth knowing before you hand the snapshot to a model and find the
274
- template less readable than the original. To narrow it, remove a rule by name with
276
+ masked too. That is the intended trade-off — the alternative is guessing which values are
277
+ throwaway — and it is worth knowing before you hand the snapshot to a model and find the template
278
+ less readable than the original. To narrow it, remove a rule by name with
275
279
  `disableDefaultPatterns: true` plus your own `customPatterns`.
276
280
 
281
+ The one shape that is *not* masked is a value that is a single `${…}` reference and nothing else —
282
+ `password: "${DB_PASSWORD}"`, `` `${process.env.TOKEN}` `` — because that is an expression and
283
+ masking it rewrites a program (or an assertion message) rather than hiding a credential. Anything
284
+ inside the braces that is not a plain reference settles it the other way:
285
+ `password: "${DB_PASSWORD:-hunter2}"` is masked, because the shell and Compose idiom for a default
286
+ puts a **hardcoded credential** in the fallback position. So does a second interpolation, or any
287
+ quote inside the braces.
288
+
289
+ The same default is masked when it is not quoted, which is how it is usually written:
290
+
291
+ ```bash
292
+ # .env, docker-compose.yml, a workflow, a shell script — all of these are masked
293
+ POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-MyProdPassword123!}
294
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-MyProdPassword123!}
295
+ ```
296
+
297
+ The fallback is what gets replaced; the variable name stays, exactly as `POSTGRES_PASSWORD:` does.
298
+ `${PORT:-3000}` is untouched: the name is not a credential name, and a port number is not one
299
+ either.
300
+
301
+ A credential assigned to a name inside a string is also masked — a connection string is a string:
302
+
303
+ ```js
304
+ const dsn = "password=MySuperSecret123;host=db.internal";
305
+ ```
306
+
307
+ What is left alone is a credential *name* in a literal, with nothing assigned to it:
308
+ `["awsSecret:", "apiKey:"]` is a list of keys, not two credentials, and a documentation line showing
309
+ `password: "…"` inside a code span is showing an example. The difference is whether the literal
310
+ closes between the name and the value.
311
+
312
+ `readonly` and `declare` are shell assignments in a shell file and declarations in a source file, and
313
+ the file decides: `readonly api_key=hunter2` in `.envrc`, `.sh`, `.zsh`, a YAML or an INI file is
314
+ masked, while `private readonly apiKey = someValue` in TypeScript is left alone.
315
+
277
316
  Findings are summarised in `reports/audit.md` (one row per line, naming every rule that matched
278
317
  it), counted in `manifest.totals.redactedLines`, and shown in the `generate` summary as a
279
318
  `Redacted:` line separate from `Warnings:`.