@ankurhalder/codebase-snapshot 0.0.0-stage → 2.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/LICENSE +21 -0
- package/README.md +289 -2
- package/dist/bin.js +3725 -0
- package/dist/chunk-RGDVLQTT.js +3217 -0
- package/dist/cli.cjs +3718 -0
- package/dist/cli.d.cts +40 -0
- package/dist/cli.d.ts +40 -0
- package/dist/cli.js +487 -0
- package/dist/index.cjs +3292 -0
- package/dist/index.d.cts +364 -0
- package/dist/index.d.ts +364 -0
- package/dist/index.js +3 -0
- package/dist/types-S-5eRuRc.d.cts +372 -0
- package/dist/types-S-5eRuRc.d.ts +372 -0
- package/docs/configuration.md +244 -0
- package/package.json +93 -4
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
type Mode = "generate" | "validate" | "report";
|
|
2
|
+
type OutputFormat = "text" | "json";
|
|
3
|
+
/**
|
|
4
|
+
* How much of a file made it into the snapshot.
|
|
5
|
+
* `full` – the complete file body was emitted.
|
|
6
|
+
* `preview` – the body was truncated (by lines and/or bytes) before emission.
|
|
7
|
+
*/
|
|
8
|
+
type InclusionMode = "full" | "preview";
|
|
9
|
+
interface SourceSummary {
|
|
10
|
+
directives: string[];
|
|
11
|
+
imports: string[];
|
|
12
|
+
exports: string[];
|
|
13
|
+
/** Module specifiers re-exported wholesale via `export * from "..."`. */
|
|
14
|
+
reExports: string[];
|
|
15
|
+
functions: string[];
|
|
16
|
+
classes: string[];
|
|
17
|
+
interfaces: string[];
|
|
18
|
+
types: string[];
|
|
19
|
+
enums: string[];
|
|
20
|
+
variables: string[];
|
|
21
|
+
reactComponents: string[];
|
|
22
|
+
hooks: string[];
|
|
23
|
+
routeHandlers: string[];
|
|
24
|
+
/** Number of parser diagnostics, present only when the file has syntax errors. */
|
|
25
|
+
parseErrors?: number;
|
|
26
|
+
}
|
|
27
|
+
interface JsonSummary {
|
|
28
|
+
shape: string;
|
|
29
|
+
itemCount: number | null;
|
|
30
|
+
keys: string[];
|
|
31
|
+
sampleKeys: string[];
|
|
32
|
+
}
|
|
33
|
+
interface MarkdownSummary {
|
|
34
|
+
headings: string[];
|
|
35
|
+
frontmatterKeys: string[];
|
|
36
|
+
}
|
|
37
|
+
type SkipReason = "asset" | "binary" | "read-error" | "not-file" | "sensitive-file" | "symlink" | "default-ignore" | "excluded";
|
|
38
|
+
interface SkipRecord {
|
|
39
|
+
path: string;
|
|
40
|
+
reason: SkipReason;
|
|
41
|
+
detail?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One scanned file. Field names are canonical and unique by design:
|
|
45
|
+
* earlier releases carried `path`/`relativePath`, `sizeBytes`/`bytes` and
|
|
46
|
+
* `hashSha256`/`sha256` aliases that could only ever agree.
|
|
47
|
+
*/
|
|
48
|
+
interface FileEntry {
|
|
49
|
+
relativePath: string;
|
|
50
|
+
sizeBytes: number;
|
|
51
|
+
lines: number;
|
|
52
|
+
hashSha256: string;
|
|
53
|
+
part: string;
|
|
54
|
+
layer: string;
|
|
55
|
+
kind: string;
|
|
56
|
+
language: string;
|
|
57
|
+
inclusionMode: InclusionMode;
|
|
58
|
+
isLarge: boolean;
|
|
59
|
+
isPreviewTruncated: boolean;
|
|
60
|
+
/** Line count of the file on disk. */
|
|
61
|
+
originalLines: number;
|
|
62
|
+
/** Lines actually emitted into the snapshot. */
|
|
63
|
+
includedLines: number;
|
|
64
|
+
/** UTF-8 bytes of file content actually emitted into the snapshot. */
|
|
65
|
+
includedBytes: number;
|
|
66
|
+
summary?: SourceSummary | JsonSummary | MarkdownSummary;
|
|
67
|
+
/** True when the summary alone was too large for the part budget and was dropped. */
|
|
68
|
+
summaryOmitted?: boolean;
|
|
69
|
+
/** Emitted body after redaction/truncation. Never serialized into the manifest. */
|
|
70
|
+
content?: string;
|
|
71
|
+
}
|
|
72
|
+
interface PartFileEntry {
|
|
73
|
+
relativePath: string;
|
|
74
|
+
sourceBytes: number;
|
|
75
|
+
includedBytes: number;
|
|
76
|
+
lines: number;
|
|
77
|
+
layer: string;
|
|
78
|
+
kind: string;
|
|
79
|
+
}
|
|
80
|
+
interface PartMetadata {
|
|
81
|
+
index: number;
|
|
82
|
+
filename: string;
|
|
83
|
+
/** Exact UTF-8 byte length of the emitted part file. */
|
|
84
|
+
byteSize: number;
|
|
85
|
+
sha256: string;
|
|
86
|
+
lines: number;
|
|
87
|
+
fileCount: number;
|
|
88
|
+
layersCovered: string[];
|
|
89
|
+
includedFiles: PartFileEntry[];
|
|
90
|
+
}
|
|
91
|
+
interface SecretRule {
|
|
92
|
+
rule: string;
|
|
93
|
+
pattern: RegExp;
|
|
94
|
+
}
|
|
95
|
+
/** Shape accepted from JSON config files, where `pattern` can only be a string. */
|
|
96
|
+
interface SecretRuleInput {
|
|
97
|
+
rule: string;
|
|
98
|
+
pattern: RegExp | string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A detected secret. Deliberately carries no excerpt of the matched source:
|
|
102
|
+
* the previous `snippet` field was the vector by which raw credentials were
|
|
103
|
+
* written into `reports/audit.md`.
|
|
104
|
+
*/
|
|
105
|
+
interface SecretFinding {
|
|
106
|
+
rule: string;
|
|
107
|
+
line: number;
|
|
108
|
+
path?: string;
|
|
109
|
+
}
|
|
110
|
+
interface ManifestTotals {
|
|
111
|
+
candidateFiles: number;
|
|
112
|
+
includedFiles: number;
|
|
113
|
+
fullFiles: number;
|
|
114
|
+
previewFiles: number;
|
|
115
|
+
skippedFiles: number;
|
|
116
|
+
totalSourceBytes: number;
|
|
117
|
+
totalContentBytes: number;
|
|
118
|
+
partCount: number;
|
|
119
|
+
warningCount: number;
|
|
120
|
+
}
|
|
121
|
+
interface ManifestArtifact {
|
|
122
|
+
byteSize: number;
|
|
123
|
+
sha256: string;
|
|
124
|
+
}
|
|
125
|
+
interface ManifestIndexes {
|
|
126
|
+
routes: Array<{
|
|
127
|
+
path: string;
|
|
128
|
+
methods: string[];
|
|
129
|
+
part: string;
|
|
130
|
+
}>;
|
|
131
|
+
components: Array<{
|
|
132
|
+
path: string;
|
|
133
|
+
symbols: string[];
|
|
134
|
+
part: string;
|
|
135
|
+
layer: string;
|
|
136
|
+
}>;
|
|
137
|
+
handlers: Array<{
|
|
138
|
+
path: string;
|
|
139
|
+
exports: string[];
|
|
140
|
+
part: string;
|
|
141
|
+
}>;
|
|
142
|
+
features: Array<{
|
|
143
|
+
name: string;
|
|
144
|
+
files: number;
|
|
145
|
+
}>;
|
|
146
|
+
imports: {
|
|
147
|
+
byFile: Array<{
|
|
148
|
+
path: string;
|
|
149
|
+
imports: string[];
|
|
150
|
+
exports: string[];
|
|
151
|
+
}>;
|
|
152
|
+
centralFiles: Array<{
|
|
153
|
+
path: string;
|
|
154
|
+
importedByCount: number;
|
|
155
|
+
}>;
|
|
156
|
+
};
|
|
157
|
+
directives: Array<{
|
|
158
|
+
path: string;
|
|
159
|
+
directives: string[];
|
|
160
|
+
}>;
|
|
161
|
+
}
|
|
162
|
+
/** Effective configuration recorded so `validate` can detect config drift. */
|
|
163
|
+
interface ManifestConfigSnapshot {
|
|
164
|
+
maxPartBytes: number;
|
|
165
|
+
largeFileBytes: number;
|
|
166
|
+
previewHeadLines: number;
|
|
167
|
+
previewTailLines: number;
|
|
168
|
+
includeMonolith: boolean;
|
|
169
|
+
includeSensitiveFiles: boolean;
|
|
170
|
+
artifacts: ResolvedArtifacts;
|
|
171
|
+
include: string[];
|
|
172
|
+
layersMode: "merge" | "replace";
|
|
173
|
+
budgets: ResolvedBudgets;
|
|
174
|
+
excludePatterns: string[];
|
|
175
|
+
layers: string[];
|
|
176
|
+
kinds: string[];
|
|
177
|
+
}
|
|
178
|
+
interface Manifest {
|
|
179
|
+
schemaVersion: string;
|
|
180
|
+
generatedAt: string;
|
|
181
|
+
generator: {
|
|
182
|
+
name: string;
|
|
183
|
+
version: string;
|
|
184
|
+
};
|
|
185
|
+
git: {
|
|
186
|
+
branch: string;
|
|
187
|
+
commit: string;
|
|
188
|
+
isDirty: boolean;
|
|
189
|
+
};
|
|
190
|
+
budgets: {
|
|
191
|
+
maxPartBytes: number;
|
|
192
|
+
largeFileBytes: number;
|
|
193
|
+
};
|
|
194
|
+
config: ManifestConfigSnapshot;
|
|
195
|
+
counts: {
|
|
196
|
+
totalFiles: number;
|
|
197
|
+
totalSizeBytes: number;
|
|
198
|
+
totalLines: number;
|
|
199
|
+
partsCount: number;
|
|
200
|
+
skippedCount: number;
|
|
201
|
+
largeFileCount: number;
|
|
202
|
+
truncatedPreviewCount: number;
|
|
203
|
+
};
|
|
204
|
+
totals: ManifestTotals;
|
|
205
|
+
fingerprint: string;
|
|
206
|
+
validation: {
|
|
207
|
+
status: "passed" | "failed";
|
|
208
|
+
errors: string[];
|
|
209
|
+
};
|
|
210
|
+
artifacts: {
|
|
211
|
+
fullJson: ManifestArtifact | null;
|
|
212
|
+
fullMarkdown: ManifestArtifact | null;
|
|
213
|
+
};
|
|
214
|
+
layers: Record<string, number>;
|
|
215
|
+
kinds: Record<string, number>;
|
|
216
|
+
indexes: ManifestIndexes;
|
|
217
|
+
parts: PartMetadata[];
|
|
218
|
+
files: FileEntry[];
|
|
219
|
+
skipped: SkipRecord[];
|
|
220
|
+
warnings: string[];
|
|
221
|
+
}
|
|
222
|
+
interface LayerRule {
|
|
223
|
+
name: string;
|
|
224
|
+
match: string | RegExp | ((filePath: string) => boolean);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Which snapshot artifacts get written.
|
|
228
|
+
*
|
|
229
|
+
* Everything a developer opens is produced by default; each entry exists only so
|
|
230
|
+
* a single artifact can be switched off without disabling the rest. `monolith`
|
|
231
|
+
* is the one exception, because it duplicates the entire payload: it follows
|
|
232
|
+
* `includeMonolith` (off by default) unless set here explicitly.
|
|
233
|
+
*/
|
|
234
|
+
interface ArtifactSelection {
|
|
235
|
+
/** `README.md` — the orientation page for humans and models. */
|
|
236
|
+
readme?: boolean;
|
|
237
|
+
/** `manifest.json` — the registry `validate` and `report` read. */
|
|
238
|
+
manifest?: boolean;
|
|
239
|
+
/** `parts/part-XXX.md` — the partitioned code payload. */
|
|
240
|
+
parts?: boolean;
|
|
241
|
+
/** `reports/navigation.md` — file-to-part lookup index. */
|
|
242
|
+
navigation?: boolean;
|
|
243
|
+
/** `reports/project-tree.md` — ASCII repository tree. */
|
|
244
|
+
projectTree?: boolean;
|
|
245
|
+
/** `reports/audit.md` — security, layer, kind and skip findings. */
|
|
246
|
+
audit?: boolean;
|
|
247
|
+
/** `full-codebase.{json,md}` — one file per format, the whole payload again. */
|
|
248
|
+
monolith?: boolean;
|
|
249
|
+
}
|
|
250
|
+
/** Every artifact decision made concrete. */
|
|
251
|
+
type ResolvedArtifacts = Required<ArtifactSelection>;
|
|
252
|
+
interface KindRule {
|
|
253
|
+
kind: string;
|
|
254
|
+
match: string | RegExp | ((filePath: string) => boolean);
|
|
255
|
+
}
|
|
256
|
+
/** A byte budget: either raw bytes or a human-readable size such as `"120k"`. */
|
|
257
|
+
type ByteSize = number | string;
|
|
258
|
+
/** Per-layer part budgets. Unlisted layers fall back to `default`. */
|
|
259
|
+
interface BudgetConfig {
|
|
260
|
+
default?: ByteSize;
|
|
261
|
+
byLayer?: Record<string, ByteSize>;
|
|
262
|
+
}
|
|
263
|
+
/** Every budget resolved to concrete byte counts. */
|
|
264
|
+
interface ResolvedBudgets {
|
|
265
|
+
default: number;
|
|
266
|
+
byLayer: Record<string, number>;
|
|
267
|
+
}
|
|
268
|
+
/** Lifecycle hooks. Not serialized into the manifest (functions are not data). */
|
|
269
|
+
interface HooksConfig {
|
|
270
|
+
/** Runs once after a successful generate, with the manifest that was written. */
|
|
271
|
+
afterGenerate?: (manifest: Manifest) => void;
|
|
272
|
+
}
|
|
273
|
+
interface SnapshotConfig {
|
|
274
|
+
cwd?: string;
|
|
275
|
+
branch?: string;
|
|
276
|
+
outDir?: string;
|
|
277
|
+
tmpDir?: string;
|
|
278
|
+
/** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
|
|
279
|
+
maxPartBytes?: ByteSize;
|
|
280
|
+
largeFileBytes?: ByteSize;
|
|
281
|
+
previewHeadLines?: number;
|
|
282
|
+
previewTailLines?: number;
|
|
283
|
+
includeMonolith?: boolean;
|
|
284
|
+
maxMonolithBytes?: ByteSize | null;
|
|
285
|
+
/** Opt in to emitting `.env`/`*.pem`/`.npmrc`/credential files. Default false. */
|
|
286
|
+
includeSensitiveFiles?: boolean;
|
|
287
|
+
/** Allow-list of globs. When non-empty, only matching files are included. */
|
|
288
|
+
include?: string[];
|
|
289
|
+
/** Switch individual snapshot artifacts off. Everything is on by default. */
|
|
290
|
+
artifacts?: ArtifactSelection;
|
|
291
|
+
/**
|
|
292
|
+
* How user `layers`/`kinds` interact with the detected preset (default
|
|
293
|
+
* `"merge"`): user rules are evaluated first, then the preset fills the gaps.
|
|
294
|
+
* `"replace"` discards the preset entirely.
|
|
295
|
+
*/
|
|
296
|
+
layersMode?: "merge" | "replace";
|
|
297
|
+
/** Per-layer part budgets. */
|
|
298
|
+
budgets?: BudgetConfig;
|
|
299
|
+
hooks?: HooksConfig;
|
|
300
|
+
layers?: LayerRule[];
|
|
301
|
+
kinds?: KindRule[];
|
|
302
|
+
excludePatterns?: string[];
|
|
303
|
+
secrets?: {
|
|
304
|
+
customPatterns?: SecretRuleInput[];
|
|
305
|
+
disableDefaultPatterns?: boolean;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
interface ResolvedSnapshotConfig {
|
|
309
|
+
cwd: string;
|
|
310
|
+
branch: string;
|
|
311
|
+
outDir: string;
|
|
312
|
+
tmpDir: string;
|
|
313
|
+
maxPartBytes: number;
|
|
314
|
+
largeFileBytes: number;
|
|
315
|
+
previewHeadLines: number;
|
|
316
|
+
previewTailLines: number;
|
|
317
|
+
includeMonolith: boolean;
|
|
318
|
+
maxMonolithBytes: number | null;
|
|
319
|
+
includeSensitiveFiles: boolean;
|
|
320
|
+
/** Empty means "no allow-list"; every discovered file is a candidate. */
|
|
321
|
+
include: string[];
|
|
322
|
+
artifacts: ResolvedArtifacts;
|
|
323
|
+
layersMode: "merge" | "replace";
|
|
324
|
+
budgets: ResolvedBudgets;
|
|
325
|
+
hooks: HooksConfig;
|
|
326
|
+
layers: LayerRule[];
|
|
327
|
+
kinds: KindRule[];
|
|
328
|
+
excludePatterns: string[];
|
|
329
|
+
secretRules: SecretRule[];
|
|
330
|
+
}
|
|
331
|
+
interface CliOptions {
|
|
332
|
+
mode: Mode;
|
|
333
|
+
format: OutputFormat;
|
|
334
|
+
dryRun: boolean;
|
|
335
|
+
cwd?: string;
|
|
336
|
+
outDir?: string;
|
|
337
|
+
includeMonolith?: boolean;
|
|
338
|
+
includeSensitiveFiles?: boolean;
|
|
339
|
+
maxMonolithBytes?: number | null;
|
|
340
|
+
maxPartBytes?: number;
|
|
341
|
+
largeFileBytes?: number;
|
|
342
|
+
/** Artifact names to suppress for this invocation (`--skip-artifacts`). */
|
|
343
|
+
skipArtifacts?: string[];
|
|
344
|
+
}
|
|
345
|
+
interface ValidationDrift {
|
|
346
|
+
missingInSnapshot: string[];
|
|
347
|
+
missingOnDisk: string[];
|
|
348
|
+
hashMismatches: Array<{
|
|
349
|
+
path: string;
|
|
350
|
+
diskHash: string;
|
|
351
|
+
snapshotHash: string;
|
|
352
|
+
}>;
|
|
353
|
+
/** Emitted `parts/*.md` that are missing or no longer match their recorded hash. */
|
|
354
|
+
partIssues: Array<{
|
|
355
|
+
part: string;
|
|
356
|
+
issue: string;
|
|
357
|
+
}>;
|
|
358
|
+
/** `counts`/`totals`/`budgets` that disagree with the rest of the manifest. */
|
|
359
|
+
totalsMismatches: string[];
|
|
360
|
+
/** Effective-configuration differences between the manifest and the current run. */
|
|
361
|
+
configMismatches: string[];
|
|
362
|
+
}
|
|
363
|
+
interface ValidationResult {
|
|
364
|
+
valid: boolean;
|
|
365
|
+
manifestPresent: boolean;
|
|
366
|
+
drift: ValidationDrift;
|
|
367
|
+
errors: string[];
|
|
368
|
+
/** Non-fatal observations (e.g. branch moved on). */
|
|
369
|
+
warnings: string[];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
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, ValidationDrift as q };
|
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
## Classification: layers and kinds
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
export default {
|
|
45
|
+
layersMode: "merge", // default: your rules first, then the preset's
|
|
46
|
+
layers: [
|
|
47
|
+
{ name: "billing", match: "src/features/billing/**" },
|
|
48
|
+
{ name: "legacy", match: (p) => p.includes(".legacy.") },
|
|
49
|
+
],
|
|
50
|
+
kinds: [
|
|
51
|
+
{ kind: "route", match: /(^|\/)route\.tsx?$/ },
|
|
52
|
+
{ kind: "hook", match: "src/hooks/**" },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Rules are evaluated in order, **first match wins**, with an `other` fallback.
|
|
58
|
+
`match` may be a string, a `RegExp`, or a predicate function.
|
|
59
|
+
|
|
60
|
+
`layersMode: "merge"` (the default) keeps the detected preset's rules and lets
|
|
61
|
+
yours win by being evaluated first. `"replace"` discards the preset entirely.
|
|
62
|
+
|
|
63
|
+
Presets are auto-detected: **nextjs** → **vite** → **node** → **default**.
|
|
64
|
+
|
|
65
|
+
## Sizing: byte budgets
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
export default {
|
|
69
|
+
maxPartBytes: "120k", // hard ceiling per part; default 300 KB, min 4096 B
|
|
70
|
+
largeFileBytes: "200k", // above this a file gets a head/tail preview
|
|
71
|
+
previewHeadLines: 80,
|
|
72
|
+
previewTailLines: 40,
|
|
73
|
+
|
|
74
|
+
budgets: { // optional per-layer overrides
|
|
75
|
+
default: "120k",
|
|
76
|
+
byLayer: { docs: 40_000, core: 300_000 },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Budgets accept raw bytes (`122880`) or human sizes (`"120k"`, `"1.5mb"`, `"2 GB"`).
|
|
82
|
+
Anything unparseable is an error rather than a silent fallback to the default.
|
|
83
|
+
|
|
84
|
+
Files under different budgets are partitioned separately and the resulting parts
|
|
85
|
+
are re-numbered, so filenames and `Previous:`/`Next:` links stay correct.
|
|
86
|
+
|
|
87
|
+
## Artifacts: what gets written
|
|
88
|
+
|
|
89
|
+
Everything is on by default. Each artifact can be switched off independently —
|
|
90
|
+
`artifacts` in config, or `--skip-artifacts` / `--only-artifacts` on the CLI.
|
|
91
|
+
|
|
92
|
+
| Artifact | Default | File |
|
|
93
|
+
| :--- | :--- | :--- |
|
|
94
|
+
| `readme` | on | `README.md` |
|
|
95
|
+
| `manifest` | on | `manifest.json` — **off breaks `validate` and `report`** |
|
|
96
|
+
| `parts` | on | `parts/part-XXX.md` |
|
|
97
|
+
| `navigation` | on | `reports/navigation.md` — dangles if `parts` is off |
|
|
98
|
+
| `projectTree` | on | `reports/project-tree.md` |
|
|
99
|
+
| `audit` | on | `reports/audit.md` |
|
|
100
|
+
| `monolith` | off | `full-codebase.{json,md}` |
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
export default {
|
|
104
|
+
artifacts: { parts: false, navigation: false }, // reports-only pass
|
|
105
|
+
includeMonolith: false,
|
|
106
|
+
maxMonolithBytes: null,
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Unknown artifact names are rejected, not ignored. The selection is recorded in
|
|
111
|
+
`manifest.config.artifacts`, so changing it later shows up as config drift.
|
|
112
|
+
|
|
113
|
+
The monolith is built from the **pre-partition** contents, so it holds the whole
|
|
114
|
+
file even when a part had to truncate it.
|
|
115
|
+
|
|
116
|
+
## Secrets: redaction rules
|
|
117
|
+
|
|
118
|
+
21 built-in rules cover PEM keys, JWT, AWS, GitHub, Slack, Stripe, Google,
|
|
119
|
+
OpenAI/Anthropic, npm, PyPI, SendGrid, Twilio, DigitalOcean, Azure, database
|
|
120
|
+
URIs, basic-auth URLs and `.env`-style assignments.
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
export default {
|
|
124
|
+
secrets: {
|
|
125
|
+
customPatterns: [
|
|
126
|
+
{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ },
|
|
127
|
+
{ rule: "from-json", pattern: "ACME_[a-z0-9]{10}" }, // strings allowed
|
|
128
|
+
],
|
|
129
|
+
disableDefaultPatterns: false,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Only the matched span is replaced; the rest of the line survives. Multi-line PEM
|
|
135
|
+
blocks are masked as a block, including ones cut off before their `END` marker.
|
|
136
|
+
No excerpt of a matched line is ever written to any artifact.
|
|
137
|
+
|
|
138
|
+
## Output layout
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
export default {
|
|
142
|
+
outDir: "snapshot",
|
|
143
|
+
tmpDir: "snapshot.tmp",
|
|
144
|
+
branch: "main", // fallback when git cannot report one
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`outDir` may not be the working directory, an ancestor, or the filesystem root,
|
|
149
|
+
and `outDir`/`tmpDir` may not contain each other. Writes are atomic: the previous
|
|
150
|
+
snapshot survives until the new one is fully in place.
|
|
151
|
+
|
|
152
|
+
## Hooks
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
export default {
|
|
156
|
+
hooks: {
|
|
157
|
+
afterGenerate: (manifest) => {
|
|
158
|
+
// runs after a successful generate (and after a dry run)
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A throwing hook becomes a warning on the manifest — it never loses a snapshot
|
|
165
|
+
that was already written.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
# Commands
|
|
170
|
+
|
|
171
|
+
| Command | Purpose |
|
|
172
|
+
| :--- | :--- |
|
|
173
|
+
| `generate` *(default)* | Write the snapshot |
|
|
174
|
+
| `validate`, `check` | Re-hash sources and parts, compare config, detect drift |
|
|
175
|
+
| `report` | Print metrics, layer distribution and drift status |
|
|
176
|
+
| `explain <path>` | Show how one path would be treated |
|
|
177
|
+
| `diff <a> <b>` | Compare two snapshots |
|
|
178
|
+
| `init` | Write a starter config |
|
|
179
|
+
|
|
180
|
+
## `explain`
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
$ snap explain src/app/page.tsx
|
|
184
|
+
|
|
185
|
+
src/app/page.tsx
|
|
186
|
+
|
|
187
|
+
included yes
|
|
188
|
+
layer app (rule (p) => p.startsWith("src/app/") || ...)
|
|
189
|
+
kind component (rule (p) => { const base = p.split("/").pop() ...)
|
|
190
|
+
language tsx
|
|
191
|
+
size 48 B, 1 line(s)
|
|
192
|
+
part part-001.md
|
|
193
|
+
inclusion full
|
|
194
|
+
secrets none
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Excluded paths say why:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
$ snap explain .env
|
|
201
|
+
|
|
202
|
+
.env
|
|
203
|
+
|
|
204
|
+
included no
|
|
205
|
+
reason sensitive-file
|
|
206
|
+
detail credential-bearing file; enable includeSensitiveFiles to emit it
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Exits `0` when the path is included, `1` when it is not. `--format json` emits the
|
|
210
|
+
same data as an object.
|
|
211
|
+
|
|
212
|
+
## `diff`
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
$ snap diff old-snapshot new-snapshot
|
|
216
|
+
|
|
217
|
+
before 2026-11-03T10:00:00.000Z 812 file(s), 4 part(s)
|
|
218
|
+
after 2026-11-03T11:30:00.000Z 820 file(s), 4 part(s)
|
|
219
|
+
|
|
220
|
+
+ 9 added
|
|
221
|
+
- 1 removed
|
|
222
|
+
~ 3 changed
|
|
223
|
+
= 808 unchanged
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Both arguments may be snapshot directories or paths to a `manifest.json`.
|
|
227
|
+
|
|
228
|
+
## `--watch`
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npx @ankurhalder/codebase-snapshot generate --watch
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Re-generates on every change, ignoring writes into the output and temporary
|
|
235
|
+
directories (including the atomic-swap backup) so it cannot trigger itself.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
# Drift
|
|
240
|
+
|
|
241
|
+
`validate` compares five things: source hashes on disk, the emitted parts (size
|
|
242
|
+
and SHA-256), the manifest's own bookkeeping, the recorded configuration
|
|
243
|
+
(budgets, includes, excludes, artifacts, layers mode), and git state. Changing a
|
|
244
|
+
config value without re-generating is reported as drift, not silently accepted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankurhalder/codebase-snapshot",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.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
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"typescript": "^5.0.0"
|
|
84
|
+
},
|
|
85
|
+
"overrides": {
|
|
86
|
+
"esbuild": "^0.28.1"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@types/node": "^22.0.0",
|
|
90
|
+
"tsup": "^8.3.0"
|
|
91
|
+
},
|
|
92
|
+
"engines": {
|
|
93
|
+
"node": ">=18.0.0"
|
|
94
|
+
}
|
|
95
|
+
}
|