@ankurhalder/codebase-snapshot 0.0.0-stage → 2.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.
@@ -0,0 +1,340 @@
1
+ # Configuration reference
2
+
3
+ Everything `@ankurhalder/codebase-snapshot` can be told to do, in one place.
4
+
5
+ ```bash
6
+ npx @ankurhalder/codebase-snapshot init # writes a starter config
7
+ ```
8
+
9
+ Config files are resolved in this order, first match wins:
10
+
11
+ `snapshot.config.ts` → `.js` → `.mjs` → `.cjs` → `.json` → `.snapshotrc.json` → `.snapshotrc` → `"snapshot"` in `package.json`.
12
+
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.
14
+
15
+ ---
16
+
17
+ ## Scoping: which files get in
18
+
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
+ };
24
+ ```
25
+
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`.
30
+
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`).
34
+
35
+ ```javascript
36
+ export default {
37
+ includeSensitiveFiles: false, // .env, *.pem, .npmrc, .ssh/, .tfstate …
38
+ };
39
+ ```
40
+
41
+ ## Project presets
42
+
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
79
+ export default {
80
+ preset: "react-vite",
81
+ };
82
+ ```
83
+
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.
88
+
89
+ ---
90
+
91
+ ## Interactive setup
92
+
93
+ ```bash
94
+ npm create codebase-snapshot # or: npx @ankurhalder/codebase-snapshot init
95
+ ```
96
+
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.
116
+ ## Classification: layers and kinds
117
+
118
+ ```javascript
119
+ export default {
120
+ layersMode: "merge", // default: your rules first, then the preset's
121
+ layers: [
122
+ { name: "billing", match: "src/features/billing/**" },
123
+ { name: "legacy", match: (p) => p.includes(".legacy.") },
124
+ ],
125
+ kinds: [
126
+ { kind: "route", match: /(^|\/)route\.tsx?$/ },
127
+ { kind: "hook", match: "src/hooks/**" },
128
+ ],
129
+ };
130
+ ```
131
+
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.
134
+
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.
137
+
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`.
143
+
144
+ ## Sizing: byte budgets
145
+
146
+ ```javascript
147
+ 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
150
+ previewHeadLines: 80,
151
+ previewTailLines: 40,
152
+
153
+ budgets: { // optional per-layer overrides
154
+ default: "120k",
155
+ byLayer: { docs: 40_000, core: 300_000 },
156
+ },
157
+ };
158
+ ```
159
+
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.
162
+
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
167
+
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.
170
+
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}` |
180
+
181
+ ```javascript
182
+ export default {
183
+ artifacts: { parts: false, navigation: false }, // reports-only pass
184
+ includeMonolith: false,
185
+ maxMonolithBytes: null,
186
+ };
187
+ ```
188
+
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.
191
+
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.
194
+
195
+ ## Secrets: redaction rules
196
+
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.
200
+
201
+ ```javascript
202
+ export default {
203
+ secrets: {
204
+ customPatterns: [
205
+ { rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ },
206
+ { rule: "from-json", pattern: "ACME_[a-z0-9]{10}" }, // strings allowed
207
+ ],
208
+ disableDefaultPatterns: false,
209
+ },
210
+ };
211
+ ```
212
+
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.
216
+
217
+ ## Output layout
218
+
219
+ ```javascript
220
+ export default {
221
+ outDir: "snapshot",
222
+ tmpDir: "snapshot.tmp",
223
+ branch: "main", // fallback when git cannot report one
224
+ };
225
+ ```
226
+
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.
230
+
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.
237
+
238
+ ## Hooks
239
+
240
+ ```javascript
241
+ export default {
242
+ hooks: {
243
+ afterGenerate: (manifest) => {
244
+ // runs after a successful generate (and after a dry run)
245
+ },
246
+ },
247
+ };
248
+ ```
249
+
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.
257
+
258
+ Note that a hook runs in the **process** working directory, so with
259
+ `snap --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`.
262
+
263
+ ---
264
+
265
+ # Commands
266
+
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 |
275
+
276
+ ## `explain`
277
+
278
+ ```
279
+ $ snap 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
+ ```
292
+
293
+ Excluded paths say why:
294
+
295
+ ```
296
+ $ snap explain .env
297
+
298
+ .env
299
+
300
+ included no
301
+ reason sensitive-file
302
+ detail credential-bearing file; enable includeSensitiveFiles to emit it
303
+ ```
304
+
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`
309
+
310
+ ```
311
+ $ snap diff old-snapshot new-snapshot
312
+
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)
315
+
316
+ + 9 added
317
+ - 1 removed
318
+ ~ 3 changed
319
+ = 808 unchanged
320
+ ```
321
+
322
+ Both arguments may be snapshot directories or paths to a `manifest.json`.
323
+
324
+ ## `--watch`
325
+
326
+ ```bash
327
+ npx @ankurhalder/codebase-snapshot generate --watch
328
+ ```
329
+
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.
@@ -0,0 +1,136 @@
1
+ # Installation
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.
6
+
7
+ Requires **Node.js 18 or newer**.
8
+
9
+ ## One-off use (no install)
10
+
11
+ ```bash
12
+ npx @ankurhalder/codebase-snapshot generate
13
+ ```
14
+
15
+ Equivalent one-liners for other clients:
16
+
17
+ ```bash
18
+ yarn dlx -p @ankurhalder/codebase-snapshot snap generate # yarn Berry
19
+ yarn global add @ankurhalder/codebase-snapshot # yarn classic (installs globally)
20
+ pnpm dlx @ankurhalder/codebase-snapshot generate
21
+ bunx @ankurhalder/codebase-snapshot generate
22
+ ```
23
+
24
+ ## Add to a project
25
+
26
+ ```bash
27
+ npm install -D @ankurhalder/codebase-snapshot
28
+ yarn add -D @ankurhalder/codebase-snapshot # yarn classic and Berry
29
+ pnpm add -D @ankurhalder/codebase-snapshot
30
+ bun add -d @ankurhalder/codebase-snapshot
31
+ ```
32
+
33
+ Then run it through your client so it resolves from the project's `node_modules`:
34
+
35
+ ```bash
36
+ npx snap generate
37
+ yarn snap generate
38
+ pnpm exec snap generate
39
+ bun run snap generate
40
+ ```
41
+
42
+ Both `snap` and `codebase-snapshot` are exposed as binaries — pick whichever you prefer.
43
+
44
+ Adding it to `package.json` scripts works the same everywhere:
45
+
46
+ ```json
47
+ {
48
+ "scripts": {
49
+ "snapshot": "codebase-snapshot generate",
50
+ "snapshot:validate": "codebase-snapshot validate"
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Global install
56
+
57
+ ```bash
58
+ npm install -g @ankurhalder/codebase-snapshot
59
+ pnpm add -g @ankurhalder/codebase-snapshot
60
+ bun add -g @ankurhalder/codebase-snapshot
61
+ ```
62
+
63
+ A global install exists for convenience, but pinning the version per project (the
64
+ `-D`/`--save-dev` examples above) is the better default: snapshots are only reproducible
65
+ if the generator version is reproducible.
66
+
67
+ ## Version pinning
68
+
69
+ ```bash
70
+ npx @ankurhalder/codebase-snapshot@2 generate
71
+ pnpm dlx @ankurhalder/codebase-snapshot@2.1.0 generate
72
+ ```
73
+
74
+ ## Verified clients
75
+
76
+ Every client below was used to install the published package and run
77
+ `snap --version` successfully. This matrix is enforced in CI
78
+ (`.github/workflows/ci.yml` → `client-install`), so a change that breaks a client fails
79
+ the build rather than a user's terminal.
80
+
81
+ | Client | Version tested | Run installed binary with |
82
+ | :--- | :--- | :--- |
83
+ | npm | 10.x | `npx snap` |
84
+ | yarn (classic) | 1.22.22 | `yarn snap` |
85
+ | yarn (Berry, `nodeLinker: pnp`) | 4.6.0 | `yarn snap` |
86
+ | pnpm | 10.28.0 | `pnpm exec snap` |
87
+ | bun | 1.4.4 | `bun run snap` |
88
+
89
+ Yarn Berry's default **strict PnP** mode works: there is no `node_modules` directory, and
90
+ the runtime `typescript` dependency still resolves through yarn's `.pnp.cjs` loader.
91
+
92
+ ## Package manager workspaces
93
+
94
+ The binary is resolved from the workspace it is installed in. In a monorepo, install it
95
+ in the package that runs the snapshot (usually the root), or use any client's one-off
96
+ runner (`npx` / `pnpm dlx` / `yarn dlx` / `bunx`) which works from any subdirectory.
97
+
98
+ ## Uninstalling
99
+
100
+ Standard for your client (`npm uninstall`, `yarn remove`, `pnpm remove`, `bun remove`).
101
+ Snapshots already written to `snapshot/` are ordinary files and are left alone.
102
+
103
+ ## Not supported yet
104
+
105
+ - **`pip`, `brew`, `scoop`, `docker`, and standalone binaries.** These require shipping a
106
+ prebuilt executable rather than JavaScript, and are tracked as future work in
107
+ [docs/plans/distribution](../plans/distribution/README.md). Until then, any machine
108
+ running the tool needs Node 18+.
109
+ - **Deno.** The API can be imported under Deno's npm compatibility, but running the CLI
110
+ needs a `./bin` subpath export that is not published yet.
111
+
112
+ ## Troubleshooting
113
+
114
+ **`snap: command not found`** — when installed locally (not globally), the binary lives
115
+ in `node_modules/.bin` and is only on your `PATH` inside a package script or a client
116
+ runner. Use `npx snap`, `pnpm exec snap`, `yarn snap`, or `bun run snap`.
117
+
118
+ **`Cannot find module 'typescript'`** — the package declares `typescript` as a runtime
119
+ dependency for AST analysis. If your client prunes or externalizes it (some bundlers and
120
+ `--ignore-scripts` CI setups do), rerun a clean install, or pin `typescript@^5` yourself.
121
+
122
+ **CI has no Node** — that is not supported yet; this package is JavaScript and needs a
123
+ Node runtime. See the future-work note above.
124
+
125
+ **`yarn snap` prints extra lines, or breaks a script that parses output** — yarn classic
126
+ wraps everything: it echoes `yarn run 1.22.22`, the resolved command, your tool's output,
127
+ and a `Done in 0.37s.` trailer. For a human that is harmless; for anything that parses
128
+ `snap`'s stdout it is not. In scripts and CI, call the binary directly:
129
+
130
+ ```bash
131
+ ./node_modules/.bin/snap generate
132
+ npx snap generate # same, without the yarn wrapper
133
+ ```
134
+
135
+ This is why the CI job in this repository reads the version from a line that *is* the
136
+ version rather than taking the first or last line of output.
package/package.json CHANGED
@@ -1,6 +1,96 @@
1
1
  {
2
2
  "name": "@ankurhalder/codebase-snapshot",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.1.0",
4
+ "description": "Production-grade LLM codebase snapshot generator, auditor, and AST partitioner",
5
+ "keywords": [
6
+ "codebase",
7
+ "snapshot",
8
+ "llm",
9
+ "ai",
10
+ "context",
11
+ "ast",
12
+ "audit",
13
+ "codebase-snapshot",
14
+ "code-analysis"
15
+ ],
16
+ "author": "Ankur Halder <ankur.halder12345@gmail.com>",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/ankurhalder/codebase-snapshot.git"
21
+ },
22
+ "homepage": "https://github.com/ankurhalder/codebase-snapshot#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/ankurhalder/codebase-snapshot/issues"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "type": "module",
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "sideEffects": false,
34
+ "bin": {
35
+ "codebase-snapshot": "./dist/bin.js",
36
+ "snap": "./dist/bin.js"
37
+ },
38
+ "exports": {
39
+ ".": {
40
+ "import": {
41
+ "types": "./dist/index.d.ts",
42
+ "default": "./dist/index.js"
43
+ },
44
+ "require": {
45
+ "types": "./dist/index.d.cts",
46
+ "default": "./dist/index.cjs"
47
+ }
48
+ },
49
+ "./cli": {
50
+ "import": {
51
+ "types": "./dist/cli.d.ts",
52
+ "default": "./dist/cli.js"
53
+ },
54
+ "require": {
55
+ "types": "./dist/cli.d.cts",
56
+ "default": "./dist/cli.cjs"
57
+ }
58
+ },
59
+ "./package.json": "./package.json"
60
+ },
61
+ "files": [
62
+ "dist/*.js",
63
+ "dist/*.cjs",
64
+ "dist/*.d.ts",
65
+ "dist/*.d.cts",
66
+ "docs/*.md",
67
+ "README.md",
68
+ "LICENSE"
69
+ ],
70
+ "scripts": {
71
+ "build": "tsup",
72
+ "dev": "tsup --watch",
73
+ "typecheck": "tsc --noEmit",
74
+ "lint": "node scripts/lint.mjs",
75
+ "check": "npm run lint && npm run typecheck && npm test",
76
+ "pretest": "npm run build",
77
+ "test": "node --test tests/*.test.js",
78
+ "test:coverage": "node --test --experimental-test-coverage tests/*.test.js",
79
+ "prepare": "npm run build",
80
+ "prepublishOnly": "npm run build",
81
+ "publish:create": "npm publish packages/create --access public"
82
+ },
83
+ "dependencies": {
84
+ "typescript": "^5.0.0"
85
+ },
86
+ "overrides": {
87
+ "esbuild": "^0.28.1"
88
+ },
89
+ "devDependencies": {
90
+ "@types/node": "^22.0.0",
91
+ "tsup": "^8.3.0"
92
+ },
93
+ "engines": {
94
+ "node": ">=18.0.0"
95
+ }
96
+ }