@ankurhalder/codebase-snapshot 4.0.6 → 4.1.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.
@@ -1,5 +1,21 @@
1
1
  type Mode = "generate" | "validate" | "report";
2
2
  type OutputFormat = "text" | "json";
3
+ /**
4
+ * How files are discovered.
5
+ *
6
+ * `auto` lists files through git when git can do it and walks the tree otherwise; `git` and `walk`
7
+ * force one path. It is part of the manifest's recorded settings because it decides which files a
8
+ * snapshot contains — the same repository scanned both ways is two different snapshots.
9
+ */
10
+ type DiscoveryMode = "auto" | "git" | "walk";
11
+ /**
12
+ * How a file's bytes became text.
13
+ *
14
+ * Absent from a `FileEntry` means plain UTF-8, which is the overwhelmingly common case. The other
15
+ * values are recorded because they change what the artifact actually contains: `utf-16le`/`utf-16be`
16
+ * files were transcoded, and `utf-8-lossy` files carry U+FFFD where their undecodable bytes were.
17
+ */
18
+ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
3
19
  /**
4
20
  * How much of a file made it into the snapshot.
5
21
  * `full` – the complete file body was emitted.
@@ -64,7 +80,15 @@ type SkipReason = "binary" | "read-error" | "not-file"
64
80
  /** Excluded by a built-in rule: the ignore list, a preset, or an output dir. */
65
81
  | "default-ignore"
66
82
  /** Excluded by the user's own `excludePatterns` or `include` allow-list. */
67
- | "excluded";
83
+ | "excluded"
84
+ /**
85
+ * A directory that belongs to another repository: a nested checkout or a submodule.
86
+ *
87
+ * A submodule is a single gitlink entry, so `stat` reports a directory where a file was
88
+ * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
+ * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
+ */
91
+ | "nested-repository";
68
92
  interface SkipRecord {
69
93
  /** Path as it may be shown in an artifact: secret spans are masked. */
70
94
  path: string;
@@ -99,6 +123,11 @@ interface FileEntry {
99
123
  summaryQuality?: SummaryQuality;
100
124
  /** True when the summary alone was too large for the part budget and was dropped. */
101
125
  summaryOmitted?: boolean;
126
+ /**
127
+ * How the file's bytes were decoded. Absent means plain UTF-8. Present when the file was
128
+ * transcoded from UTF-16, or when it was not valid UTF-8 and lost bytes to U+FFFD.
129
+ */
130
+ encoding?: TextEncoding;
102
131
  /** Emitted body after redaction/truncation. Never serialized into the manifest. */
103
132
  content?: string;
104
133
  }
@@ -215,6 +244,8 @@ interface ManifestConfigSnapshot {
215
244
  previewTailLines: number;
216
245
  includeMonolith: boolean;
217
246
  includeSensitiveFiles: boolean;
247
+ /** How files were discovered; `undefined` on manifests written before this was recorded. */
248
+ discovery?: DiscoveryMode;
218
249
  artifacts: ResolvedArtifacts;
219
250
  include: string[];
220
251
  preset: string;
@@ -368,6 +399,12 @@ interface ResolvedSnapshotConfig {
368
399
  includeMonolith: boolean;
369
400
  maxMonolithBytes: number | null;
370
401
  includeSensitiveFiles: boolean;
402
+ /**
403
+ * How files are discovered; `auto` picks git when it can list files and walks the tree otherwise.
404
+ * Optional because a resolved config can arrive from an older caller that never named it — every
405
+ * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
406
+ */
407
+ discovery?: DiscoveryMode;
371
408
  /** Empty means "no allow-list"; every discovered file is a candidate. */
372
409
  include: string[];
373
410
  /** The preset that was resolved (detected or forced). */
@@ -400,6 +437,8 @@ interface CliOptions {
400
437
  interface ValidationDrift {
401
438
  missingInSnapshot: string[];
402
439
  missingOnDisk: string[];
440
+ /** Files that exist but could not be read, so their content could not be verified. */
441
+ unreadable: string[];
403
442
  hashMismatches: Array<{
404
443
  path: string;
405
444
  diskHash: string;
@@ -424,4 +463,4 @@ interface ValidationResult {
424
463
  warnings: string[];
425
464
  }
426
465
 
427
- export type { ArtifactSelection as A, ByteSize as B, CliOptions as C, FileEntry as F, HooksConfig as H, InclusionMode as I, JsonSummary as J, KindRule as K, LayerRule as L, MarkdownSummary as M, OutputFormat as O, PartMetadata as P, ResolvedSnapshotConfig as R, SourceSummary as S, ValidationResult as V, SnapshotConfig as a, SecretRule as b, SecretFinding as c, Manifest as d, SkipRecord as e, BudgetConfig as f, ManifestArtifact as g, ManifestConfigSnapshot as h, ManifestIndexes as i, ManifestTotals as j, Mode as k, PartFileEntry as l, ResolvedArtifacts as m, ResolvedBudgets as n, SecretRuleInput as o, SkipReason as p, SummaryQuality as q, ValidationDrift as r };
466
+ export type { ArtifactSelection as A, ByteSize as B, CliOptions as C, DiscoveryMode as D, FileEntry as F, HooksConfig as H, InclusionMode as I, JsonSummary as J, KindRule as K, LayerRule as L, MarkdownSummary as M, OutputFormat as O, PartMetadata as P, ResolvedSnapshotConfig as R, SecretRule as S, TextEncoding as T, ValidationResult as V, SkipRecord as a, SourceSummary as b, SnapshotConfig as c, SecretFinding as d, Manifest as e, BudgetConfig as f, ManifestArtifact as g, ManifestConfigSnapshot as h, ManifestIndexes as i, ManifestTotals as j, Mode as k, PartFileEntry as l, ResolvedArtifacts as m, ResolvedBudgets as n, SecretRuleInput as o, SkipReason as p, SummaryQuality as q, ValidationDrift as r };
package/docs/agent.md CHANGED
@@ -117,4 +117,6 @@ When you parse the manifest, remember what is conditional: `manifest.files[]` ca
117
117
  `relativePath`, `layer`, `kind`, `language`, `part`, `inclusionMode` and `hashSha256` for
118
118
  every included file, but `summary` and `summaryQuality` only for a language the tool can
119
119
  analyse. A `.gitignore` or plain-text entry has neither key, and a file whose summary was too
120
- large for its part carries `summaryOmitted: true` instead of a `summary`.
120
+ large for its part carries `summaryOmitted: true` instead of a `summary`. `encoding` appears only
121
+ when a file was not plain UTF-8: `utf-16le`/`utf-16be` were transcoded, and `utf-8-lossy` means
122
+ the body you are reading contains `U+FFFD` where undecodable bytes were.
package/docs/cli.md CHANGED
@@ -175,6 +175,7 @@ With `--format json` the same answer is one object on stdout, with only the keys
175
175
  | `layer`, `kind`, `language` | The classification, present only when `included` is `true`. |
176
176
  | `layerMatch`, `kindMatch` | Which rule chose the layer or kind. A glob is printed as written (`src/api/**`); a `RegExp` as its literal form (`/\.tsx$/i`); a function matcher, which is what the presets use, as `function (custom predicate)`. Absent when no rule matched and the `other` fallback was used. |
177
177
  | `bytes`, `lines` | Size of the file on disk. |
178
+ | `encoding` | Present only when the file is not plain UTF-8 — `utf-16le`, `utf-16be` or `utf-8-lossy`. |
178
179
  | `secretCount`, `secrets` | How many secret rules fire, and the first 20 as `{ rule, line }` — never the matched text. |
179
180
  | `part` | The part filename from the last written manifest, or `null` when it is not in that snapshot. |
180
181
  | `inclusionMode` | `full` or `preview`, when a part entry exists. |
@@ -193,6 +194,20 @@ Compares two snapshots and lists added, removed and changed files. Either argume
193
194
  snapshot directory or a `manifest.json`. Useful in review: "what changed in the context we
194
195
  hand to the model?"
195
196
 
197
+ Comparison is per file (`relativePath` + `sha256`), so on its own it cannot see a run that
198
+ repacked the *same* sources differently. `diff` therefore also compares the two manifests'
199
+ `generator.version`, `config.digest`, content fingerprint, part count and layer totals, and
200
+ prints a `payload/configuration differs:` section when the file set is identical but the
201
+ emitted payload — or the configuration that produced it — is not. That covers `--large-file-bytes`
202
+ (a preview becomes a full body), the preview head/tail counts, redaction rules, artifact
203
+ selection and per-layer budgets, none of which move a source hash. The per-file lists and the
204
+ JSON fields `added` / `removed` / `changed` / `unchanged` are unchanged; the extra data is
205
+ under `payload` in `--format json`.
206
+
207
+ Manifests written before 4.0.5 have no `config.digest`. In that case `diff` compares the
208
+ content fingerprint and the part count only, and prints `configuration not compared: a
209
+ manifest has no config digest (written before 4.0.5).` rather than implying the settings match.
210
+
196
211
  ### `describe`
197
212
 
198
213
  ```bash
@@ -242,13 +257,14 @@ narrowest cases of the same rule.
242
257
  | `--format <text\|json>` | enum | `text` | every command | `json` writes a pure JSON payload to stdout. |
243
258
  | `--dry-run` | flag | off | generate, init | Compute everything, print it, write nothing. |
244
259
  | `--watch` | flag | off | generate | Regenerate on every change until interrupted. |
245
- | `--no-config` | flag | off | generate, validate, report, explain | Ignore every `snapshot.config.*` file and run on the built-in defaults. |
260
+ | `--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. |
261
+ | `--discovery <mode>` | enum | `auto` | generate, validate, explain | How files are found: `auto` lists through git when git works and refuses when a `.git` directory cannot be listed; `git` requires git; `walk` scans the tree and reads `.gitignore` files directly. The escape hatch for a broken `.git`. |
246
262
  | `--preset <name>` | name | detected | *init only* | Force a preset instead of detecting one. |
247
263
  | `--include <globs>` | list | unset | *init only* | Allow-list recorded in the generated config. |
248
264
  | `--exclude <globs>` | list | unset | *init only* | Exclude globs recorded in the generated config. |
249
265
  | `--yes` | flag | off | *init only* | Never prompt; accept every default. |
250
266
  | `--minimal` | flag | off | *init only* | Write only settings that differ from the defaults. |
251
- | `--force` | flag | off | *init only* | Overwrite an existing config. |
267
+ | `--force` | flag | off | *init only* | Replace an existing `.ts`/`.js`/`.mjs` config in place. A JSON-family config is refused, naming the file to remove. |
252
268
  | `--install` | flag | off | *skill only* | Write the skill into the repository instead of printing it. |
253
269
  | `--target <name>` | enum | `all` | *skill only* | `claude`, `agents`, `cursor` or `all`. |
254
270
  | `-h`, `--help` | flag | — | every command | Print usage. |
@@ -263,6 +279,21 @@ config mismatch.
263
279
  Every option also accepts the `--option=value` form. That form is required for negative
264
280
  numbers, which otherwise look like flags.
265
281
 
282
+ ### `--mode`
283
+
284
+ `--mode <command>` names the command instead of passing it positionally, which is handy when
285
+ a wrapper builds the argument list. `--mode validate` and `codebase-snapshot validate` are the
286
+ same run. It accepts the nine command names, and it is deliberately strict: asking for two
287
+ different commands is an error rather than a silent winner.
288
+
289
+ ```
290
+ ❌ --mode validate contradicts the command "generate". Pass one or the other.
291
+ ❌ Unknown --mode "bogus". Expected one of: generate, validate, check, report, init, explain, diff, describe, skill.
292
+ ```
293
+
294
+ `--mode` is not listed in `describe`'s option table or `--help`, on purpose: an agent has no
295
+ reason to use it, and the positional form is the documented one.
296
+
266
297
  ## Exit codes
267
298
 
268
299
  | Code | Meaning |
@@ -297,7 +328,7 @@ npx @ankurhalder/codebase-snapshot generate --out-dir .context --max-part-bytes
297
328
  npx @ankurhalder/codebase-snapshot generate --only-artifacts parts,manifest
298
329
 
299
330
  # One file per part, with everything else left alone
300
- npx @ankurhalder/codebase-snapshot validate
331
+ npx @ankurhalder/codebase-snapshot generate --only-artifacts parts
301
332
 
302
333
  # Why is this file missing?
303
334
  npx @ankurhalder/codebase-snapshot explain src/generated/schema.ts
@@ -64,12 +64,26 @@ see [the trust boundary](../SECURITY.md#trust-boundary).
64
64
  export default {
65
65
  include: [], // allow-list; empty means "everything"
66
66
  excludePatterns: [], // extra excludes, applied on top of the preset's
67
+ discovery: "auto", // "auto" | "git" | "walk" — how files are found
67
68
  };
68
69
  ```
69
70
 
70
71
  - `include` is an allow-list of globs. When non-empty, only matching files are considered.
72
+ - `exclude` is accepted as an alias of `excludePatterns` and means the same thing; the resolved
73
+ config always records it under `excludePatterns`.
71
74
  - `excludePatterns` removes files everywhere. Presets contribute their own ecosystem ignores
72
75
  on top; those are not removable, only addable to.
76
+ - `discovery` chooses how the file list is produced, and is recorded in `manifest.config`, so
77
+ changing it shows up as config drift:
78
+ - `"auto"` (default) — list through `git ls-files` when git can do it; if a `.git` directory
79
+ exists but git cannot list the files, refuse rather than silently switch methods.
80
+ - `"git"` — require git; a directory without a usable repository is an error.
81
+ - `"walk"` — scan the tree and read `.gitignore` files directly (nested ones included), still
82
+ skipping credential files. Use it when a `.git` directory cannot be listed. It cannot
83
+ reproduce git's own ignore sources (`.git/info/exclude`, your global `core.excludesFile`),
84
+ git's inclusion of files that are tracked although a `.gitignore` now matches them, or its
85
+ treatment of a submodule as a single entry. The same setting is available per run as
86
+ [`--discovery`](cli.md#options).
73
87
 
74
88
  Built-in ignores always apply: `.git`, `node_modules`, `.next`, `dist`, `build`, lockfiles,
75
89
  minified assets and binaries. Sensitive files are handled separately (see
@@ -270,14 +284,17 @@ snapshot the marker describes.
270
284
  export default {
271
285
  hooks: {
272
286
  afterGenerate: (manifest) => {
273
- // runs after a successful generate (and after a dry run)
287
+ // runs after a real generate; a dry run does not call it
274
288
  },
275
289
  },
276
290
  };
277
291
  ```
278
292
 
279
293
  A throwing hook is downgraded to a warning on the manifest — it never loses a snapshot that
280
- was already written.
294
+ was already written. A hook may return a promise: it is not awaited (this API is synchronous),
295
+ and a rejection is recorded as a warning on the re-shipped manifest and marker. Anything the
296
+ hook writes to stdout is redirected to stderr for the duration of the call, so `--format json`
297
+ stays parseable.
281
298
 
282
299
  A hook may write its own files into the output directory; that is safe across reruns because
283
300
  ownership is established by the marker rather than by the filenames present.
@@ -311,6 +328,7 @@ export default {
311
328
  // ── Scope ────────────────────────────────────────────────
312
329
  include: [],
313
330
  excludePatterns: [],
331
+ discovery: "auto", // "auto" | "git" | "walk"
314
332
 
315
333
  // ── Artifacts ────────────────────────────────────────────
316
334
  artifacts: {
@@ -364,5 +382,12 @@ npx codebase-snapshot init --preset go --max-part-bytes 200k \
364
382
  ```
365
383
 
366
384
  `--minimal` writes only what differs from the defaults. `--dry-run` prints the config and
367
- writes nothing. `--force` overwrites an existing config; without it, an existing config is
368
- left alone and the command exits `1`.
385
+ writes nothing. `--force` replaces an existing ES-module config (`.ts`, `.js`, `.mjs`) in
386
+ place; without it, an existing config is left alone and the command exits `1`.
387
+
388
+ It will not replace a JSON-family config — `snapshot.config.json`, `.snapshotrc`,
389
+ `.snapshotrc.json`, or the `"snapshot"` key in `package.json` — because this wizard writes an
390
+ ES-module file and a new file beside the old one would silently shadow it. Those cases exit `1`
391
+ with a message naming the file to delete or rename. Note that this means `--force` writing
392
+ `snapshot.config.ts` does shadow a `"snapshot"` key in `package.json`, since a dedicated config
393
+ file always wins.
package/docs/install.md CHANGED
@@ -53,13 +53,15 @@ most reproducible option:
53
53
  ```json
54
54
  {
55
55
  "scripts": {
56
- "snapshot": "codebase-snapshot generate",
57
- "snapshot:validate": "codebase-snapshot validate"
56
+ "snapshot": "npx @ankurhalder/codebase-snapshot generate",
57
+ "snapshot:validate": "npx @ankurhalder/codebase-snapshot validate"
58
58
  }
59
59
  }
60
60
  ```
61
61
 
62
- `codebase-snapshot init` writes these entries for you.
62
+ `codebase-snapshot init` writes these entries for you, in exactly this `npx` form: the bare
63
+ `codebase-snapshot` command only exists on `PATH` inside a client runner or a script that has
64
+ already resolved the package, and `init` does not assume the install has happened yet.
63
65
 
64
66
  ## One-off runners for other clients
65
67
 
package/docs/output.md CHANGED
@@ -129,9 +129,18 @@ values — the hash is the whole 64-character digest, never elided.
129
129
  - Only `isPreviewTruncated` tells you content is missing; `isLarge` alone does not.
130
130
  - `hashSha256` is the hash of the source file on disk — `validate` recomputes it to detect
131
131
  drift. It is the full 64-character digest.
132
+ - `encoding` is present **only** when the file is not plain UTF-8; its absence means clean UTF-8.
133
+ `utf-16le` and `utf-16be` mean the file was transcoded to UTF-8 before it was emitted, so a
134
+ UTF-16 source file is readable in the parts instead of vanishing as "binary". `utf-8-lossy`
135
+ means the file is not valid UTF-8 at all: its undecodable byte spans became `U+FFFD`, the count
136
+ is reported in `warnings`, and the file is kept rather than dropped. The hash still covers the
137
+ raw bytes, so a lossy file validates as current — `encoding` is how you learn that the body you
138
+ are reading is not byte-exact.
132
139
  - `summary` and `summaryQuality` are **conditional**. Both keys are present for a language
133
140
  with an analyser and both are absent for a shapeless one — a `.gitignore`, a `.txt`. Treat a
134
- missing `summary` as "no structural information", not as an empty one.
141
+ missing `summary` as "no structural information", not as an empty one. A file larger than
142
+ 2 MB gets no summary at all: parsing it costs more memory than the file itself, and its body is
143
+ a head/tail preview at that size anyway. `manifest.warnings` names those files.
135
144
  - `summary` has one of three shapes. Code files get the thirteen keys above (`directives`,
136
145
  `imports`, `exports`, `reExports`, `functions`, `classes`, `interfaces`, `types`, `enums`,
137
146
  `variables`, `reactComponents`, `hooks`, `routeHandlers`), plus `parseErrors` when the parser
@@ -160,12 +169,13 @@ Reasons, and what each one means:
160
169
  | `default-ignore` | A built-in rule caught it, the preset's ecosystem ignores did (`node_modules`, `__pycache__`, `target`, …), or it sat inside an output directory. |
161
170
  | `sensitive-file` | Its name is credential-bearing (`.env`, `*.pem`, `.npmrc`, …). |
162
171
  | `sensitive-path` | The path itself matched a secret rule. The file is skipped and the path is masked in every artifact. |
163
- | `binary` | It is not text. |
172
+ | `binary` | It is not text: it holds a NUL byte outside a UTF-16 stream, or more than 1% control characters (a 4 KB blob of `0x07` is valid UTF-8 but is not text). `detail` carries the size and, when there is one to give, the reason: the control-byte count (`Size: 13 B; 5 control byte(s)`), or the offset of a NUL found past the 8 MB head scan (`Size: 9000010 B; NUL byte at offset 9000000`). A NUL inside the head scan is caught by the cheap extension-and-prefix check, so its `detail` is the size alone. |
164
173
  | `symlink` | It is a link, and links are not followed. A directory link names its target in `detail`. |
165
174
  | `outside-root` | It is a link whose target resolves outside the scanned root. |
166
175
  | `gitignore` | A `.gitignore` excluded it, in a scan that is not reading git's index. |
167
176
  | `read-error` | It could not be read. |
168
- | `not-file` | It is a directory, socket or FIFO. |
177
+ | `not-file` | It is a directory, socket or FIFO. A directory that belongs to another repository is reported as `nested-repository` instead. |
178
+ | `nested-repository` | It is a nested checkout or a submodule: a directory whose contents are tracked by a different repository, so they are not part of this codebase. |
169
179
 
170
180
  Every path the scanner discovers is recorded — nothing is dropped silently, in either discovery
171
181
  mode. Inside a git repository the candidate list comes from git, which applies `.gitignore`
@@ -191,14 +201,17 @@ example). See [Troubleshooting](troubleshooting.md#a-file-is-missing-from-the-sn
191
201
  replacement is complete.
192
202
  - **The tool refuses to replace a directory it did not write.** Pointing `--out-dir` at
193
203
  `src/` is refused with exit `1` and the directory is left untouched — a mistyped output
194
- path cannot delete source code. Empty directories, directories containing only
195
- `.gitkeep`/`.gitignore`/`.DS_Store`, and directories holding a complete snapshot this build
196
- wrote are accepted.
204
+ path cannot delete source code. Empty directories, directories that hold nothing but
205
+ `.DS_Store`, and directories holding a complete snapshot this build wrote are accepted.
206
+ A `.gitkeep` or `.gitignore` inside the directory is not accepted, because the generator never
207
+ writes either into an output directory: a directory that holds one is somebody's data, so the
208
+ run refuses instead of deleting it.
197
209
  - **Ownership is the marker, not a filename.** `.codebase-snapshot` is a JSON record carrying
198
210
  this generator's name and `version`, the marker's `schemaVersion`, a `createdAt` timestamp,
199
- a per-snapshot `id`, the run `state` (`staging` while it is being written, `complete` once it
200
- is whole), the `manifestSha256` digest of the manifest it shipped, and `entries` — the
201
- inventory of everything it wrote. A zero-byte marker, a marker that is not JSON, a marker
211
+ a `pid`, a per-snapshot `id`, the run `state` (`staging` while it is being written, `complete`
212
+ once it is whole), the `manifestSha256` digest of the manifest it shipped, and `entries` — the
213
+ inventory of everything it wrote. `pid` is what lets a later run tell a crashed run's staging
214
+ directory and lock from a live one. A zero-byte marker, a marker that is not JSON, a marker
202
215
  written by 3.0.0 (no `id`), a bare `manifest.json`, and a directory with files you added
203
216
  after the run are all refused.
204
217
  See [Troubleshooting](troubleshooting.md#refusing-to-replace-outdir-).
@@ -83,8 +83,13 @@ and the digest of the manifest it shipped. A file with the right name is not eno
83
83
  | carries our marker but you added files | `it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: custom.txt. Remove them yourself, or point outDir at a new or empty directory.` |
84
84
 
85
85
  **Fix** — point `--out-dir` somewhere new, or delete the directory yourself. Do not delete
86
- your source. Accepted without complaint: a missing directory, an empty directory, one
87
- holding only `.gitkeep`/`.gitignore`/`.DS_Store`, and a complete snapshot this build wrote.
86
+ your source. Accepted without complaint: a missing directory, an empty directory, one holding
87
+ nothing but `.DS_Store`, and a complete snapshot this build wrote.
88
+
89
+ `.gitkeep` and `.gitignore` are **not** accepted, even in a directory that holds nothing else.
90
+ Both are files you create — the generator never writes either into an output directory — so a
91
+ directory holding one is somebody's data and is refused rather than replaced. Move the file out,
92
+ or point `--out-dir` somewhere new.
88
93
 
89
94
  A snapshot written by 3.0.0 or earlier is **not** upgraded in place: its marker has three keys
90
95
  and no `id`, so the new build cannot prove it wrote the directory and refuses. Delete the old
@@ -113,6 +118,44 @@ settings and still report success.
113
118
  loadable on your Node version: a `.ts` config needs Node ≥ 22.6 **and** an ES-module project.
114
119
  `codebase-snapshot init` picks the right filename for the project automatically.
115
120
 
121
+ ### `This directory is a git repository, but git could not list its files.`
122
+
123
+ ```
124
+ ❌ Failed to generate snapshot: This directory is a git repository, but git could not list its files. A plain walk honours .gitignore files, but git also applies .git/info/exclude and your global excludes file, includes files that are tracked although a .gitignore now matches them, and treats a submodule as a single entry. Fix the repository (git status should work), run outside git, or pass --discovery walk to accept those differences.
125
+ ```
126
+
127
+ `git` is present and a `.git` directory exists, but it cannot list the files — an empty or
128
+ half-initialised `.git`, a permissions problem, or a damaged index. The tool refuses rather
129
+ than silently choosing between two discovery methods: which files are in the snapshot would
130
+ otherwise depend on whether git happened to work.
131
+
132
+ **Fix** — make `git status` work in that directory (repair or re-clone the repository), generate
133
+ from a copy that has no `.git` directory, or pass `--discovery walk` to scan the tree anyway.
134
+ The walk reads every `.gitignore` it meets, nested ones included, and still skips credential
135
+ files; what it cannot reproduce is git's own ignore sources (`.git/info/exclude` and your global
136
+ `core.excludesFile`), git's inclusion of files that are tracked although a `.gitignore` now
137
+ matches them, and its treatment of a submodule as one entry instead of a tree.
138
+
139
+ `--discovery git` says the opposite — require git — so it refuses in a directory that has no
140
+ usable repository:
141
+
142
+ ```
143
+ ❌ Failed to generate snapshot: Discovery mode is `git`, but git could not list this directory's files. Run inside a working repository, or pass --discovery walk (or auto) to scan the tree directly.
144
+ ```
145
+
146
+ ### `Another generate run is already using the staging directory …`
147
+
148
+ ```
149
+ ❌ Failed to generate snapshot: Another generate run is already using the staging directory /repo/snapshot.tmp. Wait for it to finish, or delete /repo/codebase-snapshot-1a2b3c4d5e6f7890.lock if no other run is active.
150
+ ```
151
+
152
+ A second `generate` against the same repository started while the first still held the staging
153
+ directory — two terminals, or a CI job and a developer at once. The lock is advisory and on
154
+ disk, so a run that was killed cannot release it.
155
+
156
+ **Fix** — wait for the other run, then retry. If no other run is active, the named lock file is
157
+ stale: delete it, and the next run proceeds.
158
+
116
159
  ### `Refusing to generate: …`
117
160
 
118
161
  ```
@@ -195,7 +238,7 @@ npx codebase-snapshot explain src/generated/schema.ts
195
238
  | :--- | :--- | :--- |
196
239
  | `sensitive-file` | `.env`, `*.pem`, `.npmrc`, `.ssh/`, `.tfstate`, … | `--include-sensitive-files` if you really want it. |
197
240
  | `sensitive-path` | The path itself matched a secret rule, so the file is skipped and its path is masked in every artifact. | Rename the file; a path that looks like a key is treated as one. |
198
- | `binary` | Not text. | Nothing to do. |
241
+ | `binary` | Not text: it contains a NUL byte outside a UTF-16 stream, or is more than 1% control characters. The `detail` names the file size and the reason — including the offset of the first NUL when that byte sits past the 8 MB head scan. | Nothing to do. A UTF-16 file is *not* binary — it is transcoded and reported with `encoding`, not skipped. |
199
242
  | `default-ignore` | A built-in rule, the preset's ecosystem ignores (`node_modules`, `__pycache__`, `target`, `vendor`, …), or the output directory. | A preset's ignores are not configurable; a custom `excludePatterns` can only remove more. |
200
243
  | `excluded` | Your own `excludePatterns`, or a path outside a non-empty `include` allow-list. | Remove the pattern, or widen `include`. |
201
244
  | `symlink` | Links are not followed (a directory link names its target in `detail`). | Snapshot the target. |
@@ -203,6 +246,7 @@ npx codebase-snapshot explain src/generated/schema.ts
203
246
  | `gitignore` | A `.gitignore` excluded it, in a scan that is not reading git's index. | Remove the entry, or add the path to `include`. |
204
247
  | `read-error` | Unreadable file. | Check permissions. |
205
248
  | `not-file` | A directory, socket, FIFO. | Expected. |
249
+ | `nested-repository` | A nested checkout or submodule — a directory tracked by a different repository. | Expected; snapshot that repository separately if you need its code. |
206
250
 
207
251
  Inside a git repository the candidate list comes from git, which applies `.gitignore` before
208
252
  the scanner runs; those ignored paths are listed separately and recorded too, so a `.gitignore`d
@@ -238,6 +282,19 @@ under a `- Warnings: N` line in the summary, and none of them stops a run:
238
282
  - `every artifact is disabled: only the .codebase-snapshot ownership marker will be written.`
239
283
  - A `hooks.afterGenerate` error is downgraded to a warning: the snapshot it was called for is
240
284
  already written and is not lost.
285
+ - `N file(s) exceed 2 MB and were not parsed for a structural summary: …` — the named files are
286
+ emitted as head/tail previews without an `imports`/`exports` summary. Parsing them costs several
287
+ times their own size in memory, which is how a single 50 MB source file used to reach 3.3 GB of
288
+ resident memory. Nothing to fix; raise the part budget if the preview is too small.
289
+ - `N file(s) are UTF-16LE and were transcoded to UTF-8: …` — those files were UTF-16 and are
290
+ now ordinary text in the snapshot. They are listed in `manifest.files[]` with
291
+ `encoding: "utf-16le"`, or `"utf-16be"`. Nothing to fix; the warning exists because the bytes
292
+ in the artifact are not the bytes on disk.
293
+ - `N file(s) are not valid UTF-8; M undecodable byte(s) became U+FFFD: …` — the named files are
294
+ not UTF-8 at all (Latin-1 is the usual cause). They are kept, with `encoding: "utf-8-lossy"`,
295
+ but the undecodable spans are substituted, so **do not hand the emitted body to a model as if
296
+ it were the source**. Convert the file to UTF-8 (`iconv -f WINDOWS-1252 -t UTF-8 file > file.tmp`)
297
+ and regenerate. The snapshot still validates as current, because the hash covers the raw bytes.
241
298
 
242
299
  `validate` produces its own warnings, printed with a `⚠ ` prefix and never written to the
243
300
  manifest, because they describe the state of the repository rather than the snapshot:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankurhalder/codebase-snapshot",
3
- "version": "4.0.6",
3
+ "version": "4.1.0",
4
4
  "description": "Production-grade LLM codebase snapshot generator, auditor, and AST partitioner",
5
5
  "keywords": [
6
6
  "codebase",