@ankurhalder/codebase-snapshot 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/install.md CHANGED
@@ -1,42 +1,33 @@
1
1
  # Installation
2
2
 
3
- `@ankurhalder/codebase-snapshot` is published to the public npm registry, so it installs
4
- with **any** npm-compatible client. There is nothing extra to configure and no second
5
- registry to add.
3
+ `@ankurhalder/codebase-snapshot` is published to the public npm registry, so it installs with
4
+ **any** npm-compatible client. There is no second registry to add and nothing to configure.
6
5
 
7
6
  Requires **Node.js 18 or newer**.
8
7
 
9
- ## One-off use (no install)
8
+ ## One-off use
10
9
 
11
10
  ```bash
12
11
  npx @ankurhalder/codebase-snapshot generate
13
12
  ```
14
13
 
14
+ This is the shortest path and needs no install. The command is also available as
15
+ `codebase-snapshot` and, once installed, as the short alias `snap`.
16
+
15
17
  Pin the version when you need the same build on every machine:
16
18
 
17
19
  ```bash
18
20
  npx @ankurhalder/codebase-snapshot@latest generate
19
- npx @ankurhalder/codebase-snapshot@3 generate
21
+ npx @ankurhalder/codebase-snapshot@4 generate
20
22
  ```
21
23
 
22
- The bare form works because this package ships **exactly one executable**
23
- (`codebase-snapshot`). That is deliberate: npm resolves which binary to run by matching the
24
- typed command against the package's `bin` keys, and no key can contain an `@scope/`, so a
25
- scoped package with several executables cannot be run by its bare name. If a command ever
26
- exits with no output, clear the npm cache and retry — see troubleshooting.
27
-
28
- `npx -p @ankurhalder/codebase-snapshot codebase-snapshot generate`, `pnpm dlx`,
29
- `yarn dlx` and `bunx` are equivalent. A normal `npm install -D` followed by
30
- `npx codebase-snapshot` is the more reproducible choice for a project.
24
+ **Why the bare name works.** npm resolves which binary to run from the package's `bin`
25
+ entries. This package declares `codebase-snapshot` and `snap` pointing at the *same file*, so
26
+ npm treats them as aliases and can always resolve one. Keep them aliases: two *different*
27
+ executables under a scoped package name would make the bare form unresolvable.
31
28
 
32
- Equivalent one-liners for other clients:
33
-
34
- ```bash
35
- yarn dlx -p @ankurhalder/codebase-snapshot codebase-snapshot generate # yarn Berry
36
- yarn global add @ankurhalder/codebase-snapshot # yarn classic (installs globally)
37
- pnpm dlx @ankurhalder/codebase-snapshot generate
38
- bunx @ankurhalder/codebase-snapshot generate
39
- ```
29
+ If a command exits with no output at all, a stale global install is the usual cause — see
30
+ [Troubleshooting](#troubleshooting).
40
31
 
41
32
  ## Add to a project
42
33
 
@@ -47,18 +38,17 @@ pnpm add -D @ankurhalder/codebase-snapshot
47
38
  bun add -d @ankurhalder/codebase-snapshot
48
39
  ```
49
40
 
50
- Then run it through your client so it resolves from the project's `node_modules`:
51
-
52
- ```bash
53
- npx codebase-snapshot generate
54
- yarn codebase-snapshot generate
55
- pnpm exec codebase-snapshot generate
56
- bun run codebase-snapshot generate
57
- ```
41
+ Then run it through your client so the binary resolves from the project:
58
42
 
59
- Both `codebase-snapshot` and `codebase-snapshot` are exposed as binaries — pick whichever you prefer.
43
+ | Client | Run |
44
+ | :--- | :--- |
45
+ | npm | `npx codebase-snapshot generate` |
46
+ | yarn (classic and Berry) | `yarn codebase-snapshot generate` |
47
+ | pnpm | `pnpm exec codebase-snapshot generate` |
48
+ | bun | `bun run codebase-snapshot generate` |
60
49
 
61
- Adding it to `package.json` scripts works the same everywhere:
50
+ `snap` works everywhere `codebase-snapshot` does. Adding it to `package.json` scripts is the
51
+ most reproducible option:
62
52
 
63
53
  ```json
64
54
  {
@@ -69,85 +59,123 @@ Adding it to `package.json` scripts works the same everywhere:
69
59
  }
70
60
  ```
71
61
 
72
- ## Global install
62
+ `codebase-snapshot init` writes these entries for you.
63
+
64
+ ## One-off runners for other clients
73
65
 
74
66
  ```bash
75
- npm install -g @ankurhalder/codebase-snapshot
76
- pnpm add -g @ankurhalder/codebase-snapshot
77
- bun add -g @ankurhalder/codebase-snapshot
67
+ yarn dlx -p @ankurhalder/codebase-snapshot codebase-snapshot generate # yarn Berry
68
+ pnpm dlx @ankurhalder/codebase-snapshot generate
69
+ bunx @ankurhalder/codebase-snapshot generate
78
70
  ```
79
71
 
80
- A global install exists for convenience, but pinning the version per project (the
81
- `-D`/`--save-dev` examples above) is the better default: snapshots are only reproducible
82
- if the generator version is reproducible.
72
+ ## Agent skill
83
73
 
84
- ## Version pinning
74
+ To make a repository discoverable by an AI agent — so it finds this tool without being told
75
+ it exists — run:
85
76
 
86
77
  ```bash
87
- npx @ankurhalder/codebase-snapshot@2 generate
88
- pnpm dlx @ankurhalder/codebase-snapshot@3.0.0 generate
78
+ npx @ankurhalder/codebase-snapshot skill --install
89
79
  ```
90
80
 
81
+ That writes an `AGENTS.md` section, `.claude/skills/codebase-snapshot/SKILL.md` and
82
+ `.cursor/rules/codebase-snapshot.mdc`. Re-running only replaces its own marked block. See
83
+ [Using this with an AI agent](agent.md).
84
+
91
85
  ## Verified clients
92
86
 
93
- Every client below was used to install the published package and run
94
- `codebase-snapshot --version` successfully. This matrix is enforced in CI
95
- (`.github/workflows/ci.yml` → `client-install`), so a change that breaks a client fails
96
- the build rather than a user's terminal.
87
+ Every client below installs the published package and runs it successfully; the release check
88
+ enforces this on every pull request, so a change that breaks one fails the build rather than
89
+ a user's terminal.
97
90
 
98
- | Client | Version tested | Run installed binary with |
99
- | :--- | :--- | :--- |
100
- | npm | 10.x | `npx codebase-snapshot` |
101
- | yarn (classic) | 1.22.22 | `yarn codebase-snapshot` |
102
- | yarn (Berry, `nodeLinker: pnp`) | 4.6.0 | `yarn codebase-snapshot` |
103
- | pnpm | 10.28.0 | `pnpm exec codebase-snapshot` |
104
- | bun | 1.4.4 | `bun run codebase-snapshot` |
91
+ | Client | Run installed binary with |
92
+ | :--- | :--- |
93
+ | npm 10–11 | `npx codebase-snapshot` |
94
+ | yarn classic 1.22 | `yarn codebase-snapshot` |
95
+ | yarn Berry 4.6 (`nodeLinker: pnp`) | `yarn codebase-snapshot` |
96
+ | pnpm 10 | `pnpm exec codebase-snapshot` |
97
+ | bun 1.4 | `bun run codebase-snapshot` |
105
98
 
106
- Yarn Berry's default **strict PnP** mode works: there is no `node_modules` directory, and
107
- the runtime `typescript` dependency still resolves through yarn's `.pnp.cjs` loader.
99
+ Yarn Berry's default **strict PnP** works: there is no `node_modules` directory, and the
100
+ runtime `typescript` dependency still resolves through yarn's `.pnp.cjs` loader.
108
101
 
109
- ## Package manager workspaces
102
+ ## Before scanning a repository you do not own
110
103
 
111
- The binary is resolved from the workspace it is installed in. In a monorepo, install it
112
- in the package that runs the snapshot (usually the root), or use any client's one-off
113
- runner (`npx` / `pnpm dlx` / `yarn dlx` / `bunx`) which works from any subdirectory.
104
+ A `snapshot.config.ts`, `.js`, `.mjs` or `.cjs` in the target repository is **imported and
105
+ executed** when the tool runs there — that is what lets a config express a `RegExp` or a
106
+ `match` function. Pointing `--cwd` at a checkout you do not trust therefore runs its code.
107
+ `--no-config` ignores every config file and uses the built-in defaults plus the detected
108
+ preset, and JSON configs are parsed rather than executed. See
109
+ [the trust boundary](../SECURITY.md#trust-boundary).
114
110
 
115
- ## Uninstalling
111
+ ## Global install
116
112
 
117
- Standard for your client (`npm uninstall`, `yarn remove`, `pnpm remove`, `bun remove`).
118
- Snapshots already written to `snapshot/` are ordinary files and are left alone.
113
+ ```bash
114
+ npm install -g @ankurhalder/codebase-snapshot
115
+ ```
119
116
 
120
- ## Not supported yet
117
+ Convenient, but prefer pinning per project: snapshots are only reproducible if the generator
118
+ version is. A global install also shadows `npx` for this package — npm will run your global
119
+ copy instead of the published one, which is confusing when the two differ. That is exactly
120
+ the situation described under [Troubleshooting](#troubleshooting).
121
121
 
122
- - **`pip`, `brew`, `scoop`, `docker`, and standalone binaries.** These require shipping a
123
- prebuilt executable rather than JavaScript, and are tracked as future work in
124
- [docs/plans/distribution](../plans/distribution/README.md). Until then, any machine
125
- running the tool needs Node 18+.
126
- - **Deno.** The API can be imported under Deno's npm compatibility, but running the CLI
127
- needs a `./bin` subpath export that is not published yet.
122
+ ## Workspaces
128
123
 
129
- ## Troubleshooting
124
+ The binary resolves from the workspace it is installed in. In a monorepo, install it in the
125
+ package that runs the snapshot (usually the root), or use a one-off runner
126
+ (`npx` / `pnpm dlx` / `yarn dlx` / `bunx`) which works from any subdirectory.
130
127
 
131
- **`codebase-snapshot: command not found`** — when installed locally (not globally), the binary lives
132
- in `node_modules/.bin` and is only on your `PATH` inside a package script or a client
133
- runner. Use `npx codebase-snapshot`, `pnpm exec codebase-snapshot`, `yarn codebase-snapshot`, or `bun run codebase-snapshot`.
128
+ ## Upgrading
134
129
 
135
- **`Cannot find module 'typescript'`** — the package declares `typescript` as a runtime
136
- dependency for AST analysis. If your client prunes or externalizes it (some bundlers and
137
- `--ignore-scripts` CI setups do), rerun a clean install, or pin `typescript@^5` yourself.
130
+ ```bash
131
+ npm install -D @ankurhalder/codebase-snapshot@latest
132
+ npx codebase-snapshot generate # rewrite the snapshot with the new version
133
+ ```
134
+
135
+ `manifest.generator.version` records which version produced a snapshot. Re-run `generate`
136
+ after an upgrade so the snapshot, and the version it records, match the build you are now
137
+ running.
138
+
139
+ A snapshot written by **3.0.0 or earlier** is refused rather than replaced: its
140
+ `.codebase-snapshot` marker has no per-snapshot id, so the current build cannot prove it wrote
141
+ the directory. Delete the old snapshot directory and generate again.
142
+
143
+ See [CHANGELOG](../CHANGELOG.md) for breaking changes — 3.0.0 removed nothing, but it briefly
144
+ did in error; 4.0.0 restored the `snap` alias.
138
145
 
139
- **CI has no Node** — that is not supported yet; this package is JavaScript and needs a
140
- Node runtime. See the future-work note above.
146
+ ## Uninstalling
147
+
148
+ Standard for your client (`npm uninstall`, `yarn remove`, `pnpm remove`, `bun remove`).
149
+ Snapshots already written to the output directory are ordinary files and are left alone.
150
+ If you installed globally, `npm rm -g @ankurhalder/codebase-snapshot` also removes the
151
+ shim that shadows `npx`.
152
+
153
+ ## Troubleshooting
141
154
 
142
- **`yarn codebase-snapshot` prints extra lines, or breaks a script that parses output** — yarn classic
143
- wraps everything: it echoes `yarn run 1.22.22`, the resolved command, your tool's output,
144
- and a `Done in 0.37s.` trailer. For a human that is harmless; for anything that parses
145
- `codebase-snapshot`'s stdout it is not. In scripts and CI, call the binary directly:
155
+ **`codebase-snapshot: command not found`** — when installed locally (not globally) the binary
156
+ lives in `node_modules/.bin` and is only on `PATH` inside a package script or a client
157
+ runner. Use `npx codebase-snapshot`, `pnpm exec codebase-snapshot`,
158
+ `yarn codebase-snapshot` or `bun run codebase-snapshot`.
159
+
160
+ **A command exits 0 and prints nothing** — almost always a stale **global** install
161
+ shadowing `npx`. Check and remove it:
146
162
 
147
163
  ```bash
148
- ./node_modules/.bin/codebase-snapshot generate
149
- npx codebase-snapshot generate # same, without the yarn wrapper
164
+ npm ls -g --depth=0 | grep codebase-snapshot
165
+ npm rm -g @ankurhalder/codebase-snapshot
150
166
  ```
151
167
 
152
- This is why the CI job in this repository reads the version from a line that *is* the
153
- version rather than taking the first or last line of output.
168
+ Then re-run. If the package is not installed globally, clear the npx cache
169
+ (`npm cache clean --force`) and retry.
170
+
171
+ **`Cannot find module 'typescript'`** — the package declares `typescript` as a runtime
172
+ dependency for AST analysis. If your client prunes it (some `--ignore-scripts` CI setups do),
173
+ rerun a clean install or pin `typescript@^5` yourself.
174
+
175
+ **`yarn codebase-snapshot` prints extra lines / breaks a script** — yarn classic wraps
176
+ everything: it echoes `yarn run 1.22.x`, the resolved command, your output, then
177
+ `Done in 0.37s.`. For anything that parses the output, call the binary directly
178
+ (`./node_modules/.bin/codebase-snapshot` or `npx codebase-snapshot`).
179
+
180
+ More symptoms — refusals, config errors, drift, missing files — are in
181
+ [Troubleshooting](troubleshooting.md).
package/docs/output.md ADDED
@@ -0,0 +1,213 @@
1
+ # Snapshot output
2
+
3
+ A run writes one directory (default `./snapshot`) that is meant to be handed to a model as
4
+ context. It is self-describing: `manifest.json` indexes everything the parts contain and
5
+ explains everything they omit.
6
+
7
+ ## Layout
8
+
9
+ ```
10
+ snapshot/
11
+ ├── .codebase-snapshot ownership marker (see Safety)
12
+ ├── README.md human summary of this snapshot
13
+ ├── manifest.json machine index: files, parts, config, hashes, skips
14
+ ├── parts/
15
+ │ ├── part-001.md the code, grouped and size-budgeted
16
+ │ ├── part-002.md
17
+ │ └── …
18
+ └── reports/
19
+ ├── project-tree.md directory tree of included files
20
+ ├── navigation.md links between layers, kinds and parts
21
+ └── audit.md secrets, large files, parse diagnostics, git state
22
+ ```
23
+
24
+ With `--include-monolith` you also get `full-codebase.json` and `full-codebase.md` — the
25
+ whole repository in one file. They are off by default because they grow without bound;
26
+ `--max-monolith-bytes` skips them when they would exceed a size you set.
27
+
28
+ Every artifact can be turned off. `--skip-artifacts parts` omits the parts,
29
+ `--only-artifacts manifest` writes nothing but the manifest, and `config.artifacts` does the
30
+ same persistently. The ownership marker is always written.
31
+
32
+ ## Parts
33
+
34
+ Each `part-NNN.md` holds whole files under the byte budget (`maxPartBytes`, default 300 KB).
35
+ A file larger than the budget is not split mid-file: it is emitted as a head/tail preview and
36
+ marked as truncated. Each file section is tagged with its layer, kind, language and a
37
+ structural summary (imports, exports, classes, functions) so a reader can navigate without
38
+ opening every file.
39
+
40
+ Parts are packed in a stable order, so the same input produces the same output — which is
41
+ what makes `manifest.fingerprint` meaningful.
42
+
43
+ ## Manifest
44
+
45
+ `manifest.json` is the index and the contract between runs. Top-level keys:
46
+
47
+ | Key | Contents |
48
+ | :--- | :--- |
49
+ | `schemaVersion` | Manifest format version (`3.0.0`). |
50
+ | `generatedAt` | ISO timestamp of the run. |
51
+ | `generator` | `{ name, version }` of the tool that wrote it. |
52
+ | `git` | `{ branch, commit, isDirty }`. `branch` falls back to the configured branch on a detached HEAD and outside a repository; `commit` is `"unknown"` and `isDirty` is `false` when git cannot report. A `false` there means "not known to be dirty", not "clean tree" — check `commit` to tell the two apart. |
53
+ | `budgets` | The effective byte limits for the run: `{ maxPartBytes, largeFileBytes }`. The per-layer overrides live at `config.budgets`, which is `{ default, byLayer }`. |
54
+ | `config` | The resolved configuration actually used (preset, artifacts, sizing, scoping). |
55
+ | `counts` | `{ totalFiles, totalSizeBytes, totalLines, partsCount, skippedCount, largeFileCount, truncatedPreviewCount }`. |
56
+ | `totals` | Bookkeeping: `{ candidateFiles, includedFiles, fullFiles, previewFiles, skippedFiles, totalSourceBytes, totalContentBytes, partCount, warningCount }`. |
57
+ | `fingerprint` | Stable hash of the snapshot content. Two runs over unchanged input produce the same value. |
58
+ | `validation` | `{ status: "passed" \| "failed", errors: [] }` for the run's own integrity check. |
59
+ | `artifacts` | The optional monolith files: `{ fullJson, fullMarkdown }`. Each value is `null` when that file was not written, otherwise `{ byteSize, sha256 }`. The object itself is always present. |
60
+ | `layers`, `kinds` | File counts per layer and per kind. |
61
+ | `indexes` | Cross-references: `routes`, `components`, `handlers`, `features`, `imports`, `directives`. |
62
+ | `parts` | One entry per part (below). |
63
+ | `files` | One entry per included file (below). |
64
+ | `skipped` | One entry per excluded file (below). |
65
+ | `warnings` | Non-fatal observations, including a throwing `hooks.afterGenerate`. |
66
+
67
+ ### `parts[]`
68
+
69
+ ```json
70
+ {
71
+ "index": 1,
72
+ "filename": "part-001.md",
73
+ "byteSize": 1432,
74
+ "sha256": "3472b9edcd504403f2416606e4222f326ec207d7916bf928ade4ec7de1406d2d",
75
+ "lines": 91,
76
+ "fileCount": 2,
77
+ "layersCovered": ["root", "src"],
78
+ "includedFiles": [
79
+ { "relativePath": "package.json", "sourceBytes": 35, "includedBytes": 35, "lines": 1, "layer": "root", "kind": "data" },
80
+ { "relativePath": "src/math.ts", "sourceBytes": 63, "includedBytes": 63, "lines": 3, "layer": "src", "kind": "other" }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ `byteSize` and `sha256` are what `validate` checks against the file on disk. Both are full
86
+ values — the hash is the whole 64-character digest, never elided.
87
+
88
+ ### `files[]`
89
+
90
+ ```json
91
+ {
92
+ "relativePath": "src/math.ts",
93
+ "sizeBytes": 63,
94
+ "lines": 3,
95
+ "hashSha256": "aa229260647049a42a0446f48f1b34f19657921b7a0a27a41c0b2a294a25c919",
96
+ "part": "part-001.md",
97
+ "layer": "src",
98
+ "kind": "other",
99
+ "language": "typescript",
100
+ "inclusionMode": "full",
101
+ "isLarge": false,
102
+ "isPreviewTruncated": false,
103
+ "originalLines": 3,
104
+ "includedLines": 3,
105
+ "includedBytes": 63,
106
+ "summary": {
107
+ "directives": [], "imports": [], "exports": ["add"], "reExports": [],
108
+ "functions": ["add"], "classes": [], "interfaces": [], "types": [],
109
+ "enums": [], "variables": [], "reactComponents": [], "hooks": [],
110
+ "routeHandlers": []
111
+ },
112
+ "summaryQuality": "ast"
113
+ }
114
+ ```
115
+
116
+ - `isLarge` is a fact about the file: it is bigger than `largeFileBytes` (default 200 KB).
117
+ A large file is emitted as a head/tail excerpt rather than inlined whole.
118
+ - `isPreviewTruncated` is a fact about what was written: the body in this snapshot is **not**
119
+ the whole file, because lines were dropped.
120
+ - `inclusionMode` is `preview` exactly when `isPreviewTruncated` is `true`, and there are two
121
+ routes to it:
122
+ - **Over `largeFileBytes`** — the head/tail excerpt drops the middle, so
123
+ `isLarge: true`, `isPreviewTruncated: true`. If the head and tail happen to cover every
124
+ line, nothing is dropped and the entry stays `isLarge: true`, `inclusionMode: "full"`.
125
+ - **Under `largeFileBytes` but too big for its part** — the partitioner byte-truncates the
126
+ section to keep the part inside its budget, so `isLarge: false`,
127
+ `isPreviewTruncated: true`. This is the one that surprises people: the file is not "large"
128
+ by the configured threshold, yet its body is incomplete.
129
+ - Only `isPreviewTruncated` tells you content is missing; `isLarge` alone does not.
130
+ - `hashSha256` is the hash of the source file on disk — `validate` recomputes it to detect
131
+ drift. It is the full 64-character digest.
132
+ - `summary` and `summaryQuality` are **conditional**. Both keys are present for a language
133
+ 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.
135
+ - `summary` has one of three shapes. Code files get the thirteen keys above (`directives`,
136
+ `imports`, `exports`, `reExports`, `functions`, `classes`, `interfaces`, `types`, `enums`,
137
+ `variables`, `reactComponents`, `hooks`, `routeHandlers`), plus `parseErrors` when the parser
138
+ reported syntax errors. JSON gets `{ shape, itemCount, keys, sampleKeys }` and Markdown gets
139
+ `{ headings, frontmatterKeys }` — a shape description rather than a code summary.
140
+ - `summaryOmitted: true` means the file's summary was dropped because the summary alone would
141
+ not fit the part budget. The entry then has `summaryQuality` but no `summary`, and the
142
+ file's body is still emitted in full. It is only ever present when `true`.
143
+ - `summaryQuality` is `ast` (parsed by the TypeScript compiler), `structural` (JSON or
144
+ Markdown shape) or `heuristic` (pattern-matched). It tells you how much to trust the
145
+ summary: `heuristic` means a line scan, not a parse.
146
+
147
+ Querying this array is usually faster and cheaper than re-reading the parts.
148
+
149
+ ### `skipped[]`
150
+
151
+ ```json
152
+ { "path": ".env", "reason": "sensitive-file", "detail": "credential-bearing file; enable includeSensitiveFiles to emit it" }
153
+ ```
154
+
155
+ Reasons, and what each one means:
156
+
157
+ | `reason` | The file was dropped because |
158
+ | :--- | :--- |
159
+ | `excluded` | It matched your own `excludePatterns`, or fell outside a non-empty `include` allow-list. |
160
+ | `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
+ | `sensitive-file` | Its name is credential-bearing (`.env`, `*.pem`, `.npmrc`, …). |
162
+ | `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. |
164
+ | `symlink` | It is a link, and links are not followed. A directory link names its target in `detail`. |
165
+ | `outside-root` | It is a link whose target resolves outside the scanned root. |
166
+ | `gitignore` | A `.gitignore` excluded it, in a scan that is not reading git's index. |
167
+ | `read-error` | It could not be read. |
168
+ | `not-file` | It is a directory, socket or FIFO. |
169
+
170
+ Every path the scanner discovers is recorded — nothing is dropped silently, in either discovery
171
+ mode. Inside a git repository the candidate list comes from git, which applies `.gitignore`
172
+ before the scanner runs; the ignored paths are then listed separately and recorded here too, so
173
+ a `.gitignore`d file or directory appears in `skipped[]` with reason `gitignore` whether or not
174
+ git is present.
175
+
176
+ `explain` reports two further reasons that never appear here, because they describe a path
177
+ rather than a decision about a candidate file: `missing` (nothing exists at that path) and
178
+ `not-discovered` (the path exists but was never a candidate — inside `node_modules`, for
179
+ example). See [Troubleshooting](troubleshooting.md#a-file-is-missing-from-the-snapshot).
180
+
181
+ ## Safety
182
+
183
+ - **Sensitive files are excluded by default.** `.env`, `*.pem`, `*.key`, `.npmrc`, `.netrc`,
184
+ `.git-credentials`, `id_rsa`, `.kube/config`, `*.tfstate` and similar are skipped and
185
+ listed in `skipped[]`. `--include-sensitive-files` opts in.
186
+ - **Secret-shaped strings are redacted** inside ordinary source files. Only the matched span
187
+ is masked, never the whole line, and no excerpt of a matched line is written to any
188
+ artifact.
189
+ - **The output directory is owned by the tool.** Each run writes `.codebase-snapshot` into it
190
+ first, then swaps the new snapshot in atomically. A previous snapshot survives until the
191
+ replacement is complete.
192
+ - **The tool refuses to replace a directory it did not write.** Pointing `--out-dir` at
193
+ `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.
197
+ - **Ownership is the marker, not a filename.** `.codebase-snapshot` is a JSON record carrying
198
+ 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
202
+ written by 3.0.0 (no `id`), a bare `manifest.json`, and a directory with files you added
203
+ after the run are all refused.
204
+ See [Troubleshooting](troubleshooting.md#refusing-to-replace-outdir-).
205
+
206
+ The marker also makes hook-written artifacts safe: the entries a `hooks.afterGenerate` adds to
207
+ the output are recorded in the marker, so they do not make the next run refuse the directory.
208
+
209
+ ## Committing the snapshot
210
+
211
+ Usually you do not. Add the output directory to `.gitignore` — `codebase-snapshot init` does
212
+ it for you. If you do commit snapshots (for example, to diff context across releases),
213
+ `manifest.json` is the file that matters: it is compact and `diff` can compare two of them.