@ankurhalder/codebase-snapshot 3.0.0 → 4.0.1

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,126 +1,146 @@
1
1
  # Configuration reference
2
2
 
3
- Everything `@ankurhalder/codebase-snapshot` can be told to do, in one place.
3
+ Every setting, every default, and what each one changes. For commands and options see the
4
+ [CLI reference](cli.md).
4
5
 
5
- ```bash
6
- npx @ankurhalder/codebase-snapshot init # writes a starter config
7
- ```
6
+ Nothing here is required: with no config at all the tool runs on the defaults below.
8
7
 
9
- Config files are resolved in this order, first match wins:
8
+ ## Contents
10
9
 
11
- `snapshot.config.ts` → `.js` → `.mjs` → `.cjs` → `.json` → `.snapshotrc.json` → `.snapshotrc` → `"snapshot"` in `package.json`.
10
+ - [Config files](#config-files)
11
+ - [Scoping: which files get in](#scoping-which-files-get-in)
12
+ - [Project presets](#project-presets)
13
+ - [Classification: layers and kinds](#classification-layers-and-kinds)
14
+ - [Sizing: byte budgets](#sizing-byte-budgets)
15
+ - [Artifacts: what gets written](#artifacts-what-gets-written)
16
+ - [Secrets: redaction](#secrets-redaction)
17
+ - [Output directories](#output-directories)
18
+ - [Hooks](#hooks)
19
+ - [Complete example](#complete-example)
20
+ - [Interactive setup](#interactive-setup)
12
21
 
13
- > A `.ts` config is only importable natively from Node **22.6+**. `init` writes `.mjs` on older Node, and an unloadable config is reported on stderr rather than ignored.
22
+ ## Config files
14
23
 
15
- ---
16
-
17
- ## Scoping: which files get in
24
+ The first match wins:
18
25
 
19
- ```javascript
20
- export default {
21
- include: ["src/**", "packages/*/src/**"], // allow-list, empty = everything
22
- excludePatterns: ["**/*.test.ts", "docs"], // narrows what include let through
23
- };
26
+ ```
27
+ snapshot.config.ts
28
+ snapshot.config.js
29
+ snapshot.config.mjs
30
+ snapshot.config.cjs
31
+ snapshot.config.json
32
+ .snapshotrc / .snapshotrc.json
33
+ package.json → "snapshot"
24
34
  ```
25
35
 
26
- `include` runs first, `excludePatterns` narrows the survivors. Both accept globs
27
- (`**`, `*`, `?`, `{a,b}`, `[abc]`) or plain strings; a plain string matches a
28
- whole path segment sequence, so `"docs"` matches `docs/a.md` and `src/docs/b.md`
29
- but not `docsite/c.md`.
36
+ `snapshot.config.json` accepts comments. `package.json`'s `"snapshot"` key is read only when
37
+ no dedicated config file exists, and it must be an object (a wrong type is a warning, since
38
+ other tools use that key too).
30
39
 
31
- Every dropped file is recorded in `manifest.skipped` with `reason` and the exact
32
- pattern responsible (`excluded`, `default-ignore`, `sensitive-file`, `binary`,
33
- `symlink`, `read-error`, `not-file`).
40
+ The extension decides how the file is loaded. `.cjs` is always CommonJS and must use
41
+ `module.exports`; `.mjs` and `.ts` are always ESM and must use `export default`. `.js` follows
42
+ the package's `type` field — `module` means ESM, `commonjs` means CommonJS; with no `type`
43
+ Node decides by detecting the syntax, so `export default` works on current Node but is not
44
+ portable to versions without detection. Getting it wrong is the `Could not load …` error
45
+ above, not a silent fallback.
34
46
 
35
- ```javascript
36
- export default {
37
- includeSensitiveFiles: false, // .env, *.pem, .npmrc, .ssh/, .tfstate …
38
- };
39
- ```
47
+ A config file that exists but cannot be parsed is a **hard error** (exit `1`, nothing
48
+ written). Silently ignoring it would produce a snapshot with none of your settings.
40
49
 
41
- ## Project presets
50
+ A `.ts` config needs Node ≥ 22.6 **and** an ES-module project, because Node applies the
51
+ package's `type` field to a `.ts` file. `codebase-snapshot init` picks the right filename for
52
+ the project — `.ts` when both conditions hold, `.mjs` otherwise — and writes a plain object
53
+ with no imports so it loads even when the package is only used through `npx`.
54
+
55
+ CLI flags override the config for a single run (`--max-part-bytes`, `--out-dir`, …).
56
+ `--no-config` goes further and ignores every config file, so the run uses the built-in
57
+ defaults and the project's detected preset. A code config is executed when it is loaded, so
58
+ `--no-config` is also the way to snapshot a repository whose config you do not want to run —
59
+ see [the trust boundary](../SECURITY.md#trust-boundary).
60
+
61
+ ## Scoping: which files get in
42
62
 
43
- A preset is a detected project shape: layers, kinds, ecosystem ignores and a
44
- recommended part budget. Detection reads the repository **once** and the first
45
- matching preset wins, ordered by specificity — so a Next.js app inside a pnpm
46
- workspace is still a Next.js project.
47
-
48
- | Preset | Detected from |
49
- | :--- | :--- |
50
- | `nextjs` | `next.config.*`, or `next` |
51
- | `nuxt` | `nuxt.config.*`, or `nuxt` |
52
- | `sveltekit` | `svelte.config.*`, `@sveltejs/kit` |
53
- | `remix` | `@remix-run/*` |
54
- | `astro` | `astro.config.*`, `astro` |
55
- | `angular` | `angular.json`, `@angular/core` |
56
- | `solid` | `solid-js` |
57
- | `vue-vite` | `vue` + vite |
58
- | `react-vite` | `react` + vite |
59
- | `nest` | `@nestjs/core` |
60
- | `node-express` | `express`, `fastify`, `koa` |
61
- | `node-lib` | any `package.json` |
62
- | `python-django` | `manage.py`, or django in the manifest |
63
- | `python-fastapi` | fastapi / flask in the manifest |
64
- | `python-lib` | `pyproject.toml`, `setup.py`, `.py` files |
65
- | `go` | `go.mod` |
66
- | `rust` | `Cargo.toml` |
67
- | `java-spring` | `pom.xml`, `build.gradle` |
68
- | `dotnet` | `*.csproj`, `*.sln` |
69
- | `php-laravel` | `artisan`, `composer.json` |
70
- | `ruby-rails` | `Gemfile`, `config.ru` |
71
- | `monorepo` | `pnpm-workspace.yaml`, `workspaces`, `nx.json`, `turbo.json` |
72
- | `infra` | `*.tf`, `Chart.yaml`, `docker-compose.yml` |
73
- | `docs` | mostly Markdown, no `package.json` |
74
- | `generic` | anything else |
75
-
76
- Force one when detection is wrong:
77
-
78
- ```javascript
63
+ ```js
79
64
  export default {
80
- preset: "react-vite",
65
+ include: [], // allow-list; empty means "everything"
66
+ excludePatterns: [], // extra excludes, applied on top of the preset's
81
67
  };
82
68
  ```
83
69
 
84
- An unknown name is an error listing every valid preset — never a silent fallback.
85
- Each preset also contributes ignore patterns the generic list cannot know
86
- (`__pycache__`, `target`, `vendor`, `bin`/`obj`, …). The resolved preset is
87
- recorded in `manifest.config.preset`, so changing it shows up as config drift.
70
+ - `include` is an allow-list of globs. When non-empty, only matching files are considered.
71
+ - `excludePatterns` removes files everywhere. Presets contribute their own ecosystem ignores
72
+ on top; those are not removable, only addable to.
88
73
 
89
- ---
74
+ Built-in ignores always apply: `.git`, `node_modules`, `.next`, `dist`, `build`, lockfiles,
75
+ minified assets and binaries. Sensitive files are handled separately (see
76
+ [Secrets](#secrets-redaction)).
90
77
 
91
- ## Interactive setup
78
+ Every path the scanner discovers is recorded in `manifest.skipped[]` with the reason and the
79
+ rule that matched — nothing it sees is dropped silently, including a `.gitignore`d path in a
80
+ repository where git does the filtering. See the [`skipped[]`
81
+ reasons](output.md#skipped). To debug one path:
92
82
 
93
83
  ```bash
94
- npm create codebase-snapshot # or: npx @ankurhalder/codebase-snapshot init
84
+ npx codebase-snapshot explain src/some/file.ts
95
85
  ```
96
86
 
97
- Detects the project, asks seven questions with real defaults, and writes the
98
- config. It never prompts when either end is not a TTY: every answer has a flag.
99
-
100
- | Flag | Question it answers |
101
- | :--- | :--- |
102
- | `--preset <name>` | project type |
103
- | `--include <globs>` / `--exclude <globs>` | scope |
104
- | `--max-part-bytes <size>` | part size |
105
- | `--skip-artifacts` / `--only-artifacts` | artifacts |
106
- | `--include-sensitive-files` | safety |
107
- | `--minimal` | write only what differs from the defaults |
108
- | `--yes` | accept everything, do not prompt |
109
- | `--force` | overwrite an existing config |
110
- | `--dry-run` | print the config, write nothing |
111
-
112
- The generated config lists **every setting at its current value**, commented and
113
- grouped. That is deliberate: most people never open the documentation, and a
114
- config that only records their changes cannot tell them what else exists.
115
- Deleting any line is safe — the defaults are real.
87
+ ## Project presets
88
+
89
+ Detection reads the repository **once** and picks the highest-priority match, so a Next.js
90
+ app inside a pnpm workspace is still `nextjs`, not `monorepo`. `peerDependencies` and
91
+ `optionalDependencies` count as dependencies. A bare `package.json` ranks below any
92
+ definitive language manifest, so a Go or Rust repository with a `package.json` for tooling is
93
+ not mistaken for a Node library.
94
+
95
+ | Preset | Priority | Detected from | Budget | Ecosystem ignores |
96
+ | :--- | ---: | :--- | :--- | :--- |
97
+ | `nextjs` | 100 | `next.config.*`, or `next` | 300k | `.svelte-kit`, `.astro`, `.vite`, `.output` |
98
+ | `nuxt` | 98 | `nuxt.config.*`, or `nuxt` | 200k | `.nuxt`, `.output` |
99
+ | `sveltekit` | 96 | `svelte.config.*`, `@sveltejs/kit` | 200k | `.svelte-kit`, `.vercel` |
100
+ | `remix` | 94 | `@remix-run/*` | 200k | `.cache`, `public/build` |
101
+ | `astro` | 92 | `astro.config.*`, `astro` | 150k | `.astro`, `dist` |
102
+ | `angular` | 90 | `angular.json`, `@angular/core` | 200k | `.angular`, `dist` |
103
+ | `solid` | 88 | `solid-js`, `solid-start` | 120k | `.solid`, `dist` |
104
+ | `vue-vite` | 86 | `vue` + `vite`/`vite.config.*` | 120k | `.vite`, `dist` |
105
+ | `react-vite` | 84 | `react` + `vite`/`vite.config.*` | 120k | `.vite`, `dist` |
106
+ | `nest` | 80 | `@nestjs/core` | 200k | `dist`, `node_modules` |
107
+ | `node-express` | 78 | `express`, `fastify`, `koa`, `@hapi/hapi` | 200k | `dist` |
108
+ | `monorepo` | 70 | `pnpm-workspace.yaml`, `nx.json`, `turbo.json`, `lerna.json`, `"workspaces"` | 200k | `.turbo`, `.nx` |
109
+ | `python-django` | 56 | `manage.py`, django in `pyproject.toml`/`requirements.txt` | 150k | `__pycache__`, `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.tox`, `.eggs`, `staticfiles`, `media` |
110
+ | `python-fastapi` | 54 | fastapi or flask in `pyproject.toml`/`requirements.txt` | 150k | `__pycache__`, `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.tox`, `.eggs` |
111
+ | `go` | 50 | `go.mod` | 200k | `vendor`, `testdata` |
112
+ | `rust` | 48 | `Cargo.toml` | 200k | `target` |
113
+ | `java-spring` | 46 | `pom.xml`, `build.gradle`, `build.gradle.kts` | 200k | `.gradle`, `out`, `target` |
114
+ | `node-lib` | 45 | any `package.json` | 120k | `dist` |
115
+ | `dotnet` | 44 | `*.csproj`, `*.sln`, `global.json` | 200k | `bin`, `obj`, `.vs` |
116
+ | `php-laravel` | 42 | `artisan`, laravel in `composer.json`, `composer.json` | 150k | `vendor`, `storage`, `bootstrap/cache`, `public/build` |
117
+ | `ruby-rails` | 40 | `Gemfile`, `config.ru`, `Rakefile` | 150k | `.bundle`, `log`, `tmp`, `vendor/bundle`, `public/assets` |
118
+ | `python-lib` | 30 | `pyproject.toml`, `setup.py`, `requirements.txt`, `.py` files | 120k | `__pycache__`, `.venv`, `venv`, `.mypy_cache`, `.pytest_cache`, `.tox`, `.eggs` |
119
+ | `infra` | 24 | `*.tf`, `Chart.yaml`, `kustomization.yaml`, `docker-compose.yml` | 100k | `.terraform`, `.terragrunt-cache` |
120
+ | `docs` | 22 | ≥ 3 markdown files and no `package.json` | 100k | `.docusaurus`, `.vitepress/dist`, `site` |
121
+ | `generic` | 0 | anything else | 120k | — |
122
+
123
+ A preset contributes layers, kinds, ignores and a recommended budget — what `init` writes as
124
+ `maxPartBytes`. Force one when detection is wrong:
125
+
126
+ ```js
127
+ export default { preset: "react-vite" };
128
+ ```
129
+
130
+ An unknown name is an error listing every valid preset, never a silent fallback. The resolved
131
+ preset is recorded in `manifest.config.preset`, so changing it shows up as config drift.
132
+
116
133
  ## Classification: layers and kinds
117
134
 
118
- ```javascript
135
+ Layers group files (roughly directories), kinds label their role. Both are first-match-wins
136
+ with `other` as the fallback, and both accept a glob string, a `RegExp`, or a function.
137
+
138
+ ```js
119
139
  export default {
120
- layersMode: "merge", // default: your rules first, then the preset's
140
+ layersMode: "merge", // "merge" (default) or "replace"
121
141
  layers: [
122
- { name: "billing", match: "src/features/billing/**" },
123
- { name: "legacy", match: (p) => p.includes(".legacy.") },
142
+ { name: "api", match: "src/api/**" },
143
+ { name: "ui", match: /\.(tsx|vue|svelte)$/ },
124
144
  ],
125
145
  kinds: [
126
146
  { kind: "route", match: /(^|\/)route\.tsx?$/ },
@@ -129,115 +149,124 @@ export default {
129
149
  };
130
150
  ```
131
151
 
132
- Rules are evaluated in order, **first match wins**, with an `other` fallback.
133
- `match` may be a string, a `RegExp`, or a predicate function.
152
+ A layer rule is `{ name, match }` and a kind rule is `{ kind, match }`. The label key is
153
+ different in each, and a row that gets it wrong is rejected rather than silently ignored, so
154
+ copy the shapes above.
134
155
 
135
- `layersMode: "merge"` (the default) keeps the detected preset's rules and lets
136
- yours win by being evaluated first. `"replace"` discards the preset entirely.
156
+ - `layersMode: "merge"` keeps the preset's rules and lets yours win by being evaluated first.
157
+ - `layersMode: "replace"` discards the preset's rules entirely.
137
158
 
138
- Presets are auto-detected by specificity: the first match in descending priority wins,
139
- and `generic` is the fallback. There are 25 of them — see the table above. A framework
140
- that depends on `vite` still resolves to `react-vite` / `vue-vite` because those detectors
141
- outrank `node-lib`, and a Next.js app inside a pnpm workspace is still `nextjs`, not
142
- `monorepo`.
159
+ Classification drives per-layer budgets and the `Layers:` breakdown in `report`.
143
160
 
144
161
  ## Sizing: byte budgets
145
162
 
146
- ```javascript
163
+ ```js
147
164
  export default {
148
- maxPartBytes: "120k", // hard ceiling per part; default 300 KB, min 4096 B
149
- largeFileBytes: "200k", // above this a file gets a head/tail preview
165
+ maxPartBytes: "300k", // hard ceiling per part; default 307200, minimum 4096
166
+ largeFileBytes: "200k", // above this a file becomes a head/tail preview
150
167
  previewHeadLines: 80,
151
168
  previewTailLines: 40,
152
-
153
- budgets: { // optional per-layer overrides
169
+ budgets: { // optional per-layer overrides
154
170
  default: "120k",
155
171
  byLayer: { docs: 40_000, core: 300_000 },
156
172
  },
157
173
  };
158
174
  ```
159
175
 
160
- Budgets accept raw bytes (`122880`) or human sizes (`"120k"`, `"1.5mb"`, `"2 GB"`).
161
- Anything unparseable is an error rather than a silent fallback to the default.
176
+ Sizes accept bytes (`307200`), `"120k"`, `"1.5mb"`, and spaces (`"120 k"`). Zero and negative
177
+ values are errors; so is anything below 4096 — a part budget that small cannot hold a header,
178
+ let alone code.
162
179
 
163
- Files under different budgets are partitioned separately and the resulting parts
164
- are re-numbered, so filenames and `Previous:`/`Next:` links stay correct.
165
-
166
- ## Artifacts: what gets written
180
+ The budget is a hard ceiling: a part never exceeds it. A single file larger than the budget is
181
+ not split mid-file — it becomes a head/tail preview, marked `isPreviewTruncated`.
167
182
 
168
- Everything is on by default. Each artifact can be switched off independently —
169
- `artifacts` in config, or `--skip-artifacts` / `--only-artifacts` on the CLI.
183
+ `budgets.byLayer` lets expensive layers get more room than generated docs, for example.
170
184
 
171
- | Artifact | Default | File |
172
- | :--- | :--- | :--- |
173
- | `readme` | on | `README.md` |
174
- | `manifest` | on | `manifest.json` — **off breaks `validate` and `report`** |
175
- | `parts` | on | `parts/part-XXX.md` |
176
- | `navigation` | on | `reports/navigation.md` — dangles if `parts` is off |
177
- | `projectTree` | on | `reports/project-tree.md` |
178
- | `audit` | on | `reports/audit.md` |
179
- | `monolith` | off | `full-codebase.{json,md}` |
185
+ ## Artifacts: what gets written
180
186
 
181
- ```javascript
187
+ ```js
182
188
  export default {
183
- artifacts: { parts: false, navigation: false }, // reports-only pass
184
- includeMonolith: false,
185
- maxMonolithBytes: null,
189
+ artifacts: {
190
+ readme: true, // snapshot/README.md
191
+ manifest: true, // snapshot/manifest.json
192
+ parts: true, // snapshot/parts/*.md
193
+ navigation: true, // snapshot/reports/navigation.md
194
+ projectTree: true, // snapshot/reports/project-tree.md
195
+ audit: true, // snapshot/reports/audit.md
196
+ monolith: false, // full-codebase.json + full-codebase.md
197
+ },
186
198
  };
187
199
  ```
188
200
 
189
- Unknown artifact names are rejected, not ignored. The selection is recorded in
190
- `manifest.config.artifacts`, so changing it later shows up as config drift.
201
+ Everything except `monolith` is on by default; `monolith` follows `includeMonolith`. Turn
202
+ artifacts off with the config, `--skip-artifacts a,b`, or `--only-artifacts a,b` (the two
203
+ cannot be combined).
204
+
205
+ Two combinations warn because they produce a snapshot that contradicts itself: `manifest` off
206
+ means `validate` and `report` cannot inspect the snapshot, and `parts` off with `navigation`
207
+ on leaves links pointing at files that were never written. Both are allowed.
191
208
 
192
- The monolith is built from the **pre-partition** contents, so it holds the whole
193
- file even when a part had to truncate it.
209
+ The `.codebase-snapshot` ownership marker is always written, whatever the selection.
194
210
 
195
- ## Secrets: redaction rules
211
+ See [Snapshot output](output.md) for the layout and the manifest schema.
196
212
 
197
- 21 built-in rules cover PEM keys, JWT, AWS, GitHub, Slack, Stripe, Google,
198
- OpenAI/Anthropic, npm, PyPI, SendGrid, Twilio, DigitalOcean, Azure, database
199
- URIs, basic-auth URLs and `.env`-style assignments.
213
+ ## Secrets: redaction
200
214
 
201
- ```javascript
215
+ ```js
202
216
  export default {
203
217
  secrets: {
204
218
  customPatterns: [
219
+ { rule: "github-pat", pattern: /ghp_[A-Za-z0-9]{36}/ },
205
220
  { rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ },
206
- { rule: "from-json", pattern: "ACME_[a-z0-9]{10}" }, // strings allowed
207
221
  ],
208
222
  disableDefaultPatterns: false,
209
223
  },
224
+ includeSensitiveFiles: false, // .env, *.pem, .npmrc, .ssh/, .tfstate …
210
225
  };
211
226
  ```
212
227
 
213
- Only the matched span is replaced; the rest of the line survives. Multi-line PEM
214
- blocks are masked as a block, including ones cut off before their `END` marker.
215
- No excerpt of a matched line is ever written to any artifact.
228
+ Each entry is `{ rule, pattern }`, where `rule` is the name that appears in the audit and
229
+ `pattern` is a `RegExp`, or a string in a `.json` config. A bare `RegExp` in the array works
230
+ too — it comes out as `custom-1`, `custom-2`, … in the findings — but naming the rule is what
231
+ makes an audit entry mean something.
232
+
233
+ - Credential-bearing **files** are skipped by default and listed in `skipped[]` with reason
234
+ `sensitive-file`. `includeSensitiveFiles: true` emits them.
235
+ - Secret-shaped **strings** inside ordinary files are redacted in the emitted parts. Only the
236
+ matched span is masked, never the whole line, and no excerpt of a matched line reaches any
237
+ artifact. Multi-line PEM keys are masked as a block.
238
+ - `customPatterns` add rules; `disableDefaultPatterns` removes the built-in set — it does not
239
+ disable redaction itself.
216
240
 
217
- ## Output layout
241
+ Findings are summarised in `reports/audit.md` and in `report`.
218
242
 
219
- ```javascript
243
+ ## Output directories
244
+
245
+ ```js
220
246
  export default {
221
247
  outDir: "snapshot",
222
248
  tmpDir: "snapshot.tmp",
223
- branch: "main", // fallback when git cannot report one
224
249
  };
225
250
  ```
226
251
 
227
- `outDir` may not be the working directory, an ancestor, or the filesystem root,
228
- and `outDir`/`tmpDir` may not contain each other. Writes are atomic: the previous
229
- snapshot survives until the new one is fully in place.
252
+ `outDir` may not be the working directory, an ancestor, or the filesystem root, and
253
+ `outDir`/`tmpDir` may not contain one another — the message prints the resolved paths. Writes
254
+ are atomic: the previous snapshot survives until the new one is complete.
255
+
256
+ Both directories are also **refused if they already exist and this generator did not write
257
+ them**, so `outDir: "src"` cannot replace your source tree. Ownership is proved by the
258
+ `.codebase-snapshot` marker and nothing else: a JSON object naming this generator, with a
259
+ schema version this build understands, a per-snapshot `id`, and the list of entries it wrote.
260
+ A file with that name but no valid marker, or a directory holding a manifest from another
261
+ tool, is refused. The proof is never a filename, so an existing `docs/README.md` is safe.
230
262
 
231
- Both directories are also **refused if they already exist and this generator did
232
- not write them** — a typo such as `outDir: "src"` would otherwise replace your
233
- source tree. Ownership is proved by a `.codebase-snapshot` marker (plus a
234
- parseable `manifest.json` for older output), never by filenames, so an existing
235
- `docs/README.md` is never mistaken for a previous snapshot. An empty directory,
236
- or one holding only `.gitkeep`/`.gitignore`/`.DS_Store`, is always accepted.
263
+ Files your hook writes into the output directory are recorded in the marker's entry list, so
264
+ they do not block later runs; a file you add by hand afterwards does, because it is not in the
265
+ snapshot the marker describes.
237
266
 
238
267
  ## Hooks
239
268
 
240
- ```javascript
269
+ ```js
241
270
  export default {
242
271
  hooks: {
243
272
  afterGenerate: (manifest) => {
@@ -247,94 +276,93 @@ export default {
247
276
  };
248
277
  ```
249
278
 
250
- A throwing hook becomes a warning on the manifest — it never loses a snapshot
251
- that was already written.
252
-
253
- A hook may write its own files into the output directory. That is safe across
254
- reruns: ownership of the directory is established by a `.codebase-snapshot`
255
- marker written before any artifact, not by the set of filenames present, so
256
- custom artifacts never look like someone else's data.
279
+ A throwing hook is downgraded to a warning on the manifest — it never loses a snapshot that
280
+ was already written.
257
281
 
258
- Note that a hook runs in the **process** working directory, so with
259
- `codebase-snapshot --cwd elsewhere` a relative path resolves against your shell's directory,
260
- not the target repo. Use the absolute paths on `manifest` (or resolve against
261
- `outDir` yourself) when passing `--cwd`.
282
+ A hook may write its own files into the output directory; that is safe across reruns because
283
+ ownership is established by the marker rather than by the filenames present.
262
284
 
263
- ---
285
+ A hook runs in the **process** working directory, so with `--cwd elsewhere` a relative path
286
+ resolves against your shell, not the target repository. Use the absolute paths on `manifest`
287
+ (or resolve against `outDir` yourself) when passing `--cwd`.
264
288
 
265
- # Commands
289
+ ## Complete example
266
290
 
267
- | Command | Purpose |
268
- | :--- | :--- |
269
- | `generate` *(default)* | Write the snapshot |
270
- | `validate`, `check` | Re-hash sources and parts, compare config, detect drift |
271
- | `report` | Print metrics, layer distribution and drift status |
272
- | `explain <path>` | Show how one path would be treated |
273
- | `diff <a> <b>` | Compare two snapshots |
274
- | `init` | Write a starter config |
291
+ Every setting at its built-in default. Nothing here has to be written — the tool runs on these
292
+ values with no config at all.
275
293
 
276
- ## `explain`
294
+ ```js
295
+ export default {
296
+ // ── Project ──────────────────────────────────────────────
297
+ preset: "detected", // "detected" auto-detects; or name one of the 25
298
+ branch: "main", // recorded when git cannot report one
277
299
 
278
- ```
279
- $ codebase-snapshot explain src/app/page.tsx
280
-
281
- src/app/page.tsx
282
-
283
- included yes
284
- layer app (rule (p) => p.startsWith("src/app/") || ...)
285
- kind component (rule (p) => { const base = p.split("/").pop() ...)
286
- language tsx
287
- size 48 B, 1 line(s)
288
- part part-001.md
289
- inclusion full
290
- secrets none
291
- ```
300
+ // ── Output ───────────────────────────────────────────────
301
+ outDir: "snapshot",
302
+ tmpDir: "snapshot.tmp",
292
303
 
293
- Excluded paths say why:
304
+ // ── Size ─────────────────────────────────────────────────
305
+ maxPartBytes: "300k",
306
+ largeFileBytes: "200k",
307
+ previewHeadLines: 80,
308
+ previewTailLines: 40,
309
+ budgets: { default: "300k", byLayer: {} },
310
+
311
+ // ── Scope ────────────────────────────────────────────────
312
+ include: [],
313
+ excludePatterns: [],
314
+
315
+ // ── Artifacts ────────────────────────────────────────────
316
+ artifacts: {
317
+ readme: true,
318
+ manifest: true,
319
+ parts: true,
320
+ navigation: true,
321
+ projectTree: true,
322
+ audit: true,
323
+ monolith: false,
324
+ },
294
325
 
295
- ```
296
- $ codebase-snapshot explain .env
326
+ // ── Safety ───────────────────────────────────────────────
327
+ includeSensitiveFiles: false,
328
+ secrets: { customPatterns: [], disableDefaultPatterns: false },
297
329
 
298
- .env
330
+ // ── Classification ───────────────────────────────────────
331
+ layersMode: "merge",
332
+ layers: [],
333
+ kinds: [],
299
334
 
300
- included no
301
- reason sensitive-file
302
- detail credential-bearing file; enable includeSensitiveFiles to emit it
335
+ // ── Advanced ─────────────────────────────────────────────
336
+ includeMonolith: false,
337
+ maxMonolithBytes: null,
338
+ hooks: {},
339
+ };
303
340
  ```
304
341
 
305
- Exits `0` when the path is included, `1` when it is not. `--format json` emits the
306
- same data as an object.
307
-
308
- ## `diff`
342
+ Deleting any line is safe — the defaults are real, and each one is what the comment says.
309
343
 
310
- ```
311
- $ codebase-snapshot diff old-snapshot new-snapshot
344
+ This is **not** byte-identical to a generated file. `codebase-snapshot init` starts from these
345
+ values and substitutes two things: `preset` becomes the detected name (`node-lib` for a plain
346
+ npm package), and `maxPartBytes` with `budgets.default` become that preset's
347
+ `recommendedBudget` (`"120k"` in the same project). `generate` on its own applies no
348
+ recommended budget — without a config it uses `--max-part-bytes`, default 307200 — so the
349
+ preset's figure only takes effect once `init` has written it into a config.
312
350
 
313
- before 2026-11-03T10:00:00.000Z 812 file(s), 4 part(s)
314
- after 2026-11-03T11:30:00.000Z 820 file(s), 4 part(s)
351
+ ## Interactive setup
315
352
 
316
- + 9 added
317
- - 1 removed
318
- ~ 3 changed
319
- = 808 unchanged
353
+ ```bash
354
+ npx codebase-snapshot init
320
355
  ```
321
356
 
322
- Both arguments may be snapshot directories or paths to a `manifest.json`.
323
-
324
- ## `--watch`
357
+ Walks every tunable parameter, showing the detected value as the default. Every prompt has a
358
+ flag, so the same setup works non-interactively:
325
359
 
326
360
  ```bash
327
- npx @ankurhalder/codebase-snapshot generate --watch
361
+ npx codebase-snapshot init --yes
362
+ npx codebase-snapshot init --preset go --max-part-bytes 200k \
363
+ --include "cmd/**,internal/**" --exclude "**/*_test.go" --yes
328
364
  ```
329
365
 
330
- Re-generates on every change, ignoring writes into the output and temporary
331
- directories (including the atomic-swap backup) so it cannot trigger itself.
332
-
333
- ---
334
-
335
- # Drift
336
-
337
- `validate` compares five things: source hashes on disk, the emitted parts (size
338
- and SHA-256), the manifest's own bookkeeping, the recorded configuration
339
- (budgets, includes, excludes, artifacts, layers mode), and git state. Changing a
340
- config value without re-generating is reported as drift, not silently accepted.
366
+ `--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`.