codebase-snapshot 0.9.8 → 0.9.12

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,15 +1,17 @@
1
- import * as crypto from 'crypto';
1
+ import * as crypto2 from 'crypto';
2
2
  import { createHash } from 'crypto';
3
3
  import { AsyncLocalStorage } from 'async_hooks';
4
- import * as fs12 from 'fs';
4
+ import * as fs14 from 'fs';
5
5
  import * as os from 'os';
6
- import * as path13 from 'path';
6
+ import * as path16 from 'path';
7
7
  import ts from 'typescript';
8
8
  import { createRequire } from 'module';
9
- import { pathToFileURL } from 'url';
9
+ import { pathToFileURL, fileURLToPath } from 'url';
10
10
  import * as childProcess from 'child_process';
11
+ import { execFileSync } from 'child_process';
11
12
  import * as https from 'https';
12
13
  import * as readline from 'readline';
14
+ import { Worker } from 'worker_threads';
13
15
 
14
16
  // src/index.ts
15
17
 
@@ -125,6 +127,59 @@ var cfamilyProfile = {
125
127
  }
126
128
  };
127
129
 
130
+ // src/ast/profiles/dart.ts
131
+ var IMPORT = /^import\s+['"]([^'"]+)['"]/;
132
+ var PART = /^part(?:\s+of)?\s+['"]?([\w./]+)['"]?/;
133
+ var TYPE = /^(?:abstract\s+|base\s+|final\s+|sealed\s+|interface\s+)*(class|mixin|enum|extension|typedef)\s+([A-Za-z_]\w*)/;
134
+ var FUNC = /^(?:Future<[^>]*>|Stream<[^>]*>|void|bool|int|double|num|String|dynamic|var|[A-Z][\w<>, ?]*)\s+([A-Za-z_]\w*)\s*[(<]/;
135
+ var VARIABLE = /^(?:late\s+)?(?:final|const|var)\s+(?:[\w<>?,. ]+?\s+)?([A-Za-z_]\w*)\s*=/;
136
+ var dartProfile = {
137
+ id: "dart",
138
+ extensions: [".dart"],
139
+ summarize(_path, content) {
140
+ const summary = emptySummary();
141
+ eachLine(content, (line) => {
142
+ const trimmed = line.trim();
143
+ if (!trimmed || trimmed.startsWith("//")) return;
144
+ const imported = IMPORT.exec(trimmed);
145
+ if (imported) {
146
+ summary.imports.push(imported[1]);
147
+ return;
148
+ }
149
+ if (/^part\b/.test(trimmed)) {
150
+ const part = PART.exec(trimmed);
151
+ summary.directives.push(part ? `part ${part[1]}` : "part");
152
+ return;
153
+ }
154
+ if (!isTopLevel(line)) return;
155
+ const type = TYPE.exec(trimmed);
156
+ if (type) {
157
+ const [, keyword, name] = type;
158
+ if (keyword === "mixin" || keyword === "extension") summary.types.push(name);
159
+ else if (keyword === "enum") summary.enums.push(name);
160
+ else if (keyword === "typedef") summary.types.push(name);
161
+ else summary.classes.push(name);
162
+ if (isPublicName(name)) summary.exports.push(name);
163
+ return;
164
+ }
165
+ const func = FUNC.exec(trimmed);
166
+ if (func) {
167
+ const name = func[1];
168
+ summary.functions.push(name);
169
+ if (isPublicName(name)) summary.exports.push(name);
170
+ return;
171
+ }
172
+ const variable = VARIABLE.exec(trimmed);
173
+ if (variable) {
174
+ const name = variable[1];
175
+ summary.variables.push(name);
176
+ if (isPublicName(name)) summary.exports.push(name);
177
+ }
178
+ });
179
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
180
+ }
181
+ };
182
+
128
183
  // src/ast/profiles/dotnet.ts
129
184
  var USING2 = /^using\s+([\w.]+)\s*;/;
130
185
  var TYPE_DECL2 = /^\s*(?:public\s+|internal\s+|private\s+|protected\s+|sealed\s+|abstract\s+|static\s+|partial\s+|readonly\s+)*(class|interface|struct|record|enum)\s+([A-Za-z_]\w*)/;
@@ -165,8 +220,8 @@ var dotnetProfile = {
165
220
  };
166
221
 
167
222
  // src/ast/profiles/go.ts
168
- var FUNC = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
169
- var TYPE = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
223
+ var FUNC2 = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
224
+ var TYPE2 = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
170
225
  var VAR_CONST = /^(?:var|const)\s+([A-Za-z_]\w*)/;
171
226
  var IMPORT_SINGLE = /^import\s+(?:[\w.]+\s+)?"([^"]+)"/;
172
227
  var IMPORT_OPEN = /^import\s*\(/;
@@ -199,14 +254,14 @@ var goProfile = {
199
254
  return;
200
255
  }
201
256
  if (!isTopLevel(line)) return;
202
- const method = FUNC.exec(line);
257
+ const method = FUNC2.exec(line);
203
258
  if (method) {
204
259
  const name = method[1];
205
260
  summary.functions.push(name);
206
261
  if (isCapitalizedName(name)) summary.exports.push(name);
207
262
  return;
208
263
  }
209
- const declared = TYPE.exec(line);
264
+ const declared = TYPE2.exec(line);
210
265
  if (declared) {
211
266
  const name = declared[1];
212
267
  const body = declared[2].trim();
@@ -226,8 +281,54 @@ var goProfile = {
226
281
  }
227
282
  };
228
283
 
284
+ // src/ast/profiles/graphql.ts
285
+ var DEFINITION = /^(?:extend\s+)?(type|interface|input|enum|union|scalar|schema)\s+([A-Za-z_]\w*)/;
286
+ var OPERATION = /^(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/;
287
+ var DIRECTIVE = /^directive\s+@([A-Za-z_]\w*)/;
288
+ var IMPORT2 = /^#import\s+"([^"]+)"/;
289
+ var graphqlProfile = {
290
+ id: "graphql",
291
+ extensions: [".graphql", ".gql"],
292
+ summarize(_path, content) {
293
+ const summary = emptySummary();
294
+ eachLine(content, (line) => {
295
+ const trimmed = line.trim();
296
+ if (!trimmed || trimmed.startsWith("#") && !IMPORT2.test(trimmed)) return;
297
+ const imported = IMPORT2.exec(trimmed);
298
+ if (imported) {
299
+ summary.imports.push(imported[1]);
300
+ return;
301
+ }
302
+ const directive = DIRECTIVE.exec(trimmed);
303
+ if (directive) {
304
+ summary.functions.push(`@${directive[1]}`);
305
+ return;
306
+ }
307
+ const definition = DEFINITION.exec(trimmed);
308
+ if (definition) {
309
+ const [, keyword, name] = definition;
310
+ if (keyword === "interface") summary.interfaces.push(name);
311
+ else if (keyword === "enum") summary.enums.push(name);
312
+ else if (keyword === "input" || keyword === "scalar" || keyword === "union") {
313
+ summary.types.push(name);
314
+ } else summary.classes.push(name);
315
+ summary.exports.push(name);
316
+ return;
317
+ }
318
+ if (!isTopLevel(line)) return;
319
+ const operation = OPERATION.exec(trimmed);
320
+ if (operation) {
321
+ const name = operation[1];
322
+ summary.functions.push(name);
323
+ summary.routeHandlers.push(name);
324
+ }
325
+ });
326
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
327
+ }
328
+ };
329
+
229
330
  // src/ast/profiles/jvm.ts
230
- var IMPORT = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
331
+ var IMPORT3 = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
231
332
  var TYPE_DECL3 = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:final\s+|abstract\s+|open\s+|sealed\s+|data\s+|static\s+|partial\s+)*(class|interface|enum|record|object|trait)\s+(?:class\s+)?([A-Za-z_]\w*)/;
232
333
  var KOTLIN_FUN = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+|override\s+|suspend\s+|inline\s+|open\s+|operator\s+)*fun\s+(?:<[^>]*>\s*)?(?:[\w.]+\.)?([A-Za-z_]\w*)/;
233
334
  var SCALA_DEF = /^\s*def\s+([A-Za-z_]\w*)/;
@@ -235,9 +336,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
235
336
  var jvmProfile = {
236
337
  id: "jvm",
237
338
  extensions: [".java", ".kt", ".kts", ".scala"],
238
- summarize(path14, content) {
339
+ summarize(path17, content) {
239
340
  const summary = emptySummary();
240
- const lower = path14.toLowerCase();
341
+ const lower = path17.toLowerCase();
241
342
  const isJava = lower.endsWith(".java");
242
343
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
243
344
  eachLine(content, (line) => {
@@ -245,7 +346,7 @@ var jvmProfile = {
245
346
  if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
246
347
  return;
247
348
  }
248
- const imported = IMPORT.exec(trimmed);
349
+ const imported = IMPORT3.exec(trimmed);
249
350
  if (imported) {
250
351
  summary.imports.push(imported[1]);
251
352
  return;
@@ -326,8 +427,50 @@ var phpProfile = {
326
427
  }
327
428
  };
328
429
 
430
+ // src/ast/profiles/protobuf.ts
431
+ var DECLARATION = /^(?:message|enum|service|extend)\s+([A-Za-z_]\w*)/;
432
+ var RPC = /^\s*rpc\s+([A-Za-z_]\w*)/;
433
+ var IMPORT4 = /^\s*import\s+(?:public\s+|weak\s+)?"([^"]+)"/;
434
+ var PACKAGE = /^\s*package\s+([\w.]+)\s*;/;
435
+ var protobufProfile = {
436
+ id: "protobuf",
437
+ extensions: [".proto"],
438
+ summarize(_path, content) {
439
+ const summary = emptySummary();
440
+ eachLine(content, (line) => {
441
+ const trimmed = line.trim();
442
+ if (!trimmed || trimmed.startsWith("//")) return;
443
+ const imported = IMPORT4.exec(line);
444
+ if (imported) {
445
+ summary.imports.push(imported[1]);
446
+ return;
447
+ }
448
+ const pkg = PACKAGE.exec(line);
449
+ if (pkg) {
450
+ summary.directives.push(`package ${pkg[1]}`);
451
+ return;
452
+ }
453
+ const rpc = RPC.exec(line);
454
+ if (rpc) {
455
+ const name2 = rpc[1];
456
+ summary.functions.push(name2);
457
+ return;
458
+ }
459
+ const declaration = DECLARATION.exec(trimmed);
460
+ if (!declaration) return;
461
+ const name = declaration[1];
462
+ const keyword = /^\w+/.exec(trimmed)?.[0] ?? "";
463
+ if (keyword === "service") summary.interfaces.push(name);
464
+ else if (keyword === "enum") summary.enums.push(name);
465
+ else summary.classes.push(name);
466
+ summary.exports.push(name);
467
+ });
468
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
469
+ }
470
+ };
471
+
329
472
  // src/ast/profiles/python.ts
330
- var IMPORT2 = /^import\s+(.+)$/;
473
+ var IMPORT5 = /^import\s+(.+)$/;
331
474
  var FROM_IMPORT = /^from\s+([\w.]+)\s+import\s+(.+)$/;
332
475
  var CLASS = /^class\s+([A-Za-z_]\w*)/;
333
476
  var DEF = /^(?:async\s+)?def\s+([A-Za-z_]\w*)/;
@@ -353,7 +496,7 @@ var pythonProfile = {
353
496
  routePending = false;
354
497
  return;
355
498
  }
356
- const imported = IMPORT2.exec(trimmed);
499
+ const imported = IMPORT5.exec(trimmed);
357
500
  if (imported) {
358
501
  for (const part of imported[1].split(",")) {
359
502
  const name = part.trim().split(/\s+as\s+/)[0].trim();
@@ -437,7 +580,7 @@ var FN = /^(pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(
437
580
  var STRUCT = /^(pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_]\w*)/;
438
581
  var TRAIT = /^(pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/;
439
582
  var ENUM = /^(pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_]\w*)/;
440
- var TYPE2 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
583
+ var TYPE3 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
441
584
  var MOD = /^(pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)/;
442
585
  var IMPL = /^impl(?:<[^>]*>)?\s+(?:[\w:<>]+\s+for\s+)?([A-Za-z_]\w*)/;
443
586
  var rustProfile = {
@@ -479,7 +622,7 @@ var rustProfile = {
479
622
  if (isPublic) summary.exports.push(enumeration[2]);
480
623
  return;
481
624
  }
482
- const alias = TYPE2.exec(line);
625
+ const alias = TYPE3.exec(line);
483
626
  if (alias) {
484
627
  summary.types.push(alias[2]);
485
628
  if (isPublic) summary.exports.push(alias[2]);
@@ -506,6 +649,16 @@ var ROUTE_HANDLER_NAMES = /* @__PURE__ */ new Set([
506
649
  "HEAD",
507
650
  "OPTIONS"
508
651
  ]);
652
+ function bindingNames(name, into) {
653
+ if (ts.isIdentifier(name)) {
654
+ into.push(name.text);
655
+ return;
656
+ }
657
+ for (const element of name.elements) {
658
+ if (ts.isOmittedExpression(element)) continue;
659
+ bindingNames(element.name, into);
660
+ }
661
+ }
509
662
  function summarizeTypeScriptSource(relativePath, content) {
510
663
  const summary = {
511
664
  directives: [],
@@ -606,12 +759,15 @@ function summarizeTypeScriptSource(relativePath, content) {
606
759
  }
607
760
  if (node.exportClause && ts.isNamedExports(node.exportClause)) {
608
761
  for (const element of node.exportClause.elements) {
609
- summary.exports.push(element.name.text);
762
+ const exported = element.name.text;
763
+ summary.exports.push(exported);
764
+ if (ROUTE_HANDLER_NAMES.has(exported)) summary.routeHandlers.push(exported);
610
765
  }
611
766
  }
612
767
  }
613
768
  if (ts.isExportAssignment(node)) {
614
769
  summary.exports.push("default");
770
+ if (rendersJsx(node.expression)) summary.reactComponents.push("default");
615
771
  }
616
772
  if (ts.isFunctionDeclaration(node)) {
617
773
  const exported = isExported(node);
@@ -649,16 +805,18 @@ function summarizeTypeScriptSource(relativePath, content) {
649
805
  if (ts.isVariableStatement(node)) {
650
806
  const exported = isExported(node);
651
807
  for (const declaration of node.declarationList.declarations) {
652
- if (!ts.isIdentifier(declaration.name)) continue;
653
- const name = declaration.name.text;
654
- summary.variables.push(name);
655
- if (exported) {
656
- summary.exports.push(name);
657
- if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
658
- }
659
- if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
660
- if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
661
- summary.reactComponents.push(name);
808
+ const bound = [];
809
+ bindingNames(declaration.name, bound);
810
+ for (const name of bound) {
811
+ summary.variables.push(name);
812
+ if (exported) {
813
+ summary.exports.push(name);
814
+ if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
815
+ }
816
+ if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
817
+ if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
818
+ summary.reactComponents.push(name);
819
+ }
662
820
  }
663
821
  }
664
822
  }
@@ -690,16 +848,16 @@ function summarizeTypeScriptSource(relativePath, content) {
690
848
  // src/ast/profiles/sfc.ts
691
849
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
692
850
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
693
- function componentName(path14) {
694
- const base = path14.split("/").pop() ?? path14;
851
+ function componentName(path17) {
852
+ const base = path17.split("/").pop() ?? path17;
695
853
  return base.replace(/\.[^.]+$/, "");
696
854
  }
697
855
  var sfcProfile = {
698
856
  id: "sfc",
699
857
  extensions: [".vue", ".svelte", ".astro"],
700
- summarize(path14, content) {
858
+ summarize(path17, content) {
701
859
  let script = "";
702
- if (path14.toLowerCase().endsWith(".astro")) {
860
+ if (path17.toLowerCase().endsWith(".astro")) {
703
861
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
704
862
  if (frontmatter) script += `${frontmatter[1]}
705
863
  `;
@@ -708,7 +866,7 @@ var sfcProfile = {
708
866
  script += `${match[1]}
709
867
  `;
710
868
  }
711
- const name = componentName(path14);
869
+ const name = componentName(path17);
712
870
  if (!script.trim()) {
713
871
  const summary2 = emptySummary();
714
872
  if (name) {
@@ -720,7 +878,158 @@ var sfcProfile = {
720
878
  const summary = summarizeTypeScriptSource(`${name}.ts`, script);
721
879
  summary.reactComponents.push(name);
722
880
  summary.exports.push(name);
723
- return { summary, quality: "ast" };
881
+ return { summary: finalizeSummary(summary), quality: "ast" };
882
+ }
883
+ };
884
+
885
+ // src/ast/profiles/sql.ts
886
+ var CREATE = /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:TEMP(?:ORARY)?\s+)?(TABLE|VIEW|MATERIALIZED\s+VIEW|FUNCTION|PROCEDURE|INDEX|TYPE|SCHEMA|TRIGGER|SEQUENCE)\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_"][\w."]*)/i;
887
+ var ALTER = /^\s*ALTER\s+(TABLE|VIEW)\s+(?:IF\s+EXISTS\s+)?([A-Za-z_"][\w."]*)/i;
888
+ function bareName(identifier) {
889
+ const last = identifier.split(".").pop() ?? identifier;
890
+ return last.replace(/["`[\]]/g, "");
891
+ }
892
+ var sqlProfile = {
893
+ id: "sql",
894
+ extensions: [".sql"],
895
+ summarize(_path, content) {
896
+ const summary = emptySummary();
897
+ const seen = /* @__PURE__ */ new Set();
898
+ const declare = (kind, identifier) => {
899
+ const name = bareName(identifier);
900
+ if (!name || seen.has(`${kind}:${name}`)) return;
901
+ seen.add(`${kind}:${name}`);
902
+ const normalized = kind.toUpperCase().replace(/\s+/g, " ");
903
+ if (normalized === "TABLE" || normalized.endsWith("VIEW")) summary.classes.push(name);
904
+ else if (normalized === "TYPE" || normalized === "SCHEMA" || normalized === "SEQUENCE") {
905
+ summary.types.push(name);
906
+ } else summary.functions.push(name);
907
+ summary.exports.push(name);
908
+ };
909
+ eachLine(content, (line) => {
910
+ const trimmed = line.trim();
911
+ if (!trimmed || trimmed.startsWith("--")) return;
912
+ const created = CREATE.exec(trimmed);
913
+ if (created) {
914
+ declare(String(created[1]), String(created[2]));
915
+ return;
916
+ }
917
+ const altered = ALTER.exec(trimmed);
918
+ if (altered) declare(String(altered[1]), String(altered[2]));
919
+ });
920
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
921
+ }
922
+ };
923
+
924
+ // src/ast/profiles/swift.ts
925
+ var MODIFIERS = "(?:@\\w+(?:\\([^)]*\\))?\\s+|(?:public|open|internal|fileprivate|private|final|static|mutating|nonmutating|override|required|convenience|lazy|weak|unowned)\\s+)*";
926
+ var TYPE4 = new RegExp(`^${MODIFIERS}(class|struct|enum|protocol|actor|extension)\\s+([A-Za-z_]\\w*)`);
927
+ var FUNC3 = new RegExp(`^${MODIFIERS}func\\s+([A-Za-z_]\\w*)`);
928
+ var PROPERTY = new RegExp(`^${MODIFIERS}(?:let|var)\\s+([A-Za-z_]\\w*)`);
929
+ var IMPORT6 = /^import\s+([A-Za-z_][\w.]*)/;
930
+ var swiftProfile = {
931
+ id: "swift",
932
+ extensions: [".swift"],
933
+ summarize(_path, content) {
934
+ const summary = emptySummary();
935
+ eachLine(content, (line) => {
936
+ const trimmed = line.trim();
937
+ if (!trimmed || trimmed.startsWith("//")) return;
938
+ const imported = IMPORT6.exec(trimmed);
939
+ if (imported) {
940
+ summary.imports.push(imported[1]);
941
+ return;
942
+ }
943
+ if (!isTopLevel(line)) return;
944
+ const exported = /\b(?:public|open)\b/.test(trimmed);
945
+ const type = TYPE4.exec(trimmed);
946
+ if (type) {
947
+ const [, keyword, name] = type;
948
+ if (keyword === "protocol") summary.interfaces.push(name);
949
+ else if (keyword === "enum") summary.enums.push(name);
950
+ else if (keyword === "extension") summary.types.push(name);
951
+ else summary.classes.push(name);
952
+ if (exported) summary.exports.push(name);
953
+ return;
954
+ }
955
+ const func = FUNC3.exec(trimmed);
956
+ if (func) {
957
+ const name = func[1];
958
+ summary.functions.push(name);
959
+ if (exported) summary.exports.push(name);
960
+ return;
961
+ }
962
+ const property = PROPERTY.exec(trimmed);
963
+ if (property) {
964
+ const name = property[1];
965
+ summary.variables.push(name);
966
+ if (exported && isPublicName(name)) summary.exports.push(name);
967
+ }
968
+ });
969
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
970
+ }
971
+ };
972
+
973
+ // src/ast/profiles/terraform.ts
974
+ var BLOCK = /^\s*(resource|data|module|provider|variable|output|locals|terraform)\b\s*("([^"]+)")?\s*("([^"]+)")?/;
975
+ var TERRAFORM_VERSION = /^\s*required_version\s*=\s*"([^"]+)"/;
976
+ var terraformProfile = {
977
+ id: "terraform",
978
+ extensions: [".tf", ".tfvars"],
979
+ summarize(_path, content) {
980
+ const summary = emptySummary();
981
+ eachLine(content, (line) => {
982
+ const trimmed = line.trim();
983
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//")) return;
984
+ const version = TERRAFORM_VERSION.exec(line);
985
+ if (version) {
986
+ summary.directives.push(`required_version ${version[1]}`);
987
+ return;
988
+ }
989
+ if (/^[A-Za-z_][\w-]*\s*=/.test(trimmed)) {
990
+ summary.variables.push(/^([A-Za-z_][\w-]*)/.exec(trimmed)?.[1] ?? trimmed);
991
+ return;
992
+ }
993
+ const block = BLOCK.exec(line);
994
+ if (!block) return;
995
+ const keyword = block[1];
996
+ const first = block[3];
997
+ const second = block[5];
998
+ switch (keyword) {
999
+ case "resource":
1000
+ case "data": {
1001
+ if (!first || !second) return;
1002
+ const name = `${first}.${second}`;
1003
+ summary.classes.push(name);
1004
+ summary.exports.push(name);
1005
+ return;
1006
+ }
1007
+ case "module": {
1008
+ if (!first) return;
1009
+ summary.imports.push(first);
1010
+ return;
1011
+ }
1012
+ case "variable": {
1013
+ if (!first) return;
1014
+ summary.variables.push(first);
1015
+ return;
1016
+ }
1017
+ case "output": {
1018
+ if (!first) return;
1019
+ summary.exports.push(first);
1020
+ return;
1021
+ }
1022
+ case "provider": {
1023
+ if (!first) return;
1024
+ summary.imports.push(`provider:${first}`);
1025
+ return;
1026
+ }
1027
+ default: {
1028
+ summary.directives.push(keyword);
1029
+ }
1030
+ }
1031
+ });
1032
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
724
1033
  }
725
1034
  };
726
1035
 
@@ -734,7 +1043,13 @@ var LANGUAGE_PROFILES = [
734
1043
  phpProfile,
735
1044
  rubyProfile,
736
1045
  cfamilyProfile,
737
- sfcProfile
1046
+ sfcProfile,
1047
+ swiftProfile,
1048
+ dartProfile,
1049
+ sqlProfile,
1050
+ graphqlProfile,
1051
+ protobufProfile,
1052
+ terraformProfile
738
1053
  ];
739
1054
  var BY_EXTENSION = /* @__PURE__ */ new Map();
740
1055
  for (const profile of LANGUAGE_PROFILES) {
@@ -742,15 +1057,15 @@ for (const profile of LANGUAGE_PROFILES) {
742
1057
  BY_EXTENSION.set(extension.toLowerCase(), profile);
743
1058
  }
744
1059
  }
745
- function profileForPath(path14) {
746
- const lower = path14.toLowerCase();
1060
+ function profileForPath(path17) {
1061
+ const lower = path17.toLowerCase();
747
1062
  const dot = lower.lastIndexOf(".");
748
1063
  if (dot === -1) return null;
749
1064
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
750
1065
  }
751
- function summarizeWithProfile(path14, content) {
752
- const profile = profileForPath(path14);
753
- return profile ? profile.summarize(path14, content) : null;
1066
+ function summarizeWithProfile(path17, content) {
1067
+ const profile = profileForPath(path17);
1068
+ return profile ? profile.summarize(path17, content) : null;
754
1069
  }
755
1070
 
756
1071
  // src/ast/structured.ts
@@ -850,9 +1165,17 @@ function summarizeMarkdown(content) {
850
1165
  const frontmatterKeys = [];
851
1166
  const lines = content.replace(/^\uFEFF/, "").split("\n");
852
1167
  let inFrontmatter = false;
1168
+ let fence = null;
1169
+ let closesFrontmatter = false;
1170
+ for (let index = 1; index < lines.length; index++) {
1171
+ if (lines[index].trim() === "---") {
1172
+ closesFrontmatter = true;
1173
+ break;
1174
+ }
1175
+ }
853
1176
  for (let index = 0; index < lines.length; index++) {
854
1177
  const line = lines[index];
855
- if (index === 0 && line.trim() === "---") {
1178
+ if (index === 0 && line.trim() === "---" && closesFrontmatter) {
856
1179
  inFrontmatter = true;
857
1180
  continue;
858
1181
  }
@@ -865,7 +1188,20 @@ function summarizeMarkdown(content) {
865
1188
  if (match) frontmatterKeys.push(match[1]);
866
1189
  continue;
867
1190
  }
868
- const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
1191
+ const trimmed = line.trim();
1192
+ const fenceMatch = /^(`{3,}|~{3,})(.*)$/.exec(trimmed);
1193
+ if (fenceMatch) {
1194
+ const run = fenceMatch[1];
1195
+ const rest = fenceMatch[2].trim();
1196
+ if (fence === null) {
1197
+ fence = { marker: run[0], length: run.length };
1198
+ } else if (run[0] === fence.marker && run.length >= fence.length && rest === "") {
1199
+ fence = null;
1200
+ }
1201
+ continue;
1202
+ }
1203
+ if (fence !== null) continue;
1204
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(trimmed);
869
1205
  if (headingMatch) {
870
1206
  headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
871
1207
  }
@@ -875,6 +1211,52 @@ function summarizeMarkdown(content) {
875
1211
  frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
876
1212
  };
877
1213
  }
1214
+ function hasCollapsibleBody(node) {
1215
+ if (!("body" in node)) return false;
1216
+ const body = node.body;
1217
+ return body !== void 0 && ts.isBlock(body);
1218
+ }
1219
+ function isTsxFileName(fileName) {
1220
+ return fileName.toLowerCase().endsWith(".tsx");
1221
+ }
1222
+ function skeletonizeTypeScript(source, fileName = "file.ts") {
1223
+ const scriptKind = isTsxFileName(fileName) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
1224
+ const sourceFile = ts.createSourceFile(
1225
+ fileName,
1226
+ source,
1227
+ ts.ScriptTarget.Latest,
1228
+ true,
1229
+ scriptKind
1230
+ );
1231
+ const spans = [];
1232
+ const visit = (node) => {
1233
+ if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && hasCollapsibleBody(node)) {
1234
+ const body = node.body;
1235
+ const start = body.getStart(sourceFile);
1236
+ const end = body.getEnd();
1237
+ if (end > start) spans.push({ start, end });
1238
+ }
1239
+ ts.forEachChild(node, visit);
1240
+ };
1241
+ visit(sourceFile);
1242
+ if (spans.length === 0) return { text: source, collapsed: 0 };
1243
+ const outermost = [];
1244
+ for (const span of spans.sort((a, b) => a.start - b.start || b.end - a.end)) {
1245
+ const last = outermost[outermost.length - 1];
1246
+ if (last && span.start < last.end) continue;
1247
+ outermost.push(span);
1248
+ }
1249
+ let text2 = source;
1250
+ for (let index = outermost.length - 1; index >= 0; index -= 1) {
1251
+ const span = outermost[index];
1252
+ text2 = `${text2.slice(0, span.start)}{ /* ... */ }${text2.slice(span.end)}`;
1253
+ }
1254
+ return { text: text2, collapsed: outermost.length };
1255
+ }
1256
+ function isSkeletonizable(filePath) {
1257
+ const lower = filePath.toLowerCase();
1258
+ return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts");
1259
+ }
878
1260
  var MANIFEST_FILES = [
879
1261
  "package.json",
880
1262
  "pyproject.toml",
@@ -921,15 +1303,15 @@ var SKIPPED_WALK_DIRS = /* @__PURE__ */ new Set([
921
1303
  ]);
922
1304
  function readSmallFile(fullPath) {
923
1305
  try {
924
- const stats = fs12.statSync(fullPath);
1306
+ const stats = fs14.statSync(fullPath);
925
1307
  if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
926
- return fs12.readFileSync(fullPath, "utf8");
1308
+ return fs14.readFileSync(fullPath, "utf8");
927
1309
  } catch {
928
1310
  return null;
929
1311
  }
930
1312
  }
931
1313
  function readManifest(cwd, name) {
932
- return readSmallFile(path13.join(cwd, name));
1314
+ return readSmallFile(path16.join(cwd, name));
933
1315
  }
934
1316
  function dependenciesFromPackageJson(raw) {
935
1317
  const names = /* @__PURE__ */ new Set();
@@ -989,7 +1371,7 @@ function pnpmWorkspacePatterns(raw) {
989
1371
  function childDirectories(cwd, parent) {
990
1372
  let entries;
991
1373
  try {
992
- entries = fs12.readdirSync(path13.join(cwd, parent), { withFileTypes: true });
1374
+ entries = fs14.readdirSync(path16.join(cwd, parent), { withFileTypes: true });
993
1375
  } catch {
994
1376
  return [];
995
1377
  }
@@ -1024,13 +1406,13 @@ function workspacePackageDirs(cwd, patterns) {
1024
1406
  function scanWorkspacePackages(cwd, patterns) {
1025
1407
  const files = /* @__PURE__ */ new Set();
1026
1408
  const dependencies = /* @__PURE__ */ new Set();
1027
- for (const relative7 of workspacePackageDirs(cwd, patterns)) {
1028
- const full = path13.join(cwd, relative7);
1409
+ for (const relative8 of workspacePackageDirs(cwd, patterns)) {
1410
+ const full = path16.join(cwd, relative8);
1029
1411
  try {
1030
- for (const entry of fs12.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
1412
+ for (const entry of fs14.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
1031
1413
  } catch {
1032
1414
  }
1033
- const raw = readSmallFile(path13.join(full, "package.json"));
1415
+ const raw = readSmallFile(path16.join(full, "package.json"));
1034
1416
  if (raw !== null) {
1035
1417
  for (const name of dependenciesFromPackageJson(raw)) dependencies.add(name);
1036
1418
  }
@@ -1047,7 +1429,7 @@ function countMarkdownFiles(cwd) {
1047
1429
  directoriesSeen += 1;
1048
1430
  let entries;
1049
1431
  try {
1050
- entries = fs12.readdirSync(current.dir, { withFileTypes: true });
1432
+ entries = fs14.readdirSync(current.dir, { withFileTypes: true });
1051
1433
  } catch {
1052
1434
  continue;
1053
1435
  }
@@ -1058,7 +1440,7 @@ function countMarkdownFiles(cwd) {
1058
1440
  }
1059
1441
  if (!entry.isDirectory() || current.depth >= 2) continue;
1060
1442
  if (entry.name.startsWith(".") || SKIPPED_WALK_DIRS.has(entry.name)) continue;
1061
- queue.push({ dir: path13.join(current.dir, entry.name), depth: current.depth + 1 });
1443
+ queue.push({ dir: path16.join(current.dir, entry.name), depth: current.depth + 1 });
1062
1444
  }
1063
1445
  }
1064
1446
  return count;
@@ -1067,7 +1449,7 @@ function buildPresetContext(cwd) {
1067
1449
  const files = /* @__PURE__ */ new Set();
1068
1450
  const dirs = /* @__PURE__ */ new Set();
1069
1451
  try {
1070
- for (const entry of fs12.readdirSync(cwd, { withFileTypes: true })) {
1452
+ for (const entry of fs14.readdirSync(cwd, { withFileTypes: true })) {
1071
1453
  if (entry.isDirectory()) dirs.add(entry.name);
1072
1454
  else if (entry.isFile()) files.add(entry.name);
1073
1455
  }
@@ -1084,7 +1466,7 @@ function buildPresetContext(cwd) {
1084
1466
  const isWorkspace = files.has("pnpm-workspace.yaml") || files.has("nx.json") || files.has("turbo.json") || files.has("lerna.json") || packageJson !== void 0 && packageJson.includes('"workspaces"') || declaredWorkspaces.length > 0;
1085
1467
  let workspace = { files: /* @__PURE__ */ new Set(), dependencies: /* @__PURE__ */ new Set() };
1086
1468
  if (isWorkspace) {
1087
- const pnpmRaw = files.has("pnpm-workspace.yaml") ? readSmallFile(path13.join(cwd, "pnpm-workspace.yaml")) : null;
1469
+ const pnpmRaw = files.has("pnpm-workspace.yaml") ? readSmallFile(path16.join(cwd, "pnpm-workspace.yaml")) : null;
1088
1470
  const declared = pnpmRaw === null ? [] : pnpmWorkspacePatterns(pnpmRaw);
1089
1471
  const patterns = declared.length > 0 ? declared : declaredWorkspaces.length > 0 ? declaredWorkspaces : DEFAULT_WORKSPACE_PATTERNS;
1090
1472
  workspace = scanWorkspacePackages(cwd, patterns);
@@ -1929,10 +2311,15 @@ function detectPreset(cwd, forced) {
1929
2311
  // src/security/patterns.ts
1930
2312
  var STRONG_CREDENTIAL_NAME = "(?:password|passwd|passphrase|pwd|api[_-]?key|api[_-]?secret|secret[_-]?access[_-]?key|secret[_-]?key|client[_-]?secret[_-]?key|client[_-]?secret|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|secret|token)";
1931
2313
  var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
1932
- var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
1933
- var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/%:.~-]))([A-Za-z0-9_+=/%:~-]{1,512})(?![A-Za-z0-9_+=/%:.~-]|\\()";
1934
- var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/%:.~-]))([A-Za-z0-9_+=/%:~-]{8,512})(?![A-Za-z0-9_+=/%:.~-]|\\()";
1935
- var QUOTED_VALUE = String.raw`((?:[^"'\\\n]|\\.){1,512})`;
2314
+ var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|await|async|new|this|typeof|delete|yield|function|return|process\\.env|Buffer|Record|object)\\b)";
2315
+ var UNQUOTED_MIN_LENGTH = 2;
2316
+ var PROSE_CONTINUATION = String.raw`(?!\s+[a-z])`;
2317
+ var UNQUOTED_LITERAL = String.raw`(?![0-9]+(?![A-Za-z0-9_+=/%:.~-]))([A-Za-z0-9_][A-Za-z0-9_+=/%:~-]{${UNQUOTED_MIN_LENGTH - 1},511})(?![A-Za-z0-9_+=/%:.~-]|[({[])` + PROSE_CONTINUATION;
2318
+ var UNQUOTED_AMBIGUOUS_LITERAL = String.raw`(?![0-9]+(?![A-Za-z0-9_+=/%:.~-]))([A-Za-z0-9_][A-Za-z0-9_+=/%:~-]{7,511})(?![A-Za-z0-9_+=/%:.~-]|[({[])` + PROSE_CONTINUATION;
2319
+ var ASSIGN = String.raw`(?<![=!<>])[:=](?!=)`;
2320
+ var NOT_AFTER_QUOTE = String.raw`(?<!["'])`;
2321
+ var NOT_A_NAMESPACED_IDENTIFIER = String.raw`(?![a-z][a-z0-9]*(?:\.[a-z][a-z0-9-]*)+/)`;
2322
+ var QUOTED_VALUE = String.raw`((?:[^"'\\\n]|\\\\["'](?=[A-Za-z0-9_])|\\.){1,512})`;
1936
2323
  var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
1937
2324
  var DEFAULT_SECRET_PATTERNS = [
1938
2325
  {
@@ -1975,7 +2362,7 @@ var DEFAULT_SECRET_PATTERNS = [
1975
2362
  // literal words matched. The value is masked alone, so the key survives.
1976
2363
  rule: "aws-secret-access-key",
1977
2364
  pattern: new RegExp(
1978
- `${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
2365
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${STRONG_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
1979
2366
  "i"
1980
2367
  ),
1981
2368
  valueGroup: 1
@@ -2087,7 +2474,7 @@ var DEFAULT_SECRET_PATTERNS = [
2087
2474
  // the key name.
2088
2475
  rule: "credential-assignment",
2089
2476
  pattern: new RegExp(
2090
- `(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])${QUOTED_VALUE}\\1`,
2477
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${STRONG_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_NAMESPACED_IDENTIFIER}(["'\`])${QUOTED_VALUE}\\1`,
2091
2478
  "i"
2092
2479
  ),
2093
2480
  valueGroup: 2
@@ -2097,7 +2484,7 @@ var DEFAULT_SECRET_PATTERNS = [
2097
2484
  // are accepted only against a quoted value long enough to be a credential.
2098
2485
  rule: "credential-assignment-weak",
2099
2486
  pattern: new RegExp(
2100
- `(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])${QUOTED_VALUE}\\1`,
2487
+ `(?:["']${WEAK_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${WEAK_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}(["'\`])${QUOTED_VALUE}\\1`,
2101
2488
  "i"
2102
2489
  ),
2103
2490
  valueGroup: 2
@@ -2108,7 +2495,7 @@ var DEFAULT_SECRET_PATTERNS = [
2108
2495
  // `POSTGRES_PASSWORD:` in the output.
2109
2496
  rule: "credential-assignment-unquoted",
2110
2497
  pattern: new RegExp(
2111
- `${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2498
+ `${NOT_AFTER_QUOTE}${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2112
2499
  "i"
2113
2500
  ),
2114
2501
  valueGroup: 1
@@ -2119,7 +2506,7 @@ var DEFAULT_SECRET_PATTERNS = [
2119
2506
  // `pass: 3` / `credentials: none` readable.
2120
2507
  rule: "credential-assignment-unquoted-weak",
2121
2508
  pattern: new RegExp(
2122
- `${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
2509
+ `${NOT_AFTER_QUOTE}${WEAK_CREDENTIAL_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
2123
2510
  "i"
2124
2511
  ),
2125
2512
  valueGroup: 1
@@ -2140,7 +2527,7 @@ var DEFAULT_SECRET_PATTERNS = [
2140
2527
  // above because it also matches a *quoted key* followed by an unquoted
2141
2528
  // value, e.g. `"client_secret": abc123`.
2142
2529
  rule: "generic-secret-assignment",
2143
- pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
2530
+ pattern: /(?<!["'])(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*(?<![=!<>])[:=](?!=)\s*["']([^"'\s]{8,256})["']/i,
2144
2531
  valueGroup: 1
2145
2532
  },
2146
2533
  {
@@ -2156,8 +2543,17 @@ var DEFAULT_SECRET_PATTERNS = [
2156
2543
  // Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
2157
2544
  // comment or space disabled the rule, so `TOKEN=secret # rotate` and
2158
2545
  // `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
2546
+ //
2547
+ // The name is now required to be uppercase. With `/i`, a reassignment inside a
2548
+ // function — ` key = fs.realpathSync(stagingRoot)` — matched `key`…`KEY` and
2549
+ // was emitted as `key = [REDACTED](stagingRoot)`, which corrupted this
2550
+ // repository's own `src/index.ts`. Shell and `.env` names are uppercase by
2551
+ // convention and a lowercase `key = …` at an indent is a variable, so the case
2552
+ // of the name is the discriminator that separates the two. The value must end
2553
+ // at end-of-line, whitespace or a comment: that is what stops a call expression
2554
+ // (`NAME=f(x)`) from being read as an assigned literal.
2159
2555
  rule: "env-secret-assignment",
2160
- pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?!["'`$<{[\]\s])([^\s#;"'`)]{1,512})/i,
2556
+ pattern: /^\s{0,64}(?:export\s{1,8})?(?=[A-Z][A-Z0-9_]{0,191}\s{0,8}=)[A-Z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)[A-Z0-9_]{0,96}\s{0,8}=(?!=)\s{0,8}(?!["'`$<{[\]\s])([^\s#;"'`()]{1,512})(?=$|[\s#;])/,
2161
2557
  valueGroup: 1
2162
2558
  }
2163
2559
  ];
@@ -2295,6 +2691,12 @@ function scanAndRedactSecrets(content, rules, filePath) {
2295
2691
  continue;
2296
2692
  }
2297
2693
  const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
2694
+ if (group && isSelfNamingConstant(line, group[0], group[1])) continue;
2695
+ if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index)) continue;
2696
+ if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index)) {
2697
+ continue;
2698
+ }
2699
+ if (group && isTemplateInterpolation(line, group[0], group[1])) continue;
2298
2700
  spans.push(
2299
2701
  group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
2300
2702
  );
@@ -2324,6 +2726,46 @@ function expandOverToken(line, start, end) {
2324
2726
  while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
2325
2727
  return { start: left, end: right };
2326
2728
  }
2729
+ function startsInsideStringLiteral(line, matchIndex) {
2730
+ let runStart = matchIndex;
2731
+ while (runStart > 0 && /[A-Za-z0-9_-]/.test(line[runStart - 1])) runStart -= 1;
2732
+ const before = runStart > 0 ? line[runStart - 1] : "";
2733
+ return before === '"' || before === "'" || before === "`";
2734
+ }
2735
+ function isTemplateInterpolation(line, start, end) {
2736
+ return line[start] === "$" && line[start + 1] === "{" && line[end - 1] === "}";
2737
+ }
2738
+ function isSelfNamingConstant(line, start, end) {
2739
+ const value = line.slice(start, end);
2740
+ if (!/[a-z][A-Z]/.test(value)) return false;
2741
+ let index = start;
2742
+ if (index > 0) {
2743
+ const openingQuote = line[index - 1];
2744
+ if (openingQuote === '"' || openingQuote === "'" || openingQuote === "`") index -= 1;
2745
+ }
2746
+ while (index > 0 && /\s/.test(line[index - 1])) index -= 1;
2747
+ if (index === 0) return false;
2748
+ if (line[index - 1] !== ":" && line[index - 1] !== "=") return false;
2749
+ index -= 1;
2750
+ while (index > 0 && /\s/.test(line[index - 1])) index -= 1;
2751
+ if (index > 0) {
2752
+ const closingQuote = line[index - 1];
2753
+ if (closingQuote === '"' || closingQuote === "'" || closingQuote === "`") index -= 1;
2754
+ }
2755
+ return line.endsWith(value, index);
2756
+ }
2757
+ function isUnquotedValue(line, start) {
2758
+ const before = start > 0 ? line[start - 1] : "";
2759
+ return before !== '"' && before !== "'" && before !== "`";
2760
+ }
2761
+ var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|declare|export|using)\s+$/;
2762
+ var DECLARATION_WINDOW = 40;
2763
+ function isDeclaredBinding(line, matchIndex) {
2764
+ let runStart = matchIndex;
2765
+ while (runStart > 0 && /[A-Za-z0-9_$]/.test(line[runStart - 1])) runStart -= 1;
2766
+ const from = Math.max(0, runStart - DECLARATION_WINDOW);
2767
+ return DECLARATION_KEYWORDS.test(line.slice(from, runStart));
2768
+ }
2327
2769
  function applySpans(line, spans) {
2328
2770
  const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
2329
2771
  const merged = [];
@@ -2350,45 +2792,58 @@ function applySpans(line, spans) {
2350
2792
  function isDiscoveryMode(value) {
2351
2793
  return value === "auto" || value === "git" || value === "walk";
2352
2794
  }
2353
- var activeDiscovery = "auto";
2354
- function setActiveDiscovery(mode) {
2355
- activeDiscovery = mode;
2356
- }
2357
2795
  var PRESET_IGNORES = new Set(
2358
2796
  PRESETS.flatMap((preset) => preset.exclude ?? [])
2359
2797
  );
2360
2798
  function readLinkTarget(linkPath, fallback) {
2361
2799
  try {
2362
- return fs12.readlinkSync(linkPath);
2800
+ return fs14.readlinkSync(linkPath);
2363
2801
  } catch {
2364
2802
  return fallback;
2365
2803
  }
2366
2804
  }
2367
2805
  function isInside(root, candidate) {
2368
- const relative7 = path13.relative(root, candidate);
2369
- return relative7 === "" || !relative7.startsWith(`..${path13.sep}`) && relative7 !== ".." && !path13.isAbsolute(relative7);
2806
+ const relative8 = path16.relative(root, candidate);
2807
+ return relative8 === "" || !relative8.startsWith(`..${path16.sep}`) && relative8 !== ".." && !path16.isAbsolute(relative8);
2370
2808
  }
2809
+ var GIT_OUTPUT_LIMIT = 68157440;
2371
2810
  function getGitInfo(cwd, fallbackBranch = "main") {
2372
2811
  try {
2373
- const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
2374
- cwd,
2375
- stdio: ["ignore", "pipe", "ignore"],
2376
- encoding: "utf8"
2377
- }).trim();
2378
- const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
2379
- const commit = childProcess.execSync("git rev-parse HEAD", {
2380
- cwd,
2381
- stdio: ["ignore", "pipe", "ignore"],
2382
- encoding: "utf8"
2383
- }).trim() || "unknown";
2384
- const status = childProcess.execSync("git status --porcelain", {
2385
- cwd,
2386
- stdio: ["ignore", "pipe", "ignore"],
2387
- encoding: "utf8"
2388
- }).trim();
2389
- return { branch, commit, isDirty: status.length > 0 };
2390
- } catch {
2391
- return { branch: fallbackBranch, commit: "unknown", isDirty: false };
2812
+ const output = childProcess.execFileSync(
2813
+ "git",
2814
+ ["status", "--porcelain=v2", "--branch", "-z"],
2815
+ {
2816
+ cwd,
2817
+ stdio: ["ignore", "pipe", "ignore"],
2818
+ encoding: "utf8",
2819
+ maxBuffer: GIT_OUTPUT_LIMIT
2820
+ }
2821
+ );
2822
+ let branch = fallbackBranch;
2823
+ let commit = "unknown";
2824
+ let isDirty = false;
2825
+ const records = output.split("\0").flatMap((record) => record.split("\n"));
2826
+ for (const record of records) {
2827
+ if (record.length === 0) continue;
2828
+ if (!record.startsWith("# ")) {
2829
+ isDirty = true;
2830
+ break;
2831
+ }
2832
+ const oid = record.match(/^# branch\.oid (.+)$/);
2833
+ if (oid) {
2834
+ commit = oid[1] === "(initial)" ? "unknown" : oid[1];
2835
+ continue;
2836
+ }
2837
+ const head = record.match(/^# branch\.head (.+)$/);
2838
+ if (head) {
2839
+ branch = head[1] === "(detached)" ? fallbackBranch : head[1];
2840
+ }
2841
+ }
2842
+ return { branch, commit, isDirty };
2843
+ } catch (err) {
2844
+ const code = err.code;
2845
+ const isDirty = code === "ENOBUFS";
2846
+ return { branch: fallbackBranch, commit: "unknown", isDirty };
2392
2847
  }
2393
2848
  }
2394
2849
  var DEFAULT_IGNORED_DIRS = [
@@ -2435,16 +2890,27 @@ function isSensitivePath(relativePath) {
2435
2890
  }
2436
2891
  return matchesSensitiveList(posixPath, base);
2437
2892
  }
2893
+ var GLOBAL_PATTERNS = /* @__PURE__ */ new WeakMap();
2894
+ function globalPattern(pattern) {
2895
+ let compiled = GLOBAL_PATTERNS.get(pattern);
2896
+ if (compiled === void 0) {
2897
+ compiled = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2898
+ GLOBAL_PATTERNS.set(pattern, compiled);
2899
+ }
2900
+ return compiled;
2901
+ }
2902
+ var DEFAULT_PATH_RULES = [...DEFAULT_SECRET_PATTERNS];
2438
2903
  function pathScanRules(extraRules) {
2439
- const fallback = extraRules ?? [];
2440
- return [...DEFAULT_SECRET_PATTERNS, ...fallback];
2904
+ if (extraRules === void 0 || extraRules.length === 0) return DEFAULT_PATH_RULES;
2905
+ return [...DEFAULT_SECRET_PATTERNS, ...extraRules];
2441
2906
  }
2442
2907
  function findSecretInPath(relativePath, extraRules) {
2443
2908
  const posixPath = relativePath.replace(/\\/g, "/");
2444
2909
  const spans = [];
2445
2910
  const rules = [];
2446
2911
  for (const { rule, pattern } of pathScanRules(extraRules)) {
2447
- const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2912
+ const scan = globalPattern(pattern);
2913
+ scan.lastIndex = 0;
2448
2914
  let match;
2449
2915
  while ((match = scan.exec(posixPath)) !== null) {
2450
2916
  if (match[0].length === 0) {
@@ -2571,10 +3037,10 @@ function trimUnescapedTrailingSpaces(line) {
2571
3037
  return line.slice(0, end);
2572
3038
  }
2573
3039
  function readGitignoreRules(dir) {
2574
- const gitignorePath2 = path13.join(dir, ".gitignore");
3040
+ const gitignorePath2 = path16.join(dir, ".gitignore");
2575
3041
  let raw;
2576
3042
  try {
2577
- raw = fs12.readFileSync(gitignorePath2, "utf8");
3043
+ raw = fs14.readFileSync(gitignorePath2, "utf8");
2578
3044
  } catch {
2579
3045
  return [];
2580
3046
  }
@@ -2600,7 +3066,7 @@ function readGitignoreRules(dir) {
2600
3066
  function escapeRegExpChar(character) {
2601
3067
  return /[\\^$.*+?()[\]{}|/]/.test(character) ? `\\${character}` : character;
2602
3068
  }
2603
- function matchEscapedGitignorePattern(target, pattern) {
3069
+ function matchGitignorePattern(target, pattern) {
2604
3070
  const anchored = pattern.startsWith("/");
2605
3071
  const body = anchored ? pattern.slice(1) : pattern;
2606
3072
  let source = "";
@@ -2634,15 +3100,33 @@ function matchEscapedGitignorePattern(target, pattern) {
2634
3100
  source += "[^/]";
2635
3101
  continue;
2636
3102
  }
3103
+ if (character === "[") {
3104
+ const close = body.indexOf("]", i + 1);
3105
+ if (close !== -1 && close > i + 1) {
3106
+ const inner = body.slice(i + 1, close);
3107
+ const negated = inner.startsWith("!") || inner.startsWith("^");
3108
+ const chars = negated ? inner.slice(1) : inner;
3109
+ const safe = chars.replace(/[\\^\]]/g, (value) => `\\${value}`);
3110
+ source += negated ? `[^/${safe}]` : `[${safe}]`;
3111
+ i = close;
3112
+ continue;
3113
+ }
3114
+ }
2637
3115
  source += escapeRegExpChar(character);
2638
3116
  }
2639
3117
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
2640
3118
  return new RegExp(`${prefix}${source}(?:/.*)?$`).test(target);
2641
3119
  }
2642
- function matchesGitignorePattern(target, pattern) {
2643
- return pattern.includes("\\") ? matchEscapedGitignorePattern(target, pattern) : matchesExclude(target, pattern);
3120
+ function matchesGitignoreRule(rule, scoped, isDirectory) {
3121
+ if (!rule.dirOnly) return matchGitignorePattern(scoped, rule.pattern);
3122
+ const segments = scoped.split("/");
3123
+ const limit = isDirectory ? segments.length : segments.length - 1;
3124
+ for (let depth = 1; depth <= limit; depth += 1) {
3125
+ if (matchGitignorePattern(segments.slice(0, depth).join("/"), rule.pattern)) return true;
3126
+ }
3127
+ return false;
2644
3128
  }
2645
- function gitignoreVerdict(layers, relativePath) {
3129
+ function gitignoreVerdict(layers, relativePath, isDirectory) {
2646
3130
  let verdict = null;
2647
3131
  for (const layer2 of layers) {
2648
3132
  let scoped;
@@ -2655,8 +3139,7 @@ function gitignoreVerdict(layers, relativePath) {
2655
3139
  }
2656
3140
  let layerVerdict = null;
2657
3141
  for (const rule of layer2.rules) {
2658
- const target = rule.dirOnly ? scoped.replace(/\/.*$/, "") : scoped;
2659
- if (matchesGitignorePattern(target, rule.pattern)) layerVerdict = !rule.negated;
3142
+ if (matchesGitignoreRule(rule, scoped, isDirectory)) layerVerdict = !rule.negated;
2660
3143
  }
2661
3144
  if (layerVerdict !== null) verdict = layerVerdict;
2662
3145
  }
@@ -2681,13 +3164,14 @@ function defaultIgnoreReason(relativePath) {
2681
3164
  }
2682
3165
  function listIgnoredPaths(cwd) {
2683
3166
  try {
2684
- const stdout = childProcess.execSync(
2685
- "git ls-files -z --others --ignored --exclude-standard --directory",
3167
+ const stdout = childProcess.execFileSync(
3168
+ "git",
3169
+ ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"],
2686
3170
  {
2687
3171
  cwd,
2688
3172
  stdio: ["ignore", "pipe", "ignore"],
2689
3173
  encoding: "utf8",
2690
- maxBuffer: 32 * 1024 * 1024
3174
+ maxBuffer: GIT_OUTPUT_LIMIT
2691
3175
  }
2692
3176
  );
2693
3177
  return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
@@ -2695,13 +3179,9 @@ function listIgnoredPaths(cwd) {
2695
3179
  return [];
2696
3180
  }
2697
3181
  }
2698
- var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
2699
- function isToolOwnedDirName(name) {
2700
- return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
2701
- }
2702
3182
  function hasSnapshotMarker(target) {
2703
3183
  try {
2704
- return fs12.statSync(target).isDirectory() && fs12.existsSync(path13.join(target, ".codebase-snapshot"));
3184
+ return fs14.statSync(target).isDirectory() && fs14.existsSync(path16.join(target, ".codebase-snapshot"));
2705
3185
  } catch {
2706
3186
  return false;
2707
3187
  }
@@ -2715,7 +3195,7 @@ function makeToolOwnedDecider(cwd, toolOwnedPaths) {
2715
3195
  const prefix = parts.slice(0, depth).join("/");
2716
3196
  let hasMarker = markers.get(prefix);
2717
3197
  if (hasMarker === void 0) {
2718
- hasMarker = fs12.existsSync(path13.join(cwd, prefix, ".codebase-snapshot"));
3198
+ hasMarker = fs14.existsSync(path16.join(cwd, prefix, ".codebase-snapshot"));
2719
3199
  markers.set(prefix, hasMarker);
2720
3200
  }
2721
3201
  if (hasMarker) return true;
@@ -2730,7 +3210,7 @@ function makeLinkProbe() {
2730
3210
  let known = cache.get(absolutePath);
2731
3211
  if (known === void 0) {
2732
3212
  try {
2733
- known = fs12.lstatSync(absolutePath).isSymbolicLink();
3213
+ known = fs14.lstatSync(absolutePath).isSymbolicLink();
2734
3214
  } catch {
2735
3215
  known = false;
2736
3216
  }
@@ -2743,7 +3223,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2743
3223
  const isLink = makeLinkProbe();
2744
3224
  let rootReal = cwd;
2745
3225
  try {
2746
- rootReal = fs12.realpathSync(cwd);
3226
+ rootReal = fs14.realpathSync(cwd);
2747
3227
  } catch {
2748
3228
  }
2749
3229
  const reported = /* @__PURE__ */ new Set();
@@ -2753,7 +3233,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2753
3233
  let linkedDir = null;
2754
3234
  for (let depth = 1; depth <= parts.length; depth += 1) {
2755
3235
  const prefix = parts.slice(0, depth).join("/");
2756
- if (isLink(path13.join(cwd, prefix))) {
3236
+ if (isLink(path16.join(cwd, prefix))) {
2757
3237
  linkedDir = prefix;
2758
3238
  break;
2759
3239
  }
@@ -2764,10 +3244,10 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2764
3244
  }
2765
3245
  if (reported.has(linkedDir)) continue;
2766
3246
  reported.add(linkedDir);
2767
- const absolute = path13.join(cwd, linkedDir);
3247
+ const absolute = path16.join(cwd, linkedDir);
2768
3248
  let targetReal = absolute;
2769
3249
  try {
2770
- targetReal = fs12.realpathSync(absolute);
3250
+ targetReal = fs14.realpathSync(absolute);
2771
3251
  } catch {
2772
3252
  }
2773
3253
  const target = readLinkTarget(absolute, targetReal);
@@ -2787,13 +3267,14 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2787
3267
  return kept;
2788
3268
  }
2789
3269
  function listGitCandidates(cwd, skipped, isToolOwned) {
2790
- const stdout = childProcess.execSync(
2791
- "git ls-files -z --cached --others --exclude-standard",
3270
+ const stdout = childProcess.execFileSync(
3271
+ "git",
3272
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
2792
3273
  {
2793
3274
  cwd,
2794
3275
  stdio: ["ignore", "pipe", "ignore"],
2795
3276
  encoding: "utf8",
2796
- maxBuffer: 50 * 1024 * 1024
3277
+ maxBuffer: GIT_OUTPUT_LIMIT
2797
3278
  }
2798
3279
  );
2799
3280
  let candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
@@ -2830,7 +3311,7 @@ function collectFiles(cwd, options = {}) {
2830
3311
  const isToolOwned = makeToolOwnedDecider(cwd, [...toolOwnedPaths]);
2831
3312
  let candidates;
2832
3313
  const skipped = [];
2833
- const discovery = options.discovery ?? activeDiscovery;
3314
+ const discovery = options.discovery ?? "auto";
2834
3315
  if (discovery === "walk") {
2835
3316
  const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), toolOwnedPaths);
2836
3317
  candidates = walked.files;
@@ -2844,7 +3325,7 @@ function collectFiles(cwd, options = {}) {
2844
3325
  "Discovery mode is `git`, but git could not list this directory's files. Run inside a working repository, or pass --discovery walk (or auto) to scan the tree directly."
2845
3326
  );
2846
3327
  }
2847
- if (fs12.existsSync(path13.join(cwd, ".git"))) {
3328
+ if (fs14.existsSync(path16.join(cwd, ".git"))) {
2848
3329
  throw new Error(
2849
3330
  "This directory is a git repository, but git could not list its files. A plain walk honours .gitignore files, but git also applies .git/info/exclude and your global excludes file, includes files that are tracked although a .gitignore now matches them, and treats a submodule as a single entry. Fix the repository (git status should work), run outside git, or pass --discovery walk to accept those differences."
2850
3331
  );
@@ -2903,7 +3384,7 @@ function collectFiles(cwd, options = {}) {
2903
3384
  }
2904
3385
  function rootRealFor(root) {
2905
3386
  try {
2906
- return fs12.realpathSync(root);
3387
+ return fs14.realpathSync(root);
2907
3388
  } catch {
2908
3389
  return root;
2909
3390
  }
@@ -2911,18 +3392,18 @@ function rootRealFor(root) {
2911
3392
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
2912
3393
  const results = { files: [], skipped: [] };
2913
3394
  const stack = [];
2914
- const relativeToBase = (target) => path13.relative(baseDir, target).replace(/\\/g, "/") || ".";
3395
+ const relativeToBase = (target) => path16.relative(baseDir, target).replace(/\\/g, "/") || ".";
2915
3396
  const enter = (current, layers) => {
2916
3397
  if (visited.has(current)) return false;
2917
3398
  visited.add(current);
2918
3399
  const localRules = readGitignoreRules(current);
2919
3400
  const gitignoreLayers = localRules.length === 0 ? layers : [
2920
3401
  ...layers,
2921
- { prefix: path13.relative(baseDir, current).replace(/\\/g, "/"), rules: localRules }
3402
+ { prefix: path16.relative(baseDir, current).replace(/\\/g, "/"), rules: localRules }
2922
3403
  ];
2923
3404
  let entries;
2924
3405
  try {
2925
- entries = fs12.readdirSync(current, { withFileTypes: true });
3406
+ entries = fs14.readdirSync(current, { withFileTypes: true });
2926
3407
  } catch {
2927
3408
  results.skipped.push({
2928
3409
  path: relativeToBase(current),
@@ -2944,10 +3425,10 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2944
3425
  }
2945
3426
  const entry = frame.entries[frame.index];
2946
3427
  frame.index += 1;
2947
- const fullPath = path13.join(frame.dir, entry.name);
2948
- const relPath = path13.relative(baseDir, fullPath).replace(/\\/g, "/");
2949
- if (isToolOwnedDirName(entry.name) && reservedPaths.has(relPath)) continue;
2950
- if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
3428
+ const fullPath = path16.join(frame.dir, entry.name);
3429
+ const relPath = path16.relative(baseDir, fullPath).replace(/\\/g, "/");
3430
+ if (reservedPaths.has(relPath)) continue;
3431
+ if (entry.isDirectory() && DEFAULT_IGNORED_DIRS.includes(entry.name)) {
2951
3432
  results.skipped.push({
2952
3433
  path: relPath,
2953
3434
  reason: "default-ignore",
@@ -2963,7 +3444,7 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2963
3444
  });
2964
3445
  continue;
2965
3446
  }
2966
- if (gitignoreVerdict(frame.gitignoreLayers, relPath) === true) {
3447
+ if (gitignoreVerdict(frame.gitignoreLayers, relPath, entry.isDirectory()) === true) {
2967
3448
  if (!hasSnapshotMarker(fullPath)) {
2968
3449
  results.skipped.push({
2969
3450
  path: relPath,
@@ -2978,14 +3459,14 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2978
3459
  if (entry.isSymbolicLink()) {
2979
3460
  let targetReal;
2980
3461
  try {
2981
- targetReal = fs12.realpathSync(fullPath);
3462
+ targetReal = fs14.realpathSync(fullPath);
2982
3463
  } catch {
2983
3464
  results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
2984
3465
  continue;
2985
3466
  }
2986
3467
  let stats;
2987
3468
  try {
2988
- stats = fs12.statSync(fullPath);
3469
+ stats = fs14.statSync(fullPath);
2989
3470
  } catch {
2990
3471
  results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
2991
3472
  continue;
@@ -3027,6 +3508,28 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3027
3508
  return results;
3028
3509
  }
3029
3510
 
3511
+ // src/tokenize.ts
3512
+ var WORD_RUN = /[A-Za-z0-9_]+/g;
3513
+ var SINGLE_TOKEN = /[^\sA-Za-z0-9_]/g;
3514
+ var LINE_BREAK_RUN = /\n+/g;
3515
+ var CHARS_PER_WORD_TOKEN = 6;
3516
+ function estimateTokens(text2) {
3517
+ if (text2.length === 0) return 0;
3518
+ let total = 0;
3519
+ for (const match of text2.match(WORD_RUN) ?? []) {
3520
+ total += Math.max(1, Math.round(match.length / CHARS_PER_WORD_TOKEN));
3521
+ }
3522
+ total += text2.match(SINGLE_TOKEN)?.length ?? 0;
3523
+ total += text2.match(LINE_BREAK_RUN)?.length ?? 0;
3524
+ return total;
3525
+ }
3526
+ function bytesForTokens(tokens) {
3527
+ return Math.max(0, Math.floor(tokens * 4));
3528
+ }
3529
+ function formatTokens(count) {
3530
+ return count.toLocaleString("en-US");
3531
+ }
3532
+
3030
3533
  // src/config.ts
3031
3534
  var CONFIG_DIR_SYMBOL = /* @__PURE__ */ Symbol.for("codebase-snapshot.configDir");
3032
3535
  var CONFIG_FILENAMES = [
@@ -3040,13 +3543,13 @@ var CONFIG_FILENAMES = [
3040
3543
  ];
3041
3544
  function findConfigFile(cwd) {
3042
3545
  for (const filename of CONFIG_FILENAMES) {
3043
- const fullPath = path13.join(cwd, filename);
3044
- if (fs12.existsSync(fullPath)) return fullPath;
3546
+ const fullPath = path16.join(cwd, filename);
3547
+ if (fs14.existsSync(fullPath)) return fullPath;
3045
3548
  }
3046
- const pkgJsonPath = path13.join(cwd, "package.json");
3047
- if (!fs12.existsSync(pkgJsonPath)) return null;
3549
+ const pkgJsonPath = path16.join(cwd, "package.json");
3550
+ if (!fs14.existsSync(pkgJsonPath)) return null;
3048
3551
  try {
3049
- const parsed = JSON.parse(fs12.readFileSync(pkgJsonPath, "utf8"));
3552
+ const parsed = JSON.parse(fs14.readFileSync(pkgJsonPath, "utf8"));
3050
3553
  return parsed.snapshot === void 0 ? null : pkgJsonPath;
3051
3554
  } catch {
3052
3555
  return null;
@@ -3080,9 +3583,23 @@ function isResolvedConfig(value) {
3080
3583
  );
3081
3584
  return hasResolvedNumbers && hasResolvedStrings && hasResolvedFlags && hasResolvedRules && hasCompiledRules;
3082
3585
  }
3586
+ function normalizeBudgets(budgets, maxPartBytes) {
3587
+ const clearer = typeof maxPartBytes === "number" && Number.isFinite(maxPartBytes) && maxPartBytes > 0 ? maxPartBytes : void 0;
3588
+ if (budgets === void 0 || typeof budgets !== "object" || budgets === null) {
3589
+ return { default: clearer ?? 0, byLayer: {} };
3590
+ }
3591
+ const fallbackDefault = clearer ?? budgets.default;
3592
+ const byLayer = {};
3593
+ const layers = budgets.byLayer;
3594
+ if (layers !== void 0 && typeof layers === "object" && layers !== null) {
3595
+ for (const [layer2, value] of Object.entries(layers)) {
3596
+ byLayer[layer2] = Math.min(value, fallbackDefault);
3597
+ }
3598
+ }
3599
+ return { default: fallbackDefault, byLayer };
3600
+ }
3083
3601
  function normalizeResolvedConfig(config) {
3084
3602
  const discovery = isDiscoveryMode(config.discovery) ? config.discovery : "auto";
3085
- setActiveDiscovery(discovery);
3086
3603
  return markResolvedConfig({
3087
3604
  ...config,
3088
3605
  discovery,
@@ -3101,7 +3618,7 @@ function normalizeResolvedConfig(config) {
3101
3618
  ])
3102
3619
  ],
3103
3620
  layersMode: config.layersMode === "replace" ? "replace" : "merge",
3104
- budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
3621
+ budgets: normalizeBudgets(config.budgets, config.maxPartBytes),
3105
3622
  hooks: resolveHooks(config.hooks),
3106
3623
  excludePatterns: Array.isArray(config.excludePatterns) ? config.excludePatterns : [],
3107
3624
  layers: Array.isArray(config.layers) ? config.layers : [],
@@ -3223,6 +3740,7 @@ var CONFIG_KEYS = /* @__PURE__ */ new Set([
3223
3740
  "artifacts",
3224
3741
  "branch",
3225
3742
  "budgets",
3743
+ "changedSince",
3226
3744
  "cwd",
3227
3745
  "discovery",
3228
3746
  "exclude",
@@ -3237,12 +3755,18 @@ var CONFIG_KEYS = /* @__PURE__ */ new Set([
3237
3755
  "layersMode",
3238
3756
  "maxMonolithBytes",
3239
3757
  "maxPartBytes",
3758
+ "maxPartTokens",
3240
3759
  "outDir",
3241
3760
  "preset",
3242
3761
  "previewHeadLines",
3762
+ "staged",
3243
3763
  "previewTailLines",
3764
+ "secretRules",
3244
3765
  "secrets",
3245
- "tmpDir"
3766
+ "sarif",
3767
+ "skeleton",
3768
+ "tmpDir",
3769
+ "withDependents"
3246
3770
  ]);
3247
3771
  function assertKnownConfigKeys(value, file) {
3248
3772
  const unknownKeys = Object.keys(value).filter((key) => !CONFIG_KEYS.has(key));
@@ -3297,11 +3821,11 @@ async function loadConfigFileDetailed(cwd) {
3297
3821
  const warnings = [];
3298
3822
  const errors = [];
3299
3823
  for (const filename of CONFIG_FILENAMES) {
3300
- const fullPath = path13.join(cwd, filename);
3301
- if (!fs12.existsSync(fullPath)) continue;
3824
+ const fullPath = path16.join(cwd, filename);
3825
+ if (!fs14.existsSync(fullPath)) continue;
3302
3826
  if (filename.endsWith(".json") || filename === ".snapshotrc") {
3303
3827
  try {
3304
- const config = parseJsonish(fs12.readFileSync(fullPath, "utf8"), fullPath);
3828
+ const config = parseJsonish(fs14.readFileSync(fullPath, "utf8"), fullPath);
3305
3829
  if (config === null || typeof config !== "object" || Array.isArray(config)) {
3306
3830
  throw new Error(
3307
3831
  `${fullPath} does not hold a configuration object (received ${describeValue2(config)}).`
@@ -3335,12 +3859,16 @@ async function loadConfigFileDetailed(cwd) {
3335
3859
  continue;
3336
3860
  }
3337
3861
  }
3338
- const pkgJsonPath = path13.join(cwd, "package.json");
3339
- if (fs12.existsSync(pkgJsonPath)) {
3862
+ const pkgJsonPath = path16.join(cwd, "package.json");
3863
+ if (fs14.existsSync(pkgJsonPath)) {
3340
3864
  try {
3341
- const parsed = parseJsonish(fs12.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
3865
+ const parsed = parseJsonish(fs14.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
3342
3866
  const snapshotKey = parsed.snapshot;
3343
3867
  if (snapshotKey && typeof snapshotKey === "object" && !Array.isArray(snapshotKey)) {
3868
+ assertKnownConfigKeys(
3869
+ snapshotKey,
3870
+ `${pkgJsonPath} (the "snapshot" key)`
3871
+ );
3344
3872
  return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
3345
3873
  }
3346
3874
  if (snapshotKey !== void 0) {
@@ -3361,7 +3889,7 @@ async function loadConfigFile(cwd) {
3361
3889
  const loaded = await loadConfigFileDetailed(cwd);
3362
3890
  if (loaded.config && loaded.file && typeof loaded.config === "object") {
3363
3891
  Object.defineProperty(loaded.config, CONFIG_DIR_SYMBOL, {
3364
- value: path13.dirname(loaded.file),
3892
+ value: path16.dirname(loaded.file),
3365
3893
  enumerable: false,
3366
3894
  configurable: true
3367
3895
  });
@@ -3372,6 +3900,7 @@ async function loadConfigFile(cwd) {
3372
3900
  return loaded.config;
3373
3901
  }
3374
3902
  var MIN_MAX_PART_BYTES = 4096;
3903
+ var MIN_MAX_PART_TOKENS = MIN_MAX_PART_BYTES / 4;
3375
3904
  var ARTIFACT_NAMES = [
3376
3905
  "readme",
3377
3906
  "manifest",
@@ -3520,12 +4049,12 @@ function resolveKindRules(value) {
3520
4049
  function outputExcludePatterns(cwd, outDir, tmpDir) {
3521
4050
  const patterns = [];
3522
4051
  for (const dir of [outDir, tmpDir]) {
3523
- const absolute = path13.resolve(cwd, dir);
3524
- const relative7 = path13.relative(cwd, absolute);
3525
- if (!relative7 || relative7 === ".." || relative7.startsWith(`..${path13.sep}`) || path13.isAbsolute(relative7)) {
4052
+ const absolute = path16.resolve(cwd, dir);
4053
+ const relative8 = path16.relative(cwd, absolute);
4054
+ if (!relative8 || relative8 === ".." || relative8.startsWith(`..${path16.sep}`) || path16.isAbsolute(relative8)) {
3526
4055
  continue;
3527
4056
  }
3528
- patterns.push(`${relative7.replace(/\\/g, "/")}/**`);
4057
+ patterns.push(`${relative8.replace(/\\/g, "/")}/**`);
3529
4058
  }
3530
4059
  return patterns;
3531
4060
  }
@@ -3572,6 +4101,20 @@ function boundedBytes(value, fallback, minimum, label) {
3572
4101
  }
3573
4102
  return parsed;
3574
4103
  }
4104
+ function boundedTokens(value, label) {
4105
+ const parsed = parseByteSize(value);
4106
+ if (parsed === void 0 || !Number.isInteger(parsed)) {
4107
+ throw new Error(
4108
+ `${label} must be a whole token count such as 30000 or "30k" (received ${describeBytes(value)}).`
4109
+ );
4110
+ }
4111
+ if (parsed < MIN_MAX_PART_TOKENS) {
4112
+ throw new Error(
4113
+ `${label} must be at least ${MIN_MAX_PART_TOKENS} tokens (${MIN_MAX_PART_BYTES} bytes is the smallest part budget); received ${describeBytes(value)}.`
4114
+ );
4115
+ }
4116
+ return parsed;
4117
+ }
3575
4118
  function optionalBytes(value, label) {
3576
4119
  if (value === void 0 || value === null) return null;
3577
4120
  const parsed = parseByteSize(value);
@@ -3641,11 +4184,11 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
3641
4184
  );
3642
4185
  }
3643
4186
  const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
3644
- const resolved = path13.resolve(candidate);
3645
- if (!fs12.existsSync(resolved)) {
4187
+ const resolved = path16.resolve(candidate);
4188
+ if (!fs14.existsSync(resolved)) {
3646
4189
  throw new Error(`Working directory does not exist: ${resolved}`);
3647
4190
  }
3648
- if (!fs12.statSync(resolved).isDirectory()) {
4191
+ if (!fs14.statSync(resolved).isDirectory()) {
3649
4192
  throw new Error(`Working directory is not a directory: ${resolved}`);
3650
4193
  }
3651
4194
  return resolved;
@@ -3660,7 +4203,6 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3660
4203
  );
3661
4204
  }
3662
4205
  const discovery = requestedDiscovery ?? "auto";
3663
- setActiveDiscovery(discovery);
3664
4206
  const forcedPreset = resolvePresetName(userConfig?.preset);
3665
4207
  if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
3666
4208
  throw new Error(unknownPresetMessage(forcedPreset));
@@ -3714,13 +4256,53 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3714
4256
  MIN_MAX_PART_BYTES,
3715
4257
  "maxPartBytes"
3716
4258
  );
4259
+ const rawMaxPartTokens = cliOptions?.maxPartTokens ?? userConfig?.maxPartTokens;
4260
+ const requestedMaxPartTokens = rawMaxPartTokens === null ? void 0 : rawMaxPartTokens;
4261
+ const maxPartTokens = requestedMaxPartTokens === void 0 ? void 0 : boundedTokens(requestedMaxPartTokens, "maxPartTokens");
4262
+ const effectiveMaxPartBytes = maxPartTokens === void 0 ? maxPartBytes : Math.min(maxPartBytes, bytesForTokens(maxPartTokens));
3717
4263
  const budgets = resolveBudgets(
3718
4264
  userConfig?.budgets,
3719
- maxPartBytes,
3720
- explicitMaxPartBytes !== void 0
4265
+ effectiveMaxPartBytes,
4266
+ explicitMaxPartBytes !== void 0 || maxPartTokens !== void 0
3721
4267
  );
4268
+ const resolvedMaxPartBytes = explicitMaxPartBytes !== void 0 || maxPartTokens !== void 0 ? effectiveMaxPartBytes : budgets.default;
3722
4269
  const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
3723
4270
  const outDir = cliOptions?.outDir || configuredOutDir;
4271
+ const rawChangedSinceSetting = cliOptions?.changedSince ?? userConfig?.changedSince;
4272
+ const rawChangedSince = rawChangedSinceSetting === null ? void 0 : rawChangedSinceSetting;
4273
+ if (rawChangedSince !== void 0 && (typeof rawChangedSince !== "string" || rawChangedSince.trim().length === 0)) {
4274
+ throw new Error(
4275
+ `\`changedSince\` must be a git ref such as "main" or "HEAD~3" (received ${describeValue2(rawChangedSince)}).`
4276
+ );
4277
+ }
4278
+ const changedSince = rawChangedSince === void 0 ? void 0 : String(rawChangedSince).trim();
4279
+ const rawStaged = cliOptions?.staged ?? userConfig?.staged;
4280
+ if (rawStaged !== void 0 && typeof rawStaged !== "boolean") {
4281
+ throw new Error(`\`staged\` must be a boolean (received ${describeValue2(rawStaged)}).`);
4282
+ }
4283
+ const staged = rawStaged === true;
4284
+ const rawWithDependents = cliOptions?.withDependents ?? userConfig?.withDependents;
4285
+ if (rawWithDependents !== void 0 && typeof rawWithDependents !== "boolean") {
4286
+ throw new Error(
4287
+ `\`withDependents\` must be a boolean (received ${describeValue2(rawWithDependents)}).`
4288
+ );
4289
+ }
4290
+ const withDependents = rawWithDependents === true;
4291
+ const rawSkeleton = cliOptions?.skeleton ?? userConfig?.skeleton;
4292
+ if (rawSkeleton !== void 0 && typeof rawSkeleton !== "boolean") {
4293
+ throw new Error(`\`skeleton\` must be a boolean (received ${describeValue2(rawSkeleton)}).`);
4294
+ }
4295
+ const skeleton = rawSkeleton === true;
4296
+ const rawSarif = cliOptions?.sarif ?? userConfig?.sarif;
4297
+ if (rawSarif !== void 0 && typeof rawSarif !== "boolean") {
4298
+ throw new Error(`\`sarif\` must be a boolean (received ${describeValue2(rawSarif)}).`);
4299
+ }
4300
+ const sarif = rawSarif === true;
4301
+ if (withDependents && changedSince === void 0 && !staged) {
4302
+ throw new Error(
4303
+ "`withDependents` widens a changed-files scope, so it needs `changedSince` or `staged` (with neither, every file is already included)."
4304
+ );
4305
+ }
3724
4306
  const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
3725
4307
  const branch = strictString(userConfig?.branch, "main", "`branch`");
3726
4308
  const configuredSensitiveFiles = strictBoolean(
@@ -3741,7 +4323,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3741
4323
  branch,
3742
4324
  outDir,
3743
4325
  tmpDir,
3744
- maxPartBytes,
4326
+ maxPartBytes: resolvedMaxPartBytes,
4327
+ ...maxPartTokens === void 0 ? {} : { maxPartTokens },
3745
4328
  largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
3746
4329
  previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
3747
4330
  previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
@@ -3751,6 +4334,11 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3751
4334
  "maxMonolithBytes"
3752
4335
  ),
3753
4336
  includeSensitiveFiles,
4337
+ ...changedSince === void 0 ? {} : { changedSince },
4338
+ ...staged ? { staged } : {},
4339
+ ...withDependents ? { withDependents } : {},
4340
+ ...skeleton ? { skeleton } : {},
4341
+ ...sarif ? { sarif } : {},
3754
4342
  include: resolveInclude(userConfig?.include),
3755
4343
  preset: preset.presetName,
3756
4344
  presetExclude,
@@ -3784,14 +4372,11 @@ function matchRule(filePath, matcher) {
3784
4372
  if (pattern.endsWith("/**")) {
3785
4373
  const prefix = pattern.slice(0, -3).replace(/\/+$/, "");
3786
4374
  if (!prefix) return true;
3787
- if (!/[*?]/.test(prefix)) {
4375
+ if (!/[*?{[]/.test(prefix)) {
3788
4376
  return filePath === prefix || filePath.startsWith(`${prefix}/`);
3789
4377
  }
3790
4378
  }
3791
- if (pattern.includes("*") || pattern.includes("?")) {
3792
- return globToRegExp(pattern).test(filePath);
3793
- }
3794
- return filePath.includes(pattern);
4379
+ return matchesExclude(filePath, pattern);
3795
4380
  }
3796
4381
  return false;
3797
4382
  }
@@ -3810,6 +4395,21 @@ function classifyKind(filePath, kinds) {
3810
4395
  function configDigest(config) {
3811
4396
  const material = {
3812
4397
  maxPartBytes: config.maxPartBytes,
4398
+ // The user's own unit. `maxPartBytes` above already reflects the converted ceiling, but
4399
+ // recording the token number means changing 32k to 64k moves the digest even in the
4400
+ // corner where both round to the same byte budget.
4401
+ maxPartTokens: config.maxPartTokens ?? null,
4402
+ // A changed-files scope decides which files are in the snapshot at all, so a snapshot
4403
+ // taken over a different scope is a different snapshot.
4404
+ changedSince: config.changedSince ?? null,
4405
+ staged: config.staged === true,
4406
+ withDependents: config.withDependents === true,
4407
+ // Signature compression changes the emitted bytes of every TypeScript file it touches.
4408
+ skeleton: config.skeleton === true,
4409
+ // `sarif` is not one of `artifacts`: it writes `reports/audit.sarif` beside the audit, and
4410
+ // leaving it out of the digest meant a snapshot taken without it and one taken with it
4411
+ // compared equal even though their directories differ.
4412
+ sarif: config.sarif === true,
3813
4413
  largeFileBytes: config.largeFileBytes,
3814
4414
  previewHeadLines: config.previewHeadLines,
3815
4415
  previewTailLines: config.previewTailLines,
@@ -3822,9 +4422,19 @@ function configDigest(config) {
3822
4422
  preset: config.preset,
3823
4423
  layersMode: config.layersMode,
3824
4424
  budgets: config.budgets,
3825
- layers: config.layers.map((layer2) => layer2.name),
3826
- kinds: config.kinds.map((kind) => kind.kind),
3827
- secretRules: config.secretRules.map((rule) => `${rule.rule}:${rule.pattern.source}`).sort()
4425
+ // The matcher is what makes a layer a layer. Hashing only the names meant that
4426
+ // moving `{ name: "api", match: "src/api/**" }` to `"src/v2/**"` produced the same
4427
+ // digest, so `validate` reported "valid and up to date" for a snapshot whose files
4428
+ // had all been reclassified — and per-layer budgets applied to different files.
4429
+ layers: config.layers.map((layer2) => ({ name: layer2.name, match: String(layer2.match) })),
4430
+ kinds: config.kinds.map((kind) => ({ kind: kind.kind, match: String(kind.match) })),
4431
+ // How files are enumerated decides what is in the snapshot at all.
4432
+ discovery: config.discovery ?? "auto",
4433
+ // `flags` and `valueGroup` change *what is masked*, and a rule's source alone does
4434
+ // not imply either: the same source with and without `i` redacts different lines.
4435
+ secretRules: config.secretRules.map(
4436
+ (rule) => `${rule.rule}:${rule.pattern.source}/${rule.pattern.flags}:${rule.valueGroup ?? ""}`
4437
+ ).sort()
3828
4438
  };
3829
4439
  return createHash("sha256").update(JSON.stringify(material)).digest("hex");
3830
4440
  }
@@ -4004,6 +4614,95 @@ function generateAuditReport(manifest, secretFindings, secretRules) {
4004
4614
  }
4005
4615
  return lines.join("\n");
4006
4616
  }
4617
+ var SARIF_VERSION = "2.1.0";
4618
+ var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
4619
+ var BUILTIN_RULE_IDS = new Map(
4620
+ [
4621
+ ...DEFAULT_SECRET_PATTERNS.map((rule) => rule.rule),
4622
+ // Produced by the whole-block pass in `redact.ts`, not by a pattern in the catalogue.
4623
+ "private-key-block"
4624
+ ].map((name, index) => [name, `CS${String(index + 1).padStart(3, "0")}`])
4625
+ );
4626
+ function ruleIdFor(name) {
4627
+ const builtin = BUILTIN_RULE_IDS.get(name);
4628
+ if (builtin !== void 0) return builtin;
4629
+ const digest = crypto2.createHash("sha256").update(name).digest("hex").slice(0, 8);
4630
+ return `CS${digest.toUpperCase()}`;
4631
+ }
4632
+ function generateSarifReport(manifest, findings, rules, toolVersion) {
4633
+ const ruleNames = Array.from(
4634
+ /* @__PURE__ */ new Set([...rules.map((rule) => rule.rule), ...findings.flatMap((finding) => finding.rules)])
4635
+ ).sort();
4636
+ const ruleIndex = /* @__PURE__ */ new Map();
4637
+ const sarifRules = ruleNames.map((name, index) => {
4638
+ ruleIndex.set(name, index);
4639
+ const id = ruleIdFor(name);
4640
+ return {
4641
+ id,
4642
+ name,
4643
+ shortDescription: { text: `Credential-shaped value masked by the "${name}" rule` },
4644
+ defaultConfiguration: { level: "note" },
4645
+ help: {
4646
+ text: "A value in this line matched a secret pattern and was replaced with a redaction marker in the snapshot. If this is a real credential, rotate it; if it is a fixture, the placeholder can be made unmistakable."
4647
+ }
4648
+ };
4649
+ });
4650
+ const results = findings.filter((finding) => typeof finding.path === "string" && finding.path.length > 0).map((finding) => {
4651
+ const primary = finding.rules[0] ?? "unknown";
4652
+ const line = Math.max(1, finding.line);
4653
+ const findingPath = String(finding.path).replace(/\\/g, "/");
4654
+ return {
4655
+ ruleId: ruleIdFor(primary),
4656
+ ruleIndex: ruleIndex.get(primary) ?? 0,
4657
+ level: "note",
4658
+ message: {
4659
+ text: `Credential-shaped value masked (rule${finding.rules.length === 1 ? "" : "s"}: ${finding.rules.join(", ")})`
4660
+ },
4661
+ locations: [
4662
+ {
4663
+ physicalLocation: {
4664
+ artifactLocation: { uri: findingPath, uriBaseId: "%SRCROOT%" },
4665
+ region: { startLine: line }
4666
+ }
4667
+ }
4668
+ ],
4669
+ partialFingerprints: {
4670
+ // Stable across commits for the same finding, which is what keeps an alert open
4671
+ // instead of closing and reopening it on every run.
4672
+ codebaseSnapshotFinding: `${findingPath}#L${line}#${finding.rules.join("+")}`
4673
+ }
4674
+ };
4675
+ }).sort(
4676
+ (a, b) => a.locations[0].physicalLocation.artifactLocation.uri.localeCompare(
4677
+ b.locations[0].physicalLocation.artifactLocation.uri
4678
+ ) || a.locations[0].physicalLocation.region.startLine - b.locations[0].physicalLocation.region.startLine
4679
+ );
4680
+ const log = {
4681
+ $schema: SARIF_SCHEMA,
4682
+ version: SARIF_VERSION,
4683
+ runs: [
4684
+ {
4685
+ tool: {
4686
+ driver: {
4687
+ name: "codebase-snapshot",
4688
+ version: toolVersion,
4689
+ informationUri: "https://github.com/ankurhalder/codebase-snapshot",
4690
+ rules: sarifRules
4691
+ }
4692
+ },
4693
+ automationDetails: {
4694
+ // The snapshot's own fingerprint: two uploads of the same snapshot are the same run,
4695
+ // so Code Scanning can tell an unchanged upload from a new one.
4696
+ id: `codebase-snapshot/${manifest.fingerprint}`
4697
+ },
4698
+ originalUriBaseIds: { "%SRCROOT%": { uri: "file:///" } },
4699
+ results
4700
+ }
4701
+ ]
4702
+ };
4703
+ return `${JSON.stringify(log, null, 2)}
4704
+ `;
4705
+ }
4007
4706
 
4008
4707
  // src/partitioner/preview.ts
4009
4708
  function countLines(content) {
@@ -4053,14 +4752,29 @@ var HEADER_RESERVE_BYTES = 2048 + Buffer.byteLength(UNTRUSTED_CONTENT_NOTICE, "u
4053
4752
  var MIN_SECTION_BUDGET_BYTES = 512;
4054
4753
  var MAX_SPLIT_PASSES = 64;
4055
4754
  var BUDGET_MARKER_RESERVE = 96;
4755
+ function insertTokenLine(markdown, tokens) {
4756
+ return markdown.replace(
4757
+ /^(- Files: .*)$/m,
4758
+ `$1
4759
+ - Estimated Tokens: ~${formatTokens(tokens)}`
4760
+ );
4761
+ }
4762
+ function replaceTokenLine(markdown, tokens) {
4763
+ const line = `- Estimated Tokens: ~${formatTokens(tokens)}`;
4764
+ if (/^- Estimated Tokens: .*$/m.test(markdown)) {
4765
+ return markdown.replace(/^- Estimated Tokens: .*$/m, line);
4766
+ }
4767
+ return insertTokenLine(markdown, tokens);
4768
+ }
4056
4769
  var BUDGET_MARKER_SUFFIX = /\n\.\.\. \[\d+ lines omitted: part budget \d+ B\] \.\.\.$/;
4057
- function truncateWithBudgetMarker(content, maxBytes, budget) {
4770
+ var TOKEN_LINE = /^- Estimated Tokens: .*$/m;
4771
+ var TOKEN_LINE_SLACK = 32;
4772
+ function truncateWithBudgetMarker(content, maxBytes, budget, originalLines) {
4058
4773
  const base = content.replace(BUDGET_MARKER_SUFFIX, "");
4059
- const baseLines = countLines(base);
4060
4774
  const truncated = truncateToBytes(base, Math.max(0, maxBytes));
4061
4775
  if (!truncated.isTruncated) return { content: base, isTruncated: false };
4062
4776
  const keptLines = countLines(truncated.content);
4063
- const omitted = Math.max(1, baseLines - keptLines);
4777
+ const omitted = Math.max(1, Math.max(countLines(base), originalLines) - keptLines);
4064
4778
  return {
4065
4779
  content: `${truncated.content}
4066
4780
  ... [${omitted} lines omitted: part budget ${budget} B] ...`,
@@ -4074,7 +4788,7 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
4074
4788
  const prepared = files.map((file) => prepareSection(file, sectionBudget));
4075
4789
  let groups = greedyGroups(prepared, budget);
4076
4790
  for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
4077
- const rendered = renderGroups(groups, contentFingerprint);
4791
+ const rendered = renderGroups(groups, budget, contentFingerprint);
4078
4792
  const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
4079
4793
  if (oversized === -1) return rendered;
4080
4794
  const group = groups[oversized];
@@ -4090,28 +4804,49 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
4090
4804
  0,
4091
4805
  Buffer.byteLength(currentContent, "utf8") - overflow - BUDGET_MARKER_RESERVE
4092
4806
  );
4093
- const shrunk = truncateWithBudgetMarker(currentContent, shrinkTo, budget);
4807
+ const shrunk = truncateWithBudgetMarker(
4808
+ currentContent,
4809
+ shrinkTo,
4810
+ budget,
4811
+ section.file.originalLines
4812
+ );
4094
4813
  if (shrunk.isTruncated) {
4095
4814
  applyTruncatedContent(section.file, shrunk.content);
4096
4815
  section.text = renderFileSection(section.file);
4097
4816
  section.byteSize = Buffer.byteLength(section.text, "utf8");
4098
4817
  continue;
4099
4818
  }
4100
- const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
4101
- const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
4819
+ const marker2 = TRUNCATION_MARKER;
4820
+ assertBudgetHoldsMarker(budget);
4821
+ const target = Math.max(
4822
+ 0,
4823
+ budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
4824
+ );
4102
4825
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
4103
4826
  rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
4104
4827
  return rendered;
4105
4828
  }
4106
- return enforceBudget(renderGroups(groups, contentFingerprint), budget);
4829
+ return enforceBudget(renderGroups(groups, budget, contentFingerprint), budget);
4107
4830
  }
4108
- function enforceBudget(parts, budget) {
4109
- const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
4110
- const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
4111
- return parts.map((part) => {
4831
+ var TRUNCATION_MARKER = "\n... [part truncated: cannot fit budget] ...\n";
4832
+ function assertBudgetHoldsMarker(budget) {
4833
+ const minimum = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
4834
+ if (budget < minimum) {
4835
+ throw new Error(
4836
+ `A part budget of ${budget} B cannot hold the truncation notice (${minimum} B), so a clipped part could not fit and still say it was clipped. Use a budget of at least ${minimum} B.`
4837
+ );
4838
+ }
4839
+ }
4840
+ function enforceBudget(parts, budget) {
4841
+ const target = Math.max(
4842
+ 0,
4843
+ budget - Buffer.byteLength(TRUNCATION_MARKER, "utf8") - TOKEN_LINE_SLACK
4844
+ );
4845
+ return parts.map((part) => {
4112
4846
  if (part.metadata.byteSize <= budget) return part;
4847
+ assertBudgetHoldsMarker(budget);
4113
4848
  const clipped = truncateToBytes(part.markdown, target);
4114
- return rehashPart(part, clipped.content + marker2);
4849
+ return rehashPart(part, clipped.content + TRUNCATION_MARKER);
4115
4850
  });
4116
4851
  }
4117
4852
  function renumberParts(parts, contentFingerprint) {
@@ -4120,7 +4855,7 @@ function renumberParts(parts, contentFingerprint) {
4120
4855
  const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
4121
4856
  const nameFor = (position) => `part-${zeroPad(position)}.md`;
4122
4857
  const marker2 = "## Contents\n";
4123
- return parts.map((part, index) => {
4858
+ const renumbered = parts.map((part, index) => {
4124
4859
  const markerIndex = part.markdown.indexOf(marker2);
4125
4860
  const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker2.length);
4126
4861
  const included = part.metadata.includedFiles;
@@ -4156,19 +4891,25 @@ function renumberParts(parts, contentFingerprint) {
4156
4891
  "## Contents",
4157
4892
  ""
4158
4893
  ].join("\n");
4159
- const markdown = (header + body).replace(/\r\n/g, "\n");
4894
+ const assembled = (header + body).replace(/\r\n/g, "\n");
4895
+ const estimatedTokens = estimateTokens(assembled);
4896
+ const markdown = insertTokenLine(assembled, estimatedTokens);
4160
4897
  const metadata = {
4161
4898
  ...part.metadata,
4162
4899
  index: index + 1,
4163
4900
  filename: nameFor(index + 1),
4164
4901
  byteSize: Buffer.byteLength(markdown, "utf8"),
4165
- sha256: crypto.createHash("sha256").update(markdown).digest("hex"),
4902
+ estimatedTokens,
4903
+ sha256: crypto2.createHash("sha256").update(markdown).digest("hex"),
4166
4904
  lines: countLines(markdown),
4167
4905
  fileCount: included.length,
4168
4906
  layersCovered: Object.keys(layerCounts).sort()
4169
4907
  };
4170
4908
  return { filename: metadata.filename, markdown, metadata };
4171
4909
  });
4910
+ return renumbered.map(
4911
+ (part) => enforceBudget([part], part.metadata.budgetBytes)[0] ?? part
4912
+ );
4172
4913
  }
4173
4914
  var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
4174
4915
  var METADATA_VALUE_MAX = 200;
@@ -4192,7 +4933,12 @@ function prepareSection(file, sectionBudget) {
4192
4933
  const overhead = byteSize - Buffer.byteLength(content, "utf8");
4193
4934
  const allowed = sectionBudget - overhead - BUDGET_MARKER_RESERVE;
4194
4935
  if (allowed <= 0) return;
4195
- const truncated = truncateWithBudgetMarker(content, allowed, sectionBudget);
4936
+ const truncated = truncateWithBudgetMarker(
4937
+ content,
4938
+ allowed,
4939
+ sectionBudget,
4940
+ file.originalLines
4941
+ );
4196
4942
  if (!truncated.isTruncated) return;
4197
4943
  applyTruncatedContent(file, truncated.content);
4198
4944
  text2 = renderFileSection(file);
@@ -4222,14 +4968,17 @@ function applyTruncatedContent(file, content) {
4222
4968
  file.includedLines = countLines(content);
4223
4969
  }
4224
4970
  function rehashPart(part, markdown) {
4971
+ const estimatedTokens = estimateTokens(markdown.replace(TOKEN_LINE, ""));
4972
+ const redistributed = replaceTokenLine(markdown, estimatedTokens);
4225
4973
  return {
4226
4974
  filename: part.filename,
4227
- markdown,
4975
+ markdown: redistributed,
4228
4976
  metadata: {
4229
4977
  ...part.metadata,
4230
- byteSize: Buffer.byteLength(markdown, "utf8"),
4231
- sha256: crypto.createHash("sha256").update(markdown).digest("hex"),
4232
- lines: countLines(markdown)
4978
+ byteSize: Buffer.byteLength(redistributed, "utf8"),
4979
+ sha256: crypto2.createHash("sha256").update(redistributed).digest("hex"),
4980
+ lines: countLines(redistributed),
4981
+ estimatedTokens
4233
4982
  }
4234
4983
  };
4235
4984
  }
@@ -4258,7 +5007,7 @@ function greedyGroups(sections, budget) {
4258
5007
  if (current.length > 0) groups.push(current);
4259
5008
  return groups;
4260
5009
  }
4261
- function renderGroups(groups, contentFingerprint) {
5010
+ function renderGroups(groups, budget, contentFingerprint) {
4262
5011
  const total = groups.length;
4263
5012
  const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
4264
5013
  return groups.map((group, index) => {
@@ -4298,9 +5047,11 @@ function renderGroups(groups, contentFingerprint) {
4298
5047
  "## Contents",
4299
5048
  ""
4300
5049
  ].join("\n");
4301
- const content = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
5050
+ const assembled = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
5051
+ const estimatedTokens = estimateTokens(assembled);
5052
+ const content = insertTokenLine(assembled, estimatedTokens);
4302
5053
  const byteSize = Buffer.byteLength(content, "utf8");
4303
- const sha256 = crypto.createHash("sha256").update(content).digest("hex");
5054
+ const sha256 = crypto2.createHash("sha256").update(content).digest("hex");
4304
5055
  const includedFiles = group.map(({ file }) => ({
4305
5056
  relativePath: file.relativePath,
4306
5057
  sourceBytes: file.sizeBytes,
@@ -4313,6 +5064,8 @@ function renderGroups(groups, contentFingerprint) {
4313
5064
  index: index + 1,
4314
5065
  filename,
4315
5066
  byteSize,
5067
+ estimatedTokens,
5068
+ budgetBytes: budget,
4316
5069
  sha256,
4317
5070
  lines: countLines(content),
4318
5071
  fileCount: group.length,
@@ -4326,6 +5079,16 @@ function emittedBody(content) {
4326
5079
  const normalised = content.replace(/\r\n/g, "\n");
4327
5080
  return normalised.endsWith("\n") ? normalised.slice(0, -1) : normalised;
4328
5081
  }
5082
+ function compactMetadata(file) {
5083
+ const count = (value) => `${value} line${value === 1 ? "" : "s"}`;
5084
+ const lines = file.includedLines === file.originalLines ? count(file.originalLines) : `${count(file.includedLines)} of ${count(file.originalLines)}`;
5085
+ return [
5086
+ `- Meta: ${capMetadata(file.layer)}/${capMetadata(file.kind)}`,
5087
+ capMetadata(file.language),
5088
+ file.inclusionMode,
5089
+ `${lines} (${file.sizeBytes} B)`
5090
+ ].join(" | ");
5091
+ }
4329
5092
  function renderFileSection(file) {
4330
5093
  const content = file.content ?? "";
4331
5094
  const body = emittedBody(content);
@@ -4337,16 +5100,9 @@ function renderFileSection(file) {
4337
5100
  `<a id="${anchor}"></a>`,
4338
5101
  `### FILE: ${displayPath(file.relativePath)}`,
4339
5102
  "",
4340
- `- Layer: ${capMetadata(file.layer)}`,
4341
- `- Kind: ${capMetadata(file.kind)}`,
4342
- `- Language: ${capMetadata(file.language)}`,
4343
- `- Inclusion: ${file.inclusionMode}`,
4344
- `- Source Bytes: ${file.sizeBytes}`,
4345
- `- Source Lines: ${file.originalLines}`,
4346
- `- Included Lines: ${file.includedLines}`,
4347
- `- SHA-256: ${file.hashSha256}`,
5103
+ compactMetadata(file),
4348
5104
  ...file.summaryQuality ? [`- Summary Quality: ${file.summaryQuality}`] : [],
4349
- ...renderSummary(file.summary),
5105
+ ...renderSummary(file.summary, file.inclusionMode),
4350
5106
  ...file.summaryOmitted ? ["- Summary: (omitted: exceeds the part budget)"] : [],
4351
5107
  "",
4352
5108
  file.inclusionMode === "preview" ? "#### Bounded Preview" : "#### Content",
@@ -4366,14 +5122,15 @@ function fenceForText(content, minimum) {
4366
5122
  const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
4367
5123
  return "`".repeat(Math.max(minimum, maxRun + 1));
4368
5124
  }
4369
- function renderSummary(summary) {
5125
+ function renderSummary(summary, inclusionMode) {
4370
5126
  if (!summary) return [];
4371
5127
  if (typeof summary === "string") return [`- Summary: ${summary}`];
5128
+ if (inclusionMode === "full") return [];
4372
5129
  return ["- Summary:", "```json", JSON.stringify(summary, null, 2), "```"];
4373
5130
  }
4374
5131
  function anchorForPath(filePath) {
4375
5132
  const slug = filePath.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4376
- const digest = crypto.createHash("sha256").update(filePath).digest("hex").slice(0, 8);
5133
+ const digest = crypto2.createHash("sha256").update(filePath).digest("hex").slice(0, 8);
4377
5134
  return `${slug || "file"}-${digest}`;
4378
5135
  }
4379
5136
 
@@ -4669,7 +5426,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
4669
5426
  ".so"
4670
5427
  ]);
4671
5428
  function isBinaryExtension(filePath) {
4672
- const ext = path13.extname(filePath).toLowerCase();
5429
+ const ext = path16.extname(filePath).toLowerCase();
4673
5430
  return BINARY_EXTENSIONS.has(ext);
4674
5431
  }
4675
5432
  function isBinaryBuffer(buffer) {
@@ -4839,13 +5596,13 @@ function isBinaryFile(absolutePath) {
4839
5596
  if (isBinaryExtension(absolutePath)) return true;
4840
5597
  let fd;
4841
5598
  try {
4842
- fd = fs12.openSync(absolutePath, "r");
5599
+ fd = fs14.openSync(absolutePath, "r");
4843
5600
  const buffer = Buffer.alloc(CHUNK);
4844
5601
  let sample;
4845
5602
  let position = 0;
4846
5603
  while (position < BINARY_SCAN_BYTES) {
4847
5604
  const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
4848
- const bytesRead = fs12.readSync(fd, buffer, 0, wanted, position);
5605
+ const bytesRead = fs14.readSync(fd, buffer, 0, wanted, position);
4849
5606
  if (bytesRead === 0) return false;
4850
5607
  const chunk = buffer.subarray(0, bytesRead);
4851
5608
  if (sample === void 0) {
@@ -4863,12 +5620,145 @@ function isBinaryFile(absolutePath) {
4863
5620
  } finally {
4864
5621
  if (fd !== void 0) {
4865
5622
  try {
4866
- fs12.closeSync(fd);
5623
+ fs14.closeSync(fd);
4867
5624
  } catch {
4868
5625
  }
4869
5626
  }
4870
5627
  }
4871
5628
  }
5629
+ function gitPaths(cwd, args) {
5630
+ let output;
5631
+ try {
5632
+ output = execFileSync("git", args, {
5633
+ cwd,
5634
+ stdio: ["ignore", "pipe", "pipe"],
5635
+ encoding: "utf8",
5636
+ maxBuffer: 64 * 1024 * 1024
5637
+ });
5638
+ } catch (err) {
5639
+ const stderr = err.stderr ?? "";
5640
+ const reason = stderr.trim() || err.message;
5641
+ throw new Error(
5642
+ `Could not list changed files (git ${args.join(" ")}): ${reason}. A changed-files snapshot needs a git working tree and a ref that exists; run \`codebase-snapshot generate\` without --changed-since to scan everything.`
5643
+ );
5644
+ }
5645
+ return output.split("\0").map((entry) => entry.trim().replace(/\\/g, "/")).filter((entry) => entry.length > 0);
5646
+ }
5647
+ function assertUsableRef(ref) {
5648
+ if (ref.startsWith("-")) {
5649
+ throw new Error(
5650
+ `--changed-since must be a git ref, not an option (received ${JSON.stringify(ref)}). Name a branch, tag, or commit such as \`main\` or \`HEAD~1\`.`
5651
+ );
5652
+ }
5653
+ }
5654
+ function resolveChangedScope(cwd, request) {
5655
+ const paths = /* @__PURE__ */ new Set();
5656
+ const descriptions = [];
5657
+ if (request.changedSince !== void 0) {
5658
+ assertUsableRef(request.changedSince);
5659
+ for (const path17 of gitPaths(cwd, [
5660
+ "diff",
5661
+ "--name-only",
5662
+ "-z",
5663
+ "--relative",
5664
+ request.changedSince,
5665
+ "--"
5666
+ ])) {
5667
+ paths.add(path17);
5668
+ }
5669
+ for (const path17 of gitPaths(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--"])) {
5670
+ paths.add(path17);
5671
+ }
5672
+ descriptions.push(`changed since ${request.changedSince}`);
5673
+ }
5674
+ if (request.staged === true) {
5675
+ for (const path17 of gitPaths(cwd, [
5676
+ "diff",
5677
+ "--cached",
5678
+ "--name-only",
5679
+ "-z",
5680
+ "--relative",
5681
+ "--"
5682
+ ])) {
5683
+ paths.add(path17);
5684
+ }
5685
+ descriptions.push("staged");
5686
+ }
5687
+ return { paths, detail: descriptions.join(" and ") };
5688
+ }
5689
+ var RELATIVE_SPECIFIER = /^\.{1,2}\//;
5690
+ var IMPORT_CANDIDATE_SUFFIXES = [
5691
+ ".ts",
5692
+ ".tsx",
5693
+ ".mts",
5694
+ ".cts",
5695
+ ".js",
5696
+ ".jsx",
5697
+ ".mjs",
5698
+ ".cjs",
5699
+ "/index.ts",
5700
+ "/index.tsx",
5701
+ "/index.js",
5702
+ "/index.mjs"
5703
+ ];
5704
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5705
+ ".ts",
5706
+ ".tsx",
5707
+ ".mts",
5708
+ ".cts",
5709
+ ".js",
5710
+ ".jsx",
5711
+ ".mjs",
5712
+ ".cjs"
5713
+ ]);
5714
+ function buildImportResolver(files) {
5715
+ const byPath = /* @__PURE__ */ new Map();
5716
+ const byPathFolded = /* @__PURE__ */ new Map();
5717
+ const byStem = /* @__PURE__ */ new Map();
5718
+ const byStemFolded = /* @__PURE__ */ new Map();
5719
+ const remember = (map, key, value) => {
5720
+ if (!map.has(key)) map.set(key, value);
5721
+ };
5722
+ for (const file of files) {
5723
+ const relativePath = file.relativePath;
5724
+ remember(byPath, relativePath, relativePath);
5725
+ remember(byPathFolded, relativePath.toLowerCase(), relativePath);
5726
+ const extension = path16.posix.extname(relativePath);
5727
+ if (!SOURCE_EXTENSIONS.has(extension)) continue;
5728
+ const stem = relativePath.slice(0, -extension.length);
5729
+ remember(byStem, stem, relativePath);
5730
+ remember(byStemFolded, stem.toLowerCase(), relativePath);
5731
+ if (path16.posix.basename(stem) === "index") {
5732
+ const directory = path16.posix.dirname(stem);
5733
+ remember(byStem, directory, relativePath);
5734
+ remember(byStemFolded, directory.toLowerCase(), relativePath);
5735
+ }
5736
+ }
5737
+ return { byPath, byPathFolded, byStem, byStemFolded };
5738
+ }
5739
+ function resolveSpecifier(fromFile, specifier, resolver) {
5740
+ if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5741
+ const joined = path16.posix.normalize(
5742
+ path16.posix.join(path16.posix.dirname(fromFile), specifier)
5743
+ );
5744
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5745
+ const direct = exact(joined);
5746
+ if (direct) return direct;
5747
+ for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
5748
+ const hit = exact(`${joined}${suffix}`);
5749
+ if (hit) return hit;
5750
+ }
5751
+ const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5752
+ const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
5753
+ return byStem(joined) ?? byStem(stem);
5754
+ }
5755
+ function importsChangedFile(importerPath, imports, changedPaths, resolver) {
5756
+ for (const specifier of imports) {
5757
+ const resolved = resolveSpecifier(importerPath, specifier, resolver);
5758
+ if (resolved !== null && changedPaths.has(resolved)) return true;
5759
+ }
5760
+ return false;
5761
+ }
4872
5762
  var LANGUAGE_BY_EXTENSION = {
4873
5763
  ".ts": "typescript",
4874
5764
  ".mts": "typescript",
@@ -4902,6 +5792,8 @@ var LANGUAGE_BY_EXTENSION = {
4902
5792
  ".java": "java",
4903
5793
  ".rb": "ruby",
4904
5794
  ".php": "php",
5795
+ ".swift": "swift",
5796
+ ".dart": "dart",
4905
5797
  ".toml": "toml",
4906
5798
  ".ini": "ini",
4907
5799
  ".env": "dotenv",
@@ -4910,7 +5802,8 @@ var LANGUAGE_BY_EXTENSION = {
4910
5802
  ".graphql": "graphql",
4911
5803
  ".gql": "graphql",
4912
5804
  ".proto": "protobuf",
4913
- ".tf": "hcl"
5805
+ ".tf": "hcl",
5806
+ ".tfvars": "hcl"
4914
5807
  };
4915
5808
  var LANGUAGE_BY_BASENAME = {
4916
5809
  Dockerfile: "dockerfile",
@@ -4923,44 +5816,66 @@ var LANGUAGE_BY_BASENAME = {
4923
5816
  };
4924
5817
  function detectLanguage(filePath) {
4925
5818
  const normalized = filePath.replace(/\\/g, "/");
4926
- const base = path13.posix.basename(normalized);
5819
+ const base = path16.posix.basename(normalized);
4927
5820
  if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
4928
5821
  if (base === ".env" || base.startsWith(".env.")) return "dotenv";
4929
- const ext = path13.posix.extname(base).toLowerCase();
5822
+ const ext = path16.posix.extname(base).toLowerCase();
4930
5823
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
4931
5824
  }
5825
+ var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5826
+ var isTsSource = (relativePath) => {
5827
+ const lower = relativePath.toLowerCase();
5828
+ return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
5829
+ };
5830
+ function importsOfText(relativePath, text2) {
5831
+ if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
5832
+ return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
5833
+ }
5834
+ function importsOfFile(relativePath, absolutePath) {
5835
+ let buffer;
5836
+ try {
5837
+ buffer = fs14.readFileSync(absolutePath);
5838
+ } catch {
5839
+ return [];
5840
+ }
5841
+ const decoded = decodeText(buffer);
5842
+ if (decoded.binary) return [];
5843
+ return importsOfText(relativePath, decoded.text);
5844
+ }
5845
+
5846
+ // src/validator/validate.ts
4932
5847
  function maskedPath(value, secretRules) {
4933
5848
  return findSecretInPath(value, secretRules)?.masked ?? value;
4934
5849
  }
4935
5850
  function confineToDirectory(directory, filename) {
4936
5851
  if (filename.includes("\0")) return null;
4937
- if (path13.isAbsolute(filename)) return null;
4938
- const base = path13.resolve(directory);
4939
- const resolved = path13.resolve(base, filename);
5852
+ if (path16.isAbsolute(filename)) return null;
5853
+ const base = path16.resolve(directory);
5854
+ const resolved = path16.resolve(base, filename);
4940
5855
  const caseInsensitive = process.platform === "win32" || process.platform === "darwin";
4941
- const prefix = caseInsensitive ? `${base}${path13.sep}`.toLowerCase() : `${base}${path13.sep}`;
5856
+ const prefix = caseInsensitive ? `${base}${path16.sep}`.toLowerCase() : `${base}${path16.sep}`;
4942
5857
  const candidate = caseInsensitive ? resolved.toLowerCase() : resolved;
4943
5858
  return candidate.startsWith(prefix) ? resolved : null;
4944
5859
  }
4945
5860
  function findStagingDirectory(config) {
4946
- const tmpDir = path13.resolve(config.cwd, config.tmpDir);
5861
+ const tmpDir = path16.resolve(config.cwd, config.tmpDir);
4947
5862
  let entries;
4948
5863
  try {
4949
- entries = fs12.readdirSync(tmpDir);
5864
+ entries = fs14.readdirSync(tmpDir);
4950
5865
  } catch {
4951
5866
  return null;
4952
5867
  }
4953
5868
  const runs = entries.filter((name) => name.startsWith("run-")).sort();
4954
5869
  const newest = runs[runs.length - 1];
4955
- return newest ? path13.join(tmpDir, newest) : null;
5870
+ return newest ? path16.join(tmpDir, newest) : null;
4956
5871
  }
4957
5872
  function describeMissingManifest(config, manifestPath) {
4958
- const outDir = path13.resolve(config.cwd, config.outDir);
4959
- const markerPath = path13.join(outDir, ".codebase-snapshot");
5873
+ const outDir = path16.resolve(config.cwd, config.outDir);
5874
+ const markerPath = path16.join(outDir, ".codebase-snapshot");
4960
5875
  let marker2 = null;
4961
5876
  try {
4962
- if (fs12.existsSync(markerPath)) {
4963
- const parsed = JSON.parse(fs12.readFileSync(markerPath, "utf8"));
5877
+ if (fs14.existsSync(markerPath)) {
5878
+ const parsed = JSON.parse(fs14.readFileSync(markerPath, "utf8"));
4964
5879
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4965
5880
  marker2 = parsed;
4966
5881
  }
@@ -4992,18 +5907,18 @@ function emptyDrift() {
4992
5907
  };
4993
5908
  }
4994
5909
  function sha256Hex(buffer) {
4995
- return crypto.createHash("sha256").update(buffer).digest("hex");
5910
+ return crypto2.createHash("sha256").update(buffer).digest("hex");
4996
5911
  }
4997
5912
  var VERIFY_SNIFF_MAX_BYTES = 8 * 1024 * 1024;
4998
- function wouldNowBeIncluded(cwd, relativePath, reason) {
4999
- const absolutePath = path13.join(cwd, relativePath);
5913
+ function wouldNowBeIncluded(cwd, relativePath) {
5914
+ const absolutePath = path16.join(cwd, relativePath);
5000
5915
  try {
5001
- const stats = fs12.statSync(absolutePath);
5916
+ const stats = fs14.statSync(absolutePath);
5002
5917
  if (!stats.isFile()) return false;
5003
- if (isBinaryFile(absolutePath)) return false;
5004
- if (reason !== "binary") return true;
5005
- if (stats.size > VERIFY_SNIFF_MAX_BYTES) return false;
5006
- return !decodeText(fs12.readFileSync(absolutePath)).binary;
5918
+ if (stats.size <= VERIFY_SNIFF_MAX_BYTES) {
5919
+ return !decodeText(fs14.readFileSync(absolutePath)).binary;
5920
+ }
5921
+ return !isBinaryFile(absolutePath);
5007
5922
  } catch {
5008
5923
  return false;
5009
5924
  }
@@ -5061,8 +5976,8 @@ function describeManifestShapeProblem(value) {
5061
5976
  function validateSnapshot(configInput) {
5062
5977
  assertUsableConfigInput(configInput);
5063
5978
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
5064
- const manifestPath = path13.resolve(config.cwd, config.outDir, "manifest.json");
5065
- if (!fs12.existsSync(manifestPath)) {
5979
+ const manifestPath = path16.resolve(config.cwd, config.outDir, "manifest.json");
5980
+ if (!fs14.existsSync(manifestPath)) {
5066
5981
  return {
5067
5982
  valid: false,
5068
5983
  manifestPresent: false,
@@ -5073,7 +5988,7 @@ function validateSnapshot(configInput) {
5073
5988
  }
5074
5989
  let manifest;
5075
5990
  try {
5076
- manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
5991
+ manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf8"));
5077
5992
  } catch (err) {
5078
5993
  return {
5079
5994
  valid: false,
@@ -5095,7 +6010,7 @@ function validateSnapshot(configInput) {
5095
6010
  }
5096
6011
  const drift = emptyDrift();
5097
6012
  const warnings = [];
5098
- const snapshotDir = path13.resolve(config.cwd, config.outDir);
6013
+ const snapshotDir = path16.resolve(config.cwd, config.outDir);
5099
6014
  const manifestFileMap = /* @__PURE__ */ new Map();
5100
6015
  for (const file of manifest.files) {
5101
6016
  if (file && typeof file.relativePath === "string") {
@@ -5113,23 +6028,72 @@ function validateSnapshot(configInput) {
5113
6028
  if (direct !== void 0) return direct;
5114
6029
  return skippedReasons.get(maskedPath(diskFile, config.secretRules)) ?? null;
5115
6030
  };
6031
+ let unchangedScope = null;
6032
+ const recordedChangedSince = manifest.config?.changedSince;
6033
+ const recordedStaged = manifest.config?.staged === true;
6034
+ const recordedWithDependents = manifest.config?.withDependents === true;
6035
+ if (recordedChangedSince !== void 0 || recordedStaged) {
6036
+ try {
6037
+ unchangedScope = new Set(
6038
+ resolveChangedScope(config.cwd, {
6039
+ ...recordedChangedSince === void 0 ? {} : { changedSince: recordedChangedSince },
6040
+ ...recordedStaged ? { staged: true } : {}
6041
+ }).paths
6042
+ );
6043
+ } catch {
6044
+ warnings.push(
6045
+ "The snapshot was generated with a changed-files scope, but that scope could not be re-resolved; files it recorded as `unchanged` were trusted rather than re-checked."
6046
+ );
6047
+ }
6048
+ }
6049
+ let dependentResolver = null;
6050
+ function becameDependent(diskFile) {
6051
+ if (!recordedWithDependents || unchangedScope === null) return false;
6052
+ dependentResolver ??= buildImportResolver(
6053
+ collection.files.map((file) => ({ relativePath: file }))
6054
+ );
6055
+ return importsChangedFile(
6056
+ diskFile,
6057
+ importsOfFile(diskFile, path16.join(config.cwd, diskFile)),
6058
+ unchangedScope,
6059
+ dependentResolver
6060
+ );
6061
+ }
5116
6062
  const collection = collectFiles(config.cwd, {
5117
6063
  includePatterns: config.include,
5118
6064
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5119
6065
  includeSensitiveFiles: config.includeSensitiveFiles,
5120
- secretRules: config.secretRules
6066
+ secretRules: config.secretRules,
6067
+ // Passed explicitly, like `generate` does. Relying on a process-wide "current
6068
+ // discovery mode" meant `validate` could enumerate files one way while the snapshot
6069
+ // had been generated another way, and then blame the difference on drift.
6070
+ discovery: config.discovery,
6071
+ // The same exclusion `generate` applies: without it, `validate` recounted the tool's
6072
+ // own output directory as a set of files missing from the snapshot, so a correct
6073
+ // snapshot was reported as drifting.
6074
+ toolOwnedPaths: [config.outDir, config.tmpDir].map(
6075
+ (directory) => path16.relative(config.cwd, path16.resolve(config.cwd, directory)).replace(/\\/g, "/")
6076
+ ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
5121
6077
  });
5122
6078
  for (const diskFile of collection.files) {
5123
6079
  const snapshotHash = manifestFileMap.get(diskFile);
5124
6080
  if (!snapshotHash) {
5125
6081
  const skipReason = skippedReasonOnDisk(diskFile);
5126
- if (skipReason !== null && !wouldNowBeIncluded(config.cwd, diskFile, skipReason)) continue;
6082
+ if (skipReason === "unchanged") {
6083
+ if (unchangedScope !== null) {
6084
+ if (unchangedScope.has(diskFile) || becameDependent(diskFile)) {
6085
+ drift.missingInSnapshot.push(diskFile);
6086
+ }
6087
+ }
6088
+ continue;
6089
+ }
6090
+ if (skipReason !== null && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
5127
6091
  drift.missingInSnapshot.push(diskFile);
5128
6092
  continue;
5129
6093
  }
5130
6094
  let diskHash;
5131
6095
  try {
5132
- diskHash = sha256Hex(fs12.readFileSync(path13.join(config.cwd, diskFile)));
6096
+ diskHash = sha256Hex(fs14.readFileSync(path16.join(config.cwd, diskFile)));
5133
6097
  } catch (err) {
5134
6098
  if (err.code === "ENOENT") {
5135
6099
  drift.missingOnDisk.push(diskFile);
@@ -5153,7 +6117,7 @@ function validateSnapshot(configInput) {
5153
6117
  }
5154
6118
  let statFailure = null;
5155
6119
  try {
5156
- fs12.statSync(confined);
6120
+ fs14.statSync(confined);
5157
6121
  } catch (err) {
5158
6122
  statFailure = err;
5159
6123
  }
@@ -5173,7 +6137,7 @@ function validateSnapshot(configInput) {
5173
6137
  ...Object.values(config.budgets.byLayer)
5174
6138
  );
5175
6139
  const partNames = /* @__PURE__ */ new Set();
5176
- const partsDir = path13.join(snapshotDir, "parts");
6140
+ const partsDir = path16.join(snapshotDir, "parts");
5177
6141
  for (const part of manifest.parts) {
5178
6142
  if (!part || typeof part.filename !== "string") continue;
5179
6143
  partNames.add(part.filename);
@@ -5188,7 +6152,7 @@ function validateSnapshot(configInput) {
5188
6152
  }
5189
6153
  let buffer;
5190
6154
  try {
5191
- buffer = fs12.readFileSync(partPath);
6155
+ buffer = fs14.readFileSync(partPath);
5192
6156
  } catch {
5193
6157
  drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
5194
6158
  continue;
@@ -5202,10 +6166,11 @@ function validateSnapshot(configInput) {
5202
6166
  if (sha256Hex(buffer) !== part.sha256) {
5203
6167
  drift.partIssues.push({ part: part.filename, issue: "sha256 does not match the manifest" });
5204
6168
  }
5205
- if (buffer.length > maxEffectiveBudget) {
6169
+ const partBudget = typeof part.budgetBytes === "number" ? part.budgetBytes : maxEffectiveBudget;
6170
+ if (buffer.length > partBudget) {
5206
6171
  drift.partIssues.push({
5207
6172
  part: part.filename,
5208
- issue: `${buffer.length} B exceeds the effective part budget ${maxEffectiveBudget} B`
6173
+ issue: typeof part.budgetBytes === "number" ? `${buffer.length} B exceeds its part budget ${partBudget} B` : `${buffer.length} B exceeds the effective part budget ${partBudget} B`
5209
6174
  });
5210
6175
  }
5211
6176
  }
@@ -5402,9 +6367,9 @@ function validateSnapshot(configInput) {
5402
6367
  ["full-codebase.md", manifest.artifacts?.fullMarkdown]
5403
6368
  ]) {
5404
6369
  if (!artifact) continue;
5405
- const artifactPath = path13.join(snapshotDir, key);
6370
+ const artifactPath = path16.join(snapshotDir, key);
5406
6371
  try {
5407
- const buffer = fs12.readFileSync(artifactPath);
6372
+ const buffer = fs14.readFileSync(artifactPath);
5408
6373
  if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
5409
6374
  drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
5410
6375
  }
@@ -5412,6 +6377,18 @@ function validateSnapshot(configInput) {
5412
6377
  drift.partIssues.push({ part: key, issue: "declared artifact is missing" });
5413
6378
  }
5414
6379
  }
6380
+ const sarifArtifact = manifest.artifacts?.sarif;
6381
+ if (sarifArtifact) {
6382
+ const sarifKey = "reports/audit.sarif";
6383
+ try {
6384
+ const buffer = fs14.readFileSync(path16.join(snapshotDir, "reports", "audit.sarif"));
6385
+ if (sha256Hex(buffer) !== sarifArtifact.sha256 || buffer.length !== sarifArtifact.byteSize) {
6386
+ drift.partIssues.push({ part: sarifKey, issue: "artifact does not match its recorded digest" });
6387
+ }
6388
+ } catch {
6389
+ drift.partIssues.push({ part: sarifKey, issue: "declared artifact is missing" });
6390
+ }
6391
+ }
5415
6392
  if (manifest.git && manifest.git.branch && manifest.git.branch !== config.branch) {
5416
6393
  warnings.push(
5417
6394
  `Snapshot was generated on branch "${manifest.git.branch}" but the configured branch is "${config.branch}".`
@@ -5474,7 +6451,7 @@ function validateSnapshot(configInput) {
5474
6451
 
5475
6452
  // src/version.ts
5476
6453
  var GENERATOR_NAME = "codebase-snapshot";
5477
- var GENERATOR_VERSION = "0.9.8";
6454
+ var GENERATOR_VERSION = "0.9.12";
5478
6455
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
5479
6456
  function isOurGeneratorName(name) {
5480
6457
  if (typeof name !== "string") return false;
@@ -5484,6 +6461,23 @@ var PACKAGE_LATEST_URL = "https://registry.npmjs.org/codebase-snapshot/latest";
5484
6461
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
5485
6462
  var FAILURE_INTERVAL_MS = 60 * 60 * 1e3;
5486
6463
  var REQUEST_TIMEOUT_MS = 1200;
6464
+ function installKind(scriptPath = process.argv[1] ?? "", cwd = process.cwd()) {
6465
+ const normalise = (value) => value.replace(/\\/g, "/").replace(/\/+$/, "");
6466
+ const script = normalise(scriptPath);
6467
+ if (script === "") return "global";
6468
+ const scriptKey = script.toLowerCase();
6469
+ if (scriptKey.includes("/_npx/")) return "npx";
6470
+ const root = normalise(cwd).toLowerCase();
6471
+ if (scriptKey.includes("/node_modules/") && root !== "" && scriptKey.startsWith(`${root}/`)) {
6472
+ return "local";
6473
+ }
6474
+ return "global";
6475
+ }
6476
+ function updateCommand(kind) {
6477
+ if (kind === "npx") return "npx -y codebase-snapshot@latest";
6478
+ if (kind === "local") return "npm install codebase-snapshot@latest";
6479
+ return "npm install -g codebase-snapshot";
6480
+ }
5487
6481
  function versionParts(value) {
5488
6482
  const core = value.trim().replace(/^v/i, "").split("-")[0];
5489
6483
  return core.split(".").map((part) => {
@@ -5504,17 +6498,17 @@ function isNewer(candidate, current) {
5504
6498
  }
5505
6499
  function cacheDirectory(env = process.env, platform = process.platform) {
5506
6500
  if (platform === "win32") {
5507
- const base2 = env.LOCALAPPDATA ?? path13.join(os.homedir(), "AppData", "Local");
5508
- return path13.join(base2, "codebase-snapshot");
6501
+ const base2 = env.LOCALAPPDATA ?? path16.join(os.homedir(), "AppData", "Local");
6502
+ return path16.join(base2, "codebase-snapshot");
5509
6503
  }
5510
6504
  if (platform === "darwin") {
5511
- return path13.join(os.homedir(), "Library", "Caches", "codebase-snapshot");
6505
+ return path16.join(os.homedir(), "Library", "Caches", "codebase-snapshot");
5512
6506
  }
5513
- const base = env.XDG_CACHE_HOME ?? path13.join(os.homedir(), ".cache");
5514
- return path13.join(base, "codebase-snapshot");
6507
+ const base = env.XDG_CACHE_HOME ?? path16.join(os.homedir(), ".cache");
6508
+ return path16.join(base, "codebase-snapshot");
5515
6509
  }
5516
6510
  function cacheFilePath(env = process.env) {
5517
- return path13.join(cacheDirectory(env), "update-check.json");
6511
+ return path16.join(cacheDirectory(env), "update-check.json");
5518
6512
  }
5519
6513
  function shouldCheck(argv, env) {
5520
6514
  const disabled = ["1", "true", "yes"].includes(
@@ -5527,13 +6521,23 @@ function shouldCheck(argv, env) {
5527
6521
  for (const skip of ["mcp", "describe", "skill", "--version", "-v", "--help", "-h"]) {
5528
6522
  if (args.includes(skip)) return false;
5529
6523
  }
5530
- if (args.some((argument) => argument.includes("json"))) return false;
6524
+ for (let index = 0; index < args.length; index += 1) {
6525
+ const argument = args[index];
6526
+ if (argument === "--format") {
6527
+ if (args[index + 1] === "json") return false;
6528
+ break;
6529
+ }
6530
+ if (argument.startsWith("--format=")) {
6531
+ if (argument.slice("--format=".length) === "json") return false;
6532
+ break;
6533
+ }
6534
+ }
5531
6535
  return true;
5532
6536
  }
5533
6537
  function readCache(file) {
5534
6538
  let raw;
5535
6539
  try {
5536
- raw = fs12.readFileSync(file, "utf8");
6540
+ raw = fs14.readFileSync(file, "utf8");
5537
6541
  } catch {
5538
6542
  return null;
5539
6543
  }
@@ -5550,8 +6554,8 @@ function readCache(file) {
5550
6554
  }
5551
6555
  function writeCache(file, entry) {
5552
6556
  try {
5553
- fs12.mkdirSync(path13.dirname(file), { recursive: true });
5554
- fs12.writeFileSync(file, `${JSON.stringify(entry)}
6557
+ fs14.mkdirSync(path16.dirname(file), { recursive: true });
6558
+ fs14.writeFileSync(file, `${JSON.stringify(entry)}
5555
6559
  `, "utf8");
5556
6560
  } catch {
5557
6561
  }
@@ -5632,9 +6636,9 @@ async function checkForUpdate(options) {
5632
6636
  if (!latest) return null;
5633
6637
  return isNewer(latest, options.current) ? { current: options.current, latest } : null;
5634
6638
  }
5635
- function formatUpdateNotice(notice) {
6639
+ function formatUpdateNotice(notice, kind) {
5636
6640
  return `\u2191 codebase-snapshot ${notice.latest} is available \u2014 you have ${notice.current}.
5637
- npm install -g codebase-snapshot`;
6641
+ ${updateCommand(kind)}`;
5638
6642
  }
5639
6643
  function describeMatcher(matcher) {
5640
6644
  if (typeof matcher === "string") return matcher;
@@ -5645,42 +6649,42 @@ function describeMatcher(matcher) {
5645
6649
  return String(matcher);
5646
6650
  }
5647
6651
  function normalizeTarget(cwd, target) {
5648
- const absolute = path13.resolve(cwd, target);
5649
- const relative7 = path13.relative(cwd, absolute);
5650
- if (relative7.startsWith("..") || path13.isAbsolute(relative7)) return target;
5651
- return relative7.split(path13.sep).join("/");
6652
+ const absolute = path16.resolve(cwd, target);
6653
+ const relative8 = path16.relative(cwd, absolute);
6654
+ if (relative8.startsWith("..") || path16.isAbsolute(relative8)) return target;
6655
+ return relative8.split(path16.sep).join("/");
5652
6656
  }
5653
6657
  function findRuleMatch(rules, relativePath) {
5654
6658
  return rules.find(
5655
6659
  (rule) => matchRule(relativePath, rule.match)
5656
6660
  );
5657
6661
  }
5658
- function findRecordedAncestor(skipped, relative7, maskedPath2) {
6662
+ function findRecordedAncestor(skipped, relative8, maskedPath2) {
5659
6663
  let nearest;
5660
6664
  for (const record of skipped) {
5661
6665
  const ancestorPath = record.path;
5662
6666
  if (ancestorPath.length === 0) continue;
5663
- const isAncestor = relative7.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
6667
+ const isAncestor = relative8.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
5664
6668
  if (!isAncestor) continue;
5665
6669
  if (!nearest || ancestorPath.length > nearest.path.length) nearest = record;
5666
6670
  }
5667
6671
  return nearest;
5668
6672
  }
5669
6673
  function readManifest2(config) {
5670
- const manifestPath = path13.resolve(config.cwd, config.outDir, "manifest.json");
6674
+ const manifestPath = path16.resolve(config.cwd, config.outDir, "manifest.json");
5671
6675
  try {
5672
- return JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
6676
+ return JSON.parse(fs14.readFileSync(manifestPath, "utf8"));
5673
6677
  } catch {
5674
6678
  return null;
5675
6679
  }
5676
6680
  }
5677
6681
  function explainPath(config, target) {
5678
- const relative7 = normalizeTarget(config.cwd, target);
5679
- const absolute = path13.join(config.cwd, relative7);
5680
- const maskedPath2 = findSecretInPath(relative7, config.secretRules)?.masked ?? relative7;
6682
+ const relative8 = normalizeTarget(config.cwd, target);
6683
+ const absolute = path16.join(config.cwd, relative8);
6684
+ const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
5681
6685
  const result = {
5682
6686
  path: maskedPath2,
5683
- exists: fs12.existsSync(absolute),
6687
+ exists: fs14.existsSync(absolute),
5684
6688
  included: false
5685
6689
  };
5686
6690
  const collection = collectFiles(config.cwd, {
@@ -5688,15 +6692,18 @@ function explainPath(config, target) {
5688
6692
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5689
6693
  includeSensitiveFiles: config.includeSensitiveFiles,
5690
6694
  secretRules: config.secretRules,
6695
+ // `explain` must enumerate files exactly as `generate` would, or it explains a
6696
+ // decision the generator never made.
6697
+ discovery: config.discovery,
5691
6698
  // Same exclusion `generate` applies, so `explain` never says "not-discovered" about a file
5692
6699
  // that is only missing because the walker dropped the tool's own output directory.
5693
6700
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
5694
- (directory) => path13.relative(config.cwd, path13.resolve(config.cwd, directory)).replace(/\\/g, "/")
5695
- ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
6701
+ (directory) => path16.relative(config.cwd, path16.resolve(config.cwd, directory)).replace(/\\/g, "/")
6702
+ ).filter((relative9) => relative9.length > 0 && !relative9.startsWith(".."))
5696
6703
  });
5697
- if (!collection.files.includes(relative7)) {
6704
+ if (!collection.files.includes(relative8)) {
5698
6705
  const record = collection.skipped.find(
5699
- (entry2) => entry2.path === relative7 || entry2.path === maskedPath2
6706
+ (entry2) => entry2.path === relative8 || entry2.path === maskedPath2
5700
6707
  );
5701
6708
  if (record) {
5702
6709
  result.reason = record.reason;
@@ -5705,7 +6712,7 @@ function explainPath(config, target) {
5705
6712
  result.reason = "missing";
5706
6713
  result.detail = "no such file or directory, relative to the configured cwd";
5707
6714
  } else {
5708
- const ancestor = findRecordedAncestor(collection.skipped, relative7, maskedPath2);
6715
+ const ancestor = findRecordedAncestor(collection.skipped, relative8, maskedPath2);
5709
6716
  if (ancestor) {
5710
6717
  result.reason = ancestor.reason;
5711
6718
  result.detail = ancestor.detail ? `${ancestor.detail} \u2014 inherited from the skipped directory \`${ancestor.path}\`` : `inside the skipped directory \`${ancestor.path}\``;
@@ -5717,21 +6724,58 @@ function explainPath(config, target) {
5717
6724
  return result;
5718
6725
  }
5719
6726
  result.included = true;
5720
- if (isBinaryFile(absolute)) {
6727
+ if (config.changedSince !== void 0 || config.staged === true) {
6728
+ let inScope;
6729
+ let scopeDetail = "";
6730
+ try {
6731
+ const scope = resolveChangedScope(config.cwd, {
6732
+ ...config.changedSince === void 0 ? {} : { changedSince: config.changedSince },
6733
+ ...config.staged === true ? { staged: true } : {}
6734
+ });
6735
+ inScope = scope.paths.has(relative8);
6736
+ if (!inScope && config.withDependents === true) {
6737
+ inScope = importsChangedFile(
6738
+ relative8,
6739
+ importsOfFile(relative8, absolute),
6740
+ scope.paths,
6741
+ buildImportResolver(collection.files.map((file) => ({ relativePath: file })))
6742
+ );
6743
+ }
6744
+ scopeDetail = scope.detail;
6745
+ } catch (err) {
6746
+ result.included = false;
6747
+ result.reason = "unknown";
6748
+ result.detail = `the changed-files scope could not be resolved: ${err.message}`;
6749
+ return result;
6750
+ }
6751
+ if (!inScope) {
6752
+ result.included = false;
6753
+ result.reason = "unchanged";
6754
+ result.detail = `not ${scopeDetail}; the snapshot was generated with a changed-files scope, which keeps only the files that moved (plus dependents, with --with-dependents)`;
6755
+ return result;
6756
+ }
6757
+ }
6758
+ let sizeOnDisk = null;
6759
+ try {
6760
+ sizeOnDisk = fs14.statSync(absolute).size;
6761
+ } catch {
6762
+ sizeOnDisk = null;
6763
+ }
6764
+ if (sizeOnDisk !== null && sizeOnDisk > BINARY_SCAN_BYTES && isBinaryFile(absolute)) {
5721
6765
  result.included = false;
5722
6766
  result.reason = "binary";
5723
6767
  result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
5724
6768
  return result;
5725
6769
  }
5726
- result.layer = classifyLayer(relative7, config.layers);
5727
- result.kind = classifyKind(relative7, config.kinds);
5728
- result.language = detectLanguage(relative7);
5729
- const layerRule = findRuleMatch(config.layers, relative7);
6770
+ result.layer = classifyLayer(relative8, config.layers);
6771
+ result.kind = classifyKind(relative8, config.kinds);
6772
+ result.language = detectLanguage(relative8);
6773
+ const layerRule = findRuleMatch(config.layers, relative8);
5730
6774
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
5731
- const kindRule = findRuleMatch(config.kinds, relative7);
6775
+ const kindRule = findRuleMatch(config.kinds, relative8);
5732
6776
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
5733
- if (fs12.existsSync(absolute) && fs12.statSync(absolute).isFile()) {
5734
- const buffer = fs12.readFileSync(absolute);
6777
+ if (fs14.existsSync(absolute) && fs14.statSync(absolute).isFile()) {
6778
+ const buffer = fs14.readFileSync(absolute);
5735
6779
  const decoded = decodeText(buffer);
5736
6780
  if (decoded.binary) {
5737
6781
  result.included = false;
@@ -5743,12 +6787,12 @@ function explainPath(config, target) {
5743
6787
  result.bytes = buffer.length;
5744
6788
  result.lines = countLines(text2);
5745
6789
  if (decoded.encoding !== "utf-8") result.encoding = decoded.encoding;
5746
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative7);
6790
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative8);
5747
6791
  result.secretCount = findings.length;
5748
6792
  result.secrets = findings.slice(0, 20).map((finding) => ({ rules: finding.rules, line: finding.line }));
5749
6793
  }
5750
6794
  const manifest = readManifest2(config);
5751
- const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative7) : void 0;
6795
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative8) : void 0;
5752
6796
  result.part = entry?.part ?? null;
5753
6797
  result.inclusionMode = entry?.inclusionMode;
5754
6798
  return result;
@@ -5979,14 +7023,14 @@ function compareManifests(before, after) {
5979
7023
  function loadManifest(target) {
5980
7024
  let isDirectory;
5981
7025
  try {
5982
- isDirectory = fs12.statSync(target).isDirectory();
7026
+ isDirectory = fs14.statSync(target).isDirectory();
5983
7027
  } catch {
5984
7028
  throw new Error(`No snapshot found at ${target}: the path does not exist.`);
5985
7029
  }
5986
- const manifestPath = isDirectory ? path13.join(target, "manifest.json") : target;
7030
+ const manifestPath = isDirectory ? path16.join(target, "manifest.json") : target;
5987
7031
  let raw;
5988
7032
  try {
5989
- raw = fs12.readFileSync(manifestPath, "utf8");
7033
+ raw = fs14.readFileSync(manifestPath, "utf8");
5990
7034
  } catch {
5991
7035
  throw new Error(`Could not read a manifest at ${manifestPath}.`);
5992
7036
  }
@@ -6043,15 +7087,15 @@ function renderDiffText(diff) {
6043
7087
  function appendGitignoreEntries(cwd, entries) {
6044
7088
  const wanted = [...new Set(entries)].filter((entry) => entry.trim().length > 0);
6045
7089
  if (wanted.length === 0) return { added: [], note: null };
6046
- const ignorePath = path13.join(cwd, ".gitignore");
7090
+ const ignorePath = path16.join(cwd, ".gitignore");
6047
7091
  const manual = `Add ${wanted.map((entry) => `"${entry}"`).join(" and ")} to your .gitignore yourself.`;
6048
7092
  let existing = "";
6049
7093
  try {
6050
- if (fs12.existsSync(ignorePath)) {
6051
- if (fs12.statSync(ignorePath).isDirectory()) {
7094
+ if (fs14.existsSync(ignorePath)) {
7095
+ if (fs14.statSync(ignorePath).isDirectory()) {
6052
7096
  return { added: [], note: `${ignorePath} is a directory, so it was not updated. ${manual}` };
6053
7097
  }
6054
- existing = fs12.readFileSync(ignorePath, "utf8");
7098
+ existing = fs14.readFileSync(ignorePath, "utf8");
6055
7099
  }
6056
7100
  } catch (err) {
6057
7101
  return {
@@ -6064,7 +7108,7 @@ function appendGitignoreEntries(cwd, entries) {
6064
7108
  if (added.length === 0) return { added: [], note: null };
6065
7109
  const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
6066
7110
  try {
6067
- fs12.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
7111
+ fs14.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
6068
7112
  `, "utf8");
6069
7113
  } catch (err) {
6070
7114
  return { added: [], note: `${err.message}. ${manual}` };
@@ -6072,7 +7116,7 @@ function appendGitignoreEntries(cwd, entries) {
6072
7116
  return { added, note: null };
6073
7117
  }
6074
7118
  function gitignorePath(cwd, filePath) {
6075
- return path13.relative(cwd, filePath).split(path13.sep).join("/");
7119
+ return path16.relative(cwd, filePath).split(path16.sep).join("/");
6076
7120
  }
6077
7121
 
6078
7122
  // src/wizard/config-template.ts
@@ -6080,7 +7124,12 @@ var DEFAULT_ANSWERS = {
6080
7124
  preset: "generic",
6081
7125
  branch: "main",
6082
7126
  maxPartBytes: "300k",
7127
+ maxPartTokens: null,
6083
7128
  largeFileBytes: "200k",
7129
+ maxMonolithBytes: null,
7130
+ discovery: "auto",
7131
+ skeleton: false,
7132
+ sarif: false,
6084
7133
  include: [],
6085
7134
  excludePatterns: [],
6086
7135
  artifacts: {
@@ -6133,6 +7182,9 @@ function renderConfig(answers) {
6133
7182
  // \u2500\u2500 Size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6134
7183
  // Hard ceiling per part. "300k", 307200 and "1.5mb" are all accepted.
6135
7184
  maxPartBytes: ${size(answers.maxPartBytes)},
7185
+ // The same ceiling expressed in tokens; it narrows maxPartBytes rather than
7186
+ // replacing it. null = not set. Minimum 1024.
7187
+ maxPartTokens: ${answers.maxPartTokens === null ? "null" : String(answers.maxPartTokens)},
6136
7188
  // Files above this size get a head/tail preview instead of the full body.
6137
7189
  largeFileBytes: ${size(answers.largeFileBytes)},
6138
7190
  previewHeadLines: 80,
@@ -6143,6 +7195,22 @@ function renderConfig(answers) {
6143
7195
  byLayer: {},
6144
7196
  },
6145
7197
 
7198
+ // \u2500\u2500 Scope and compression \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7199
+ // Replace function and method bodies with \`{ /* ... */ }\` in TypeScript files.
7200
+ // Signatures and comments stay, so the structure of the file survives.
7201
+ skeleton: ${answers.skeleton},
7202
+ // Also write reports/audit.sarif (SARIF 2.1.0) for a CI security gate. The
7203
+ // matched value is never included \u2014 only the path, the line and the rule.
7204
+ sarif: ${answers.sarif},
7205
+ // Snapshot only the files that differ from a git ref (for example "main"),
7206
+ // plus untracked ones. null and false both mean "snapshot everything".
7207
+ changedSince: null,
7208
+ // Snapshot only what is staged in the index: the pre-commit counterpart.
7209
+ staged: false,
7210
+ // With changedSince or staged, also include the files that import a changed
7211
+ // one, one hop out. Meaningless without a scope.
7212
+ withDependents: false,
7213
+
6146
7214
  // \u2500\u2500 What goes in \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6147
7215
  // Allow-list. Empty means "everything that is not ignored".
6148
7216
  // Example: ["src/**", "packages/*/src/**"]
@@ -6152,7 +7220,7 @@ function renderConfig(answers) {
6152
7220
  // How candidate files are found: "auto" lists them through git when git can
6153
7221
  // and walks the tree otherwise; "git" and "walk" force one path, and "walk" is
6154
7222
  // the escape hatch when .git exists but git cannot list it.
6155
- discovery: "auto",
7223
+ discovery: ${JSON.stringify(answers.discovery)},
6156
7224
 
6157
7225
  // \u2500\u2500 Classification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6158
7226
  // "merge" = the preset's rules plus yours (yours win). "replace" = yours only.
@@ -6183,7 +7251,7 @@ function renderConfig(answers) {
6183
7251
  monolith: ${artifacts.monolith}, // full-codebase.json + .md (roughly doubles output)
6184
7252
  },
6185
7253
  // Skip the monolith above this size. null = no limit.
6186
- maxMonolithBytes: null,
7254
+ maxMonolithBytes: ${answers.maxMonolithBytes === null ? "null" : size(answers.maxMonolithBytes)},
6187
7255
  // Top-level switch for the monolith, equivalent to artifacts.monolith.
6188
7256
  // When both are present the artifact entry wins; this one exists so the
6189
7257
  // generated file lists every setting the tool accepts.
@@ -6560,10 +7628,10 @@ var CONFIG_CANDIDATES = [
6560
7628
  ];
6561
7629
  function chooseConfigFileName(cwd) {
6562
7630
  if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
6563
- const pkgPath = path13.join(cwd, "package.json");
6564
- if (!fs12.existsSync(pkgPath)) return "snapshot.config.mjs";
7631
+ const pkgPath = path16.join(cwd, "package.json");
7632
+ if (!fs14.existsSync(pkgPath)) return "snapshot.config.mjs";
6565
7633
  try {
6566
- const parsed = JSON.parse(fs12.readFileSync(pkgPath, "utf8"));
7634
+ const parsed = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
6567
7635
  return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
6568
7636
  } catch {
6569
7637
  return "snapshot.config.mjs";
@@ -6577,7 +7645,7 @@ var SIZE_CHOICES = [
6577
7645
  ];
6578
7646
  function packageJsonHasConfig(cwd) {
6579
7647
  try {
6580
- const parsed = JSON.parse(fs12.readFileSync(path13.join(cwd, "package.json"), "utf8"));
7648
+ const parsed = JSON.parse(fs14.readFileSync(path16.join(cwd, "package.json"), "utf8"));
6581
7649
  return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
6582
7650
  } catch {
6583
7651
  return false;
@@ -6594,11 +7662,12 @@ function artifactAnswers(options) {
6594
7662
  const keep = new Set(options.onlyArtifacts);
6595
7663
  for (const name of ARTIFACT_NAMES) artifacts[name] = keep.has(name);
6596
7664
  }
7665
+ if (options.includeMonolith === true) artifacts.monolith = true;
6597
7666
  return artifacts;
6598
7667
  }
6599
7668
  function topLevelDirs(cwd) {
6600
7669
  try {
6601
- return fs12.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
7670
+ return fs14.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
6602
7671
  } catch {
6603
7672
  return [];
6604
7673
  }
@@ -6611,7 +7680,7 @@ function describeJsonValue(value) {
6611
7680
  }
6612
7681
  function writeFileOrExplain(target, content) {
6613
7682
  try {
6614
- fs12.writeFileSync(target, content, "utf8");
7683
+ fs14.writeFileSync(target, content, "utf8");
6615
7684
  } catch (err) {
6616
7685
  const code = err.code;
6617
7686
  const reason = code === "EACCES" || code === "EPERM" || code === "EROFS" ? `permission denied (${code})` : code === "EISDIR" ? "it is a directory" : `${err.message}`;
@@ -6622,7 +7691,7 @@ function canWriteModuleConfig(name, cwd) {
6622
7691
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
6623
7692
  if (!name.endsWith(".js")) return false;
6624
7693
  try {
6625
- const parsed = JSON.parse(fs12.readFileSync(path13.join(cwd, "package.json"), "utf8"));
7694
+ const parsed = JSON.parse(fs14.readFileSync(path16.join(cwd, "package.json"), "utf8"));
6626
7695
  return parsed.type === "module";
6627
7696
  } catch {
6628
7697
  return false;
@@ -6657,19 +7726,19 @@ function serializePackageJson(raw, value) {
6657
7726
  return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
6658
7727
  }
6659
7728
  function updatePackageJson(cwd, notes) {
6660
- const packagePath = path13.join(cwd, "package.json");
7729
+ const packagePath = path16.join(cwd, "package.json");
6661
7730
  const unchanged = {
6662
7731
  scripts: [],
6663
7732
  dependency: { name: GENERATOR_NAME, range: null, added: false }
6664
7733
  };
6665
- if (!fs12.existsSync(packagePath)) {
7734
+ if (!fs14.existsSync(packagePath)) {
6666
7735
  notes.push("No package.json found, so npm scripts and the devDependency were not added.");
6667
7736
  return unchanged;
6668
7737
  }
6669
7738
  let raw;
6670
7739
  let parsed;
6671
7740
  try {
6672
- raw = fs12.readFileSync(packagePath, "utf8");
7741
+ raw = fs14.readFileSync(packagePath, "utf8");
6673
7742
  parsed = JSON.parse(raw);
6674
7743
  } catch {
6675
7744
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
@@ -6694,17 +7763,26 @@ function updatePackageJson(cwd, notes) {
6694
7763
  scriptsWereBroken = true;
6695
7764
  }
6696
7765
  const added = [];
7766
+ const isSelfRepository = parsed.name === GENERATOR_NAME;
7767
+ const scriptPrefix = isSelfRepository ? "node ./dist/bin.js" : `npx ${GENERATOR_NAME}`;
6697
7768
  if (!scripts.snapshot) {
6698
- scripts.snapshot = `npx ${GENERATOR_NAME} generate`;
7769
+ scripts.snapshot = `${scriptPrefix} generate`;
6699
7770
  added.push("snapshot");
6700
7771
  }
6701
7772
  if (!scripts["snapshot:validate"]) {
6702
- scripts["snapshot:validate"] = `npx ${GENERATOR_NAME} validate`;
7773
+ scripts["snapshot:validate"] = `${scriptPrefix} validate`;
6703
7774
  added.push("snapshot:validate");
6704
7775
  }
6705
7776
  const declared = declaredRange(parsed);
6706
7777
  let dependency;
6707
- if (declared !== null) {
7778
+ if (isSelfRepository) {
7779
+ if (declared !== null) {
7780
+ notes.push(
7781
+ `package.json declares ${GENERATOR_NAME} as a dependency of itself. It was left as it is, but a package cannot depend on itself: remove the entry and run the local build instead.`
7782
+ );
7783
+ }
7784
+ dependency = { name: GENERATOR_NAME, range: declared, added: false };
7785
+ } else if (declared !== null) {
6708
7786
  dependency = { name: GENERATOR_NAME, range: declared, added: false };
6709
7787
  } else {
6710
7788
  const range = `^${GENERATOR_VERSION}`;
@@ -6754,7 +7832,15 @@ async function runInit(options) {
6754
7832
  // `validate` warn about a mismatch the user never caused.
6755
7833
  branch: getGitInfo(options.cwd).branch,
6756
7834
  maxPartBytes: options.maxPartBytes ?? detected.recommendedBudget ?? DEFAULT_ANSWERS.maxPartBytes,
6757
- largeFileBytes: DEFAULT_ANSWERS.largeFileBytes,
7835
+ // Every setting the caller named is written down, not just forwarded. A config that omits
7836
+ // what the run used is drift the next `validate` reports, and `setup` is the one command
7837
+ // that is supposed to leave a project in a consistent state.
7838
+ maxPartTokens: options.maxPartTokens ?? DEFAULT_ANSWERS.maxPartTokens,
7839
+ largeFileBytes: options.largeFileBytes ?? DEFAULT_ANSWERS.largeFileBytes,
7840
+ maxMonolithBytes: options.maxMonolithBytes ?? DEFAULT_ANSWERS.maxMonolithBytes,
7841
+ discovery: options.discovery ?? DEFAULT_ANSWERS.discovery,
7842
+ skeleton: options.skeleton ?? DEFAULT_ANSWERS.skeleton,
7843
+ sarif: options.sarif ?? DEFAULT_ANSWERS.sarif,
6758
7844
  include: options.include ?? [],
6759
7845
  excludePatterns: options.exclude ?? [],
6760
7846
  artifacts: artifactAnswers(options),
@@ -6961,7 +8047,7 @@ async function runInit(options) {
6961
8047
  const configText = renderConfig(answers);
6962
8048
  const preferredName = chooseConfigFileName(options.cwd);
6963
8049
  const presentConfigs = CONFIG_CANDIDATES.filter(
6964
- (name) => fs12.existsSync(path13.join(options.cwd, name))
8050
+ (name) => fs14.existsSync(path16.join(options.cwd, name))
6965
8051
  );
6966
8052
  const present = presentConfigs[0] ?? null;
6967
8053
  if (presentConfigs.length > 1) {
@@ -6970,7 +8056,7 @@ async function runInit(options) {
6970
8056
  );
6971
8057
  }
6972
8058
  const configName = present !== null && options.force ? present : preferredName;
6973
- const configPath = path13.join(options.cwd, configName);
8059
+ const configPath = path16.join(options.cwd, configName);
6974
8060
  const reused = present !== null && options.force !== true && options.reuseConfig === true;
6975
8061
  if (reused) notes.push(`Kept the existing ${present} instead of writing a new one.`);
6976
8062
  if (options.dryRun) {
@@ -6985,7 +8071,8 @@ async function runInit(options) {
6985
8071
  scripts: [],
6986
8072
  gitignoreEntries: [],
6987
8073
  dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
6988
- notes
8074
+ notes,
8075
+ outDir: answers.outDir
6989
8076
  };
6990
8077
  }
6991
8078
  if (present !== null && present !== preferredName && !options.force && !reused) {
@@ -7003,7 +8090,7 @@ async function runInit(options) {
7003
8090
  'package.json already defines a "snapshot" config. Remove that key, or re-run with --force to write a config file that takes precedence over it.'
7004
8091
  );
7005
8092
  }
7006
- if (fs12.existsSync(configPath) && !options.force && !reused) {
8093
+ if (fs14.existsSync(configPath) && !options.force && !reused) {
7007
8094
  throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
7008
8095
  }
7009
8096
  if (!reused) writeFileOrExplain(configPath, configText);
@@ -7021,14 +8108,15 @@ async function runInit(options) {
7021
8108
  scripts,
7022
8109
  gitignoreEntries: ignore.added,
7023
8110
  dependency,
7024
- notes
8111
+ notes,
8112
+ outDir: answers.outDir
7025
8113
  };
7026
8114
  }
7027
8115
  function describeTarget(cwd) {
7028
- const relative7 = path13.relative(process.cwd(), cwd);
7029
- if (relative7 === "") return "this directory";
7030
- if (relative7.startsWith("..") || path13.isAbsolute(relative7)) return cwd;
7031
- return relative7;
8116
+ const relative8 = path16.relative(process.cwd(), cwd);
8117
+ if (relative8 === "") return "this directory";
8118
+ if (relative8.startsWith("..") || path16.isAbsolute(relative8)) return cwd;
8119
+ return relative8;
7032
8120
  }
7033
8121
  function firstRunMessage(reason, cwd) {
7034
8122
  const where = describeTarget(cwd);
@@ -7051,7 +8139,7 @@ function planFirstRun(facts) {
7051
8139
  var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
7052
8140
  function isEffectivelyEmpty(cwd) {
7053
8141
  try {
7054
- return fs12.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
8142
+ return fs14.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
7055
8143
  } catch {
7056
8144
  return false;
7057
8145
  }
@@ -7059,19 +8147,203 @@ function isEffectivelyEmpty(cwd) {
7059
8147
  function hasPriorSnapshot(cwd, outDir) {
7060
8148
  let parsed;
7061
8149
  try {
7062
- parsed = JSON.parse(fs12.readFileSync(path13.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
8150
+ parsed = JSON.parse(fs14.readFileSync(path16.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
7063
8151
  } catch {
7064
8152
  return false;
7065
8153
  }
7066
8154
  if (parsed === null || typeof parsed !== "object") return false;
7067
8155
  return isOurGeneratorName(parsed.generator);
7068
8156
  }
8157
+ var HOOK_MARKER = `# ${GENERATOR_NAME}: pre-commit snapshot check`;
8158
+ function shellQuote(value) {
8159
+ return value.replace(/[\\"`$]/g, (character) => `\\${character}`);
8160
+ }
8161
+ function hookScript(outDir) {
8162
+ const dir = shellQuote(outDir.replace(/\\/g, "/"));
8163
+ return `#!/bin/sh
8164
+ ${HOOK_MARKER}
8165
+ #
8166
+ # Refuse the commit when the snapshot is out of date with the tree.
8167
+ #
8168
+ # Remove this file to stop the check, or edit it \u2014 a hook this tool did not write is never
8169
+ # overwritten, and neither is one it did: run \`${GENERATOR_NAME} init --install-hook\` again
8170
+ # to restore the body below.
8171
+ set -e
8172
+
8173
+ SNAPSHOT_DIR="${dir}"
8174
+
8175
+ if [ ! -f "$SNAPSHOT_DIR/manifest.json" ]; then
8176
+ echo "${GENERATOR_NAME}: no snapshot in $SNAPSHOT_DIR, skipping the staleness check." >&2
8177
+ echo " Run \\"npx ${GENERATOR_NAME} generate\\" to create one." >&2
8178
+ exit 0
8179
+ fi
8180
+
8181
+ if ! npx --no-install ${GENERATOR_NAME} validate >/dev/null 2>&1; then
8182
+ echo "${GENERATOR_NAME}: the snapshot is out of date." >&2
8183
+ echo " Run \\"npx ${GENERATOR_NAME} generate\\", stage the change, and commit again." >&2
8184
+ echo " (npx --no-install is used on purpose: this hook must not install anything)" >&2
8185
+ exit 1
8186
+ fi
8187
+ `;
8188
+ }
8189
+ function gitDirectory(cwd) {
8190
+ const dotGit = path16.join(cwd, ".git");
8191
+ let stats;
8192
+ try {
8193
+ stats = fs14.statSync(dotGit);
8194
+ } catch {
8195
+ return null;
8196
+ }
8197
+ if (stats.isDirectory()) return dotGit;
8198
+ if (!stats.isFile()) return null;
8199
+ try {
8200
+ const contents = fs14.readFileSync(dotGit, "utf8");
8201
+ const match = /^gitdir:\s*(.+)$/m.exec(contents);
8202
+ if (!match) return null;
8203
+ const target = match[1];
8204
+ const resolved = path16.isAbsolute(target.trim()) ? target.trim() : path16.join(cwd, target.trim());
8205
+ return fs14.statSync(resolved).isDirectory() ? resolved : null;
8206
+ } catch {
8207
+ return null;
8208
+ }
8209
+ }
8210
+ function installPreCommitHook(cwd, outDir = "snapshot") {
8211
+ const gitDir = gitDirectory(cwd);
8212
+ if (gitDir === null) {
8213
+ return {
8214
+ path: null,
8215
+ written: false,
8216
+ notes: [
8217
+ "Not a git working tree (no .git directory or gitdir link), so no pre-commit hook was installed."
8218
+ ]
8219
+ };
8220
+ }
8221
+ const hooksDir = path16.join(gitDir, "hooks");
8222
+ const hookPath = path16.join(hooksDir, "pre-commit");
8223
+ try {
8224
+ fs14.mkdirSync(hooksDir, { recursive: true });
8225
+ } catch (err) {
8226
+ return {
8227
+ path: hookPath,
8228
+ written: false,
8229
+ notes: [`Could not create ${hooksDir}: ${err.message}`]
8230
+ };
8231
+ }
8232
+ let existing = null;
8233
+ try {
8234
+ existing = fs14.readFileSync(hookPath, "utf8");
8235
+ } catch {
8236
+ existing = null;
8237
+ }
8238
+ if (existing !== null && !existing.includes(HOOK_MARKER)) {
8239
+ return {
8240
+ path: hookPath,
8241
+ written: false,
8242
+ notes: [
8243
+ `${hookPath} already exists and was not written by ${GENERATOR_NAME}; it was left untouched.`,
8244
+ `Add \`npx ${GENERATOR_NAME} validate\` to it to get the same check.`
8245
+ ]
8246
+ };
8247
+ }
8248
+ try {
8249
+ fs14.writeFileSync(hookPath, hookScript(outDir), "utf8");
8250
+ fs14.chmodSync(hookPath, 493);
8251
+ } catch (err) {
8252
+ return {
8253
+ path: hookPath,
8254
+ written: false,
8255
+ notes: [`Could not write ${hookPath}: ${err.message}`]
8256
+ };
8257
+ }
8258
+ return {
8259
+ path: hookPath,
8260
+ written: true,
8261
+ notes: [
8262
+ existing === null ? `Installed ${hookPath}: commits are refused while the snapshot is out of date.` : `Updated ${hookPath} to the current ${GENERATOR_NAME} check.`
8263
+ ]
8264
+ };
8265
+ }
8266
+ function workerEntry(directory, isCjs) {
8267
+ const entry = path16.join(directory, isCjs ? "index.cjs" : "index.js");
8268
+ return { path: entry, url: pathToFileURL(entry).href };
8269
+ }
8270
+ var WORKER_SOURCE = `
8271
+ const { parentPort, workerData } = require("node:worker_threads");
8272
+
8273
+ (async () => {
8274
+ try {
8275
+ const mod = await import(workerData.entryUrl);
8276
+ const loaded = await mod.loadConfigFileDetailed(workerData.cwd);
8277
+ if (loaded.errors.length > 0) {
8278
+ parentPort.postMessage({ ok: false, message: loaded.errors.join("; ") });
8279
+ return;
8280
+ }
8281
+ const config = mod.resolveConfig(loaded.config, workerData.cliOptions, workerData.cwd);
8282
+ const manifest = mod.generateSnapshot(config);
8283
+ parentPort.postMessage({
8284
+ ok: true,
8285
+ summary: {
8286
+ fingerprint: manifest.fingerprint,
8287
+ totalFiles: manifest.counts.totalFiles,
8288
+ skippedCount: manifest.counts.skippedCount,
8289
+ partsCount: manifest.counts.partsCount,
8290
+ parts: manifest.parts.map((part) => ({ filename: part.filename, byteSize: part.byteSize })),
8291
+ totalContentBytes: manifest.totals.totalContentBytes,
8292
+ warnings: manifest.warnings,
8293
+ },
8294
+ });
8295
+ } catch (error) {
8296
+ parentPort.postMessage({
8297
+ ok: false,
8298
+ message: error && error.message ? error.message : String(error),
8299
+ });
8300
+ }
8301
+ })();
8302
+ `;
8303
+ function generateInWorker(cwd, cliOptions) {
8304
+ return new Promise((resolve5) => {
8305
+ let settled = false;
8306
+ const finish = (result) => {
8307
+ if (settled) return;
8308
+ settled = true;
8309
+ resolve5(result);
8310
+ };
8311
+ let worker;
8312
+ const isCjs = typeof __dirname !== "undefined";
8313
+ const directory = isCjs ? __dirname : path16.dirname(fileURLToPath(import.meta.url));
8314
+ try {
8315
+ worker = new Worker(WORKER_SOURCE, {
8316
+ eval: true,
8317
+ workerData: {
8318
+ entryUrl: workerEntry(directory, isCjs).url,
8319
+ cwd,
8320
+ cliOptions
8321
+ }
8322
+ });
8323
+ } catch (error) {
8324
+ finish({
8325
+ ok: false,
8326
+ message: `Could not start the generate worker: ${error instanceof Error ? error.message : String(error)}`
8327
+ });
8328
+ return;
8329
+ }
8330
+ worker.on("message", (message) => finish(message));
8331
+ worker.on("error", (error) => {
8332
+ finish({ ok: false, message: `The generate worker failed: ${error.message}` });
8333
+ });
8334
+ worker.on("exit", (code) => {
8335
+ if (!settled) {
8336
+ finish({ ok: false, message: `The generate worker exited (code ${code}) without a result.` });
8337
+ }
8338
+ });
8339
+ });
8340
+ }
7069
8341
 
7070
8342
  // src/index.ts
7071
8343
  var SCHEMA_VERSION = "3.0.0";
7072
8344
  var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
7073
8345
  function sha256Hex2(input) {
7074
- return crypto.createHash("sha256").update(input).digest("hex");
8346
+ return crypto2.createHash("sha256").update(input).digest("hex");
7075
8347
  }
7076
8348
  var TS_EXTENSIONS = [
7077
8349
  ".ts",
@@ -7110,71 +8382,15 @@ function hasStructuralSummary(relativePath) {
7110
8382
  function bump(counts, key) {
7111
8383
  counts[key] = (counts[key] ?? 0) + 1;
7112
8384
  }
7113
- var RELATIVE_SPECIFIER = /^\.{1,2}\//;
7114
- var IMPORT_CANDIDATE_SUFFIXES = [
7115
- ".ts",
7116
- ".tsx",
7117
- ".mts",
7118
- ".cts",
7119
- ".js",
7120
- ".jsx",
7121
- ".mjs",
7122
- ".cjs",
7123
- "/index.ts",
7124
- "/index.tsx",
7125
- "/index.js",
7126
- "/index.mjs"
7127
- ];
7128
- var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
7129
- ".ts",
7130
- ".tsx",
7131
- ".mts",
7132
- ".cts",
7133
- ".js",
7134
- ".jsx",
7135
- ".mjs",
7136
- ".cjs"
7137
- ]);
7138
- function buildImportResolver(files) {
7139
- const byPath = /* @__PURE__ */ new Map();
7140
- const byPathFolded = /* @__PURE__ */ new Map();
7141
- const byStem = /* @__PURE__ */ new Map();
7142
- const byStemFolded = /* @__PURE__ */ new Map();
7143
- const remember = (map, key, value) => {
7144
- if (!map.has(key)) map.set(key, value);
7145
- };
7146
- for (const file of files) {
7147
- const relativePath = file.relativePath;
7148
- remember(byPath, relativePath, relativePath);
7149
- remember(byPathFolded, relativePath.toLowerCase(), relativePath);
7150
- const extension = path13.posix.extname(relativePath);
7151
- if (!SOURCE_EXTENSIONS.has(extension)) continue;
7152
- const stem = relativePath.slice(0, -extension.length);
7153
- remember(byStem, stem, relativePath);
7154
- remember(byStemFolded, stem.toLowerCase(), relativePath);
7155
- if (path13.posix.basename(stem) === "index") {
7156
- const directory = path13.posix.dirname(stem);
7157
- remember(byStem, directory, relativePath);
7158
- remember(byStemFolded, directory.toLowerCase(), relativePath);
8385
+ function retain(items, keep) {
8386
+ let write = 0;
8387
+ for (const item of items) {
8388
+ if (keep(item)) {
8389
+ items[write] = item;
8390
+ write += 1;
7159
8391
  }
7160
8392
  }
7161
- return { byPath, byPathFolded, byStem, byStemFolded };
7162
- }
7163
- function resolveSpecifier(fromFile, specifier, resolver) {
7164
- if (!RELATIVE_SPECIFIER.test(specifier)) return null;
7165
- const joined = path13.posix.normalize(
7166
- path13.posix.join(path13.posix.dirname(fromFile), specifier)
7167
- );
7168
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
7169
- const direct = exact(joined);
7170
- if (direct) return direct;
7171
- for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
7172
- const hit = exact(`${joined}${suffix}`);
7173
- if (hit) return hit;
7174
- }
7175
- const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
7176
- const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
7177
- return byStem(joined) ?? byStem(stem);
8393
+ items.length = write;
7178
8394
  }
7179
8395
  function buildIndexes(files) {
7180
8396
  const resolver = buildImportResolver(files);
@@ -7226,7 +8442,7 @@ function buildIndexes(files) {
7226
8442
  }))
7227
8443
  };
7228
8444
  }
7229
- function verifyInMemory(files, parts, maxPartBytes) {
8445
+ function verifyInMemory(files, parts) {
7230
8446
  const errors = [];
7231
8447
  const partNames = new Set(parts.map((part) => part.filename));
7232
8448
  for (const file of files) {
@@ -7242,8 +8458,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
7242
8458
  if (actualBytes !== part.metadata.byteSize) {
7243
8459
  errors.push(`${part.filename} byteSize ${part.metadata.byteSize} != actual ${actualBytes}.`);
7244
8460
  }
7245
- if (actualBytes > maxPartBytes) {
7246
- errors.push(`${part.filename} (${actualBytes} B) exceeds the part budget of ${maxPartBytes} B.`);
8461
+ const partBudget = part.metadata.budgetBytes;
8462
+ if (actualBytes > partBudget) {
8463
+ errors.push(
8464
+ `${part.filename} (${actualBytes} B) exceeds its part budget of ${partBudget} B.`
8465
+ );
7247
8466
  }
7248
8467
  if (part.metadata.includedFiles.length !== part.metadata.fileCount) {
7249
8468
  errors.push(`${part.filename} fileCount does not match its included files.`);
@@ -7256,11 +8475,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
7256
8475
  return errors;
7257
8476
  }
7258
8477
  function resolveSafeOutputDirs(config) {
7259
- const cwd = path13.resolve(config.cwd);
7260
- const outDir = path13.resolve(cwd, config.outDir);
7261
- const tmpDir = path13.resolve(cwd, config.tmpDir);
7262
- const root = path13.parse(cwd).root;
7263
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path13.sep}`);
8478
+ const cwd = path16.resolve(config.cwd);
8479
+ const outDir = path16.resolve(cwd, config.outDir);
8480
+ const tmpDir = path16.resolve(cwd, config.tmpDir);
8481
+ const root = path16.parse(cwd).root;
8482
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path16.sep}`);
7264
8483
  const reject = (reason) => {
7265
8484
  throw new Error(
7266
8485
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -7316,14 +8535,14 @@ function listEntries(dir) {
7316
8535
  const walk = (absolute, prefix) => {
7317
8536
  let children;
7318
8537
  try {
7319
- children = fs12.readdirSync(absolute, { withFileTypes: true });
8538
+ children = fs14.readdirSync(absolute, { withFileTypes: true });
7320
8539
  } catch {
7321
8540
  return;
7322
8541
  }
7323
8542
  for (const child of children) {
7324
- const relative7 = prefix === "" ? child.name : `${prefix}/${child.name}`;
7325
- entries.push(relative7);
7326
- if (child.isDirectory()) walk(path13.join(absolute, child.name), relative7);
8543
+ const relative8 = prefix === "" ? child.name : `${prefix}/${child.name}`;
8544
+ entries.push(relative8);
8545
+ if (child.isDirectory()) walk(path16.join(absolute, child.name), relative8);
7327
8546
  }
7328
8547
  };
7329
8548
  walk(dir, "");
@@ -7340,28 +8559,33 @@ function markerPayload(dir, state, id, createdAt) {
7340
8559
  pid: process.pid,
7341
8560
  entries: listEntries(dir)
7342
8561
  };
7343
- const manifestPath = path13.join(dir, "manifest.json");
7344
- if (fs12.existsSync(manifestPath)) {
7345
- marker2.manifestSha256 = sha256Hex2(fs12.readFileSync(manifestPath));
8562
+ const manifestPath = path16.join(dir, "manifest.json");
8563
+ if (fs14.existsSync(manifestPath)) {
8564
+ marker2.manifestSha256 = sha256Hex2(fs14.readFileSync(manifestPath));
7346
8565
  }
7347
8566
  return marker2;
7348
8567
  }
7349
8568
  function writeOwnershipMarker(dir, marker2) {
7350
- fs12.writeFileSync(
7351
- path13.join(dir, OWNERSHIP_MARKER),
8569
+ fs14.writeFileSync(
8570
+ path16.join(dir, OWNERSHIP_MARKER),
7352
8571
  `${JSON.stringify(marker2, null, 2)}
7353
8572
  `,
7354
8573
  "utf8"
7355
8574
  );
7356
8575
  }
8576
+ function markerBelongsTo(dir, id) {
8577
+ const evidence = readOwnershipMarker(dir);
8578
+ if (evidence.marker === null) return true;
8579
+ return evidence.marker.id === id;
8580
+ }
7357
8581
  function readOwnershipMarker(dir) {
7358
- const markerPath = path13.join(dir, OWNERSHIP_MARKER);
7359
- if (!fs12.existsSync(markerPath)) {
8582
+ const markerPath = path16.join(dir, OWNERSHIP_MARKER);
8583
+ if (!fs14.existsSync(markerPath)) {
7360
8584
  return { marker: null, problem: "it holds no .codebase-snapshot marker" };
7361
8585
  }
7362
8586
  let raw;
7363
8587
  try {
7364
- raw = fs12.readFileSync(markerPath, "utf8");
8588
+ raw = fs14.readFileSync(markerPath, "utf8");
7365
8589
  } catch {
7366
8590
  return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
7367
8591
  }
@@ -7429,11 +8653,11 @@ function assessSnapshotDirectory(dir) {
7429
8653
  marker: marker2
7430
8654
  };
7431
8655
  }
7432
- const manifestPath = path13.join(dir, "manifest.json");
7433
- if (fs12.existsSync(manifestPath)) {
8656
+ const manifestPath = path16.join(dir, "manifest.json");
8657
+ if (fs14.existsSync(manifestPath)) {
7434
8658
  let raw;
7435
8659
  try {
7436
- raw = fs12.readFileSync(manifestPath, "utf8");
8660
+ raw = fs14.readFileSync(manifestPath, "utf8");
7437
8661
  } catch {
7438
8662
  return {
7439
8663
  owned: false,
@@ -7515,13 +8739,13 @@ function containsNoFiles(dir) {
7515
8739
  const current = pending.pop();
7516
8740
  let entries;
7517
8741
  try {
7518
- entries = fs12.readdirSync(current, { withFileTypes: true });
8742
+ entries = fs14.readdirSync(current, { withFileTypes: true });
7519
8743
  } catch {
7520
8744
  return false;
7521
8745
  }
7522
8746
  for (const entry of entries) {
7523
8747
  if (entry.isDirectory()) {
7524
- pending.push(path13.join(current, entry.name));
8748
+ pending.push(path16.join(current, entry.name));
7525
8749
  } else if (!IGNORABLE_ENTRIES.has(entry.name)) {
7526
8750
  return false;
7527
8751
  }
@@ -7530,13 +8754,13 @@ function containsNoFiles(dir) {
7530
8754
  return true;
7531
8755
  }
7532
8756
  function assertReplaceable(target, label, secretRules) {
7533
- if (!fs12.existsSync(target)) return;
7534
- if (!fs12.statSync(target).isDirectory()) {
8757
+ if (!fs14.existsSync(target)) return;
8758
+ if (!fs14.statSync(target).isDirectory()) {
7535
8759
  throw new Error(
7536
8760
  `Refusing to write into ${label} ${target}: it exists but is not a directory.`
7537
8761
  );
7538
8762
  }
7539
- const entries = fs12.readdirSync(target);
8763
+ const entries = fs14.readdirSync(target);
7540
8764
  if (entries.length === 0) return;
7541
8765
  if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
7542
8766
  if (containsNoFiles(target)) return;
@@ -7556,18 +8780,18 @@ function assertReplaceable(target, label, secretRules) {
7556
8780
  );
7557
8781
  }
7558
8782
  function assertStagingRootUsable(tmpDir, secretRules) {
7559
- if (!fs12.existsSync(tmpDir)) return;
7560
- if (!fs12.statSync(tmpDir).isDirectory()) {
8783
+ if (!fs14.existsSync(tmpDir)) return;
8784
+ if (!fs14.statSync(tmpDir).isDirectory()) {
7561
8785
  throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
7562
8786
  }
7563
- const entries = fs12.readdirSync(tmpDir);
8787
+ const entries = fs14.readdirSync(tmpDir);
7564
8788
  if (entries.length === 0) return;
7565
8789
  const foreign = entries.filter((name) => {
7566
- const full = path13.join(tmpDir, name);
8790
+ const full = path16.join(tmpDir, name);
7567
8791
  if (!name.startsWith(RUN_DIR_PREFIX)) return true;
7568
8792
  let stats;
7569
8793
  try {
7570
- stats = fs12.statSync(full);
8794
+ stats = fs14.statSync(full);
7571
8795
  } catch {
7572
8796
  return true;
7573
8797
  }
@@ -7575,7 +8799,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7575
8799
  const { marker: marker2 } = readOwnershipMarker(full);
7576
8800
  if (marker2) return false;
7577
8801
  try {
7578
- return fs12.readdirSync(full).length > 0;
8802
+ return fs14.readdirSync(full).length > 0;
7579
8803
  } catch {
7580
8804
  return true;
7581
8805
  }
@@ -7590,13 +8814,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7590
8814
  function cleanStaleRunDirs(tmpDir) {
7591
8815
  let entries;
7592
8816
  try {
7593
- entries = fs12.readdirSync(tmpDir);
8817
+ entries = fs14.readdirSync(tmpDir);
7594
8818
  } catch {
7595
8819
  return;
7596
8820
  }
7597
8821
  for (const name of entries) {
7598
8822
  if (!name.startsWith(RUN_DIR_PREFIX)) continue;
7599
- const full = path13.join(tmpDir, name);
8823
+ const full = path16.join(tmpDir, name);
7600
8824
  const { marker: marker2 } = readOwnershipMarker(full);
7601
8825
  if (marker2) {
7602
8826
  const created = Date.parse(marker2.createdAt);
@@ -7604,33 +8828,33 @@ function cleanStaleRunDirs(tmpDir) {
7604
8828
  const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
7605
8829
  if (aged || abandoned) {
7606
8830
  try {
7607
- fs12.rmSync(full, { recursive: true, force: true });
8831
+ fs14.rmSync(full, { recursive: true, force: true });
7608
8832
  } catch {
7609
8833
  }
7610
8834
  }
7611
8835
  continue;
7612
8836
  }
7613
8837
  try {
7614
- if (fs12.readdirSync(full).length === 0) fs12.rmSync(full, { recursive: true, force: true });
8838
+ if (fs14.readdirSync(full).length === 0) fs14.rmSync(full, { recursive: true, force: true });
7615
8839
  } catch {
7616
8840
  }
7617
8841
  }
7618
8842
  }
7619
8843
  function removeIfEmpty(dir) {
7620
8844
  try {
7621
- if (fs12.readdirSync(dir).length === 0) fs12.rmSync(dir, { recursive: true, force: true });
8845
+ if (fs14.readdirSync(dir).length === 0) fs14.rmSync(dir, { recursive: true, force: true });
7622
8846
  } catch {
7623
8847
  }
7624
8848
  }
7625
8849
  function ensureParentDirectory(target, label) {
7626
- const parent = path13.dirname(target);
7627
- if (fs12.existsSync(parent) && !fs12.statSync(parent).isDirectory()) {
8850
+ const parent = path16.dirname(target);
8851
+ if (fs14.existsSync(parent) && !fs14.statSync(parent).isDirectory()) {
7628
8852
  throw new Error(
7629
8853
  `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
7630
8854
  );
7631
8855
  }
7632
8856
  try {
7633
- fs12.mkdirSync(parent, { recursive: true });
8857
+ fs14.mkdirSync(parent, { recursive: true });
7634
8858
  } catch (err) {
7635
8859
  throw new Error(
7636
8860
  `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
@@ -7638,8 +8862,7 @@ function ensureParentDirectory(target, label) {
7638
8862
  }
7639
8863
  }
7640
8864
  function acquireRunLock(stagingRoot) {
7641
- const key = lockKeyFor(stagingRoot);
7642
- const lockPath = path13.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
8865
+ const lockPath = lockPathFor(stagingRoot);
7643
8866
  const payload = `${JSON.stringify({
7644
8867
  pid: process.pid,
7645
8868
  stagingRoot,
@@ -7648,22 +8871,22 @@ function acquireRunLock(stagingRoot) {
7648
8871
  `;
7649
8872
  for (let attempt = 0; attempt < 2; attempt += 1) {
7650
8873
  try {
7651
- const handle = fs12.openSync(lockPath, "wx");
8874
+ const handle = fs14.openSync(lockPath, "wx");
7652
8875
  try {
7653
- fs12.writeSync(handle, payload);
8876
+ fs14.writeSync(handle, payload);
7654
8877
  } finally {
7655
- fs12.closeSync(handle);
8878
+ fs14.closeSync(handle);
7656
8879
  }
7657
8880
  return () => {
7658
8881
  try {
7659
- fs12.rmSync(lockPath, { force: true });
8882
+ fs14.rmSync(lockPath, { force: true });
7660
8883
  } catch {
7661
8884
  }
7662
8885
  };
7663
8886
  } catch (err) {
7664
8887
  if (err.code !== "EEXIST") throw err;
7665
8888
  if (attempt === 0 && lockOwnerIsGone(lockPath)) {
7666
- fs12.rmSync(lockPath, { force: true });
8889
+ fs14.rmSync(lockPath, { force: true });
7667
8890
  continue;
7668
8891
  }
7669
8892
  throw new Error(
@@ -7676,16 +8899,19 @@ function acquireRunLock(stagingRoot) {
7676
8899
  function lockKeyFor(stagingRoot) {
7677
8900
  let key = stagingRoot;
7678
8901
  try {
7679
- key = fs12.realpathSync(stagingRoot);
8902
+ key = fs14.realpathSync(stagingRoot);
7680
8903
  } catch {
7681
8904
  }
7682
8905
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
7683
8906
  return sha256Hex2(key).slice(0, 16);
7684
8907
  }
8908
+ function lockPathFor(stagingRoot) {
8909
+ return path16.join(os.tmpdir(), `codebase-snapshot-${lockKeyFor(stagingRoot)}.lock`);
8910
+ }
7685
8911
  function lockOwnerIsGone(lockPath) {
7686
8912
  let raw;
7687
8913
  try {
7688
- raw = fs12.readFileSync(lockPath, "utf8");
8914
+ raw = fs14.readFileSync(lockPath, "utf8");
7689
8915
  } catch {
7690
8916
  return false;
7691
8917
  }
@@ -7693,9 +8919,9 @@ function lockOwnerIsGone(lockPath) {
7693
8919
  try {
7694
8920
  pid = JSON.parse(raw).pid;
7695
8921
  } catch {
7696
- return false;
8922
+ return lockFileIsAncient(lockPath);
7697
8923
  }
7698
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
8924
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return lockFileIsAncient(lockPath);
7699
8925
  try {
7700
8926
  process.kill(pid, 0);
7701
8927
  return false;
@@ -7703,14 +8929,21 @@ function lockOwnerIsGone(lockPath) {
7703
8929
  return err.code === "ESRCH";
7704
8930
  }
7705
8931
  }
8932
+ function lockFileIsAncient(lockPath) {
8933
+ try {
8934
+ return Date.now() - fs14.statSync(lockPath).mtimeMs > STALE_RUN_MS;
8935
+ } catch {
8936
+ return false;
8937
+ }
8938
+ }
7706
8939
  function writeManifestAtomically(manifestPath, manifest) {
7707
8940
  const tempPath = `${manifestPath}.${process.pid}.tmp`;
7708
- fs12.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
8941
+ fs14.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7709
8942
  try {
7710
- fs12.renameSync(tempPath, manifestPath);
8943
+ fs14.renameSync(tempPath, manifestPath);
7711
8944
  } catch (err) {
7712
8945
  try {
7713
- fs12.rmSync(tempPath, { force: true });
8946
+ fs14.rmSync(tempPath, { force: true });
7714
8947
  } catch {
7715
8948
  }
7716
8949
  throw err;
@@ -7733,31 +8966,31 @@ function swapIntoPlace(stagingDir, outDir) {
7733
8966
  }
7734
8967
  function swapOnce(stagingDir, outDir) {
7735
8968
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
7736
- const hadPrevious = fs12.existsSync(outDir);
7737
- if (hadPrevious) fs12.renameSync(outDir, backupDir);
8969
+ const hadPrevious = fs14.existsSync(outDir);
8970
+ if (hadPrevious) fs14.renameSync(outDir, backupDir);
7738
8971
  try {
7739
8972
  try {
7740
- fs12.renameSync(stagingDir, outDir);
8973
+ fs14.renameSync(stagingDir, outDir);
7741
8974
  } catch (err) {
7742
8975
  if (err.code !== "EXDEV") throw err;
7743
8976
  const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
7744
8977
  try {
7745
- fs12.cpSync(stagingDir, partialDir, { recursive: true });
7746
- fs12.renameSync(partialDir, outDir);
7747
- fs12.rmSync(stagingDir, { recursive: true, force: true });
8978
+ fs14.cpSync(stagingDir, partialDir, { recursive: true });
8979
+ fs14.renameSync(partialDir, outDir);
8980
+ fs14.rmSync(stagingDir, { recursive: true, force: true });
7748
8981
  } catch (copyErr) {
7749
8982
  try {
7750
- fs12.rmSync(partialDir, { recursive: true, force: true });
8983
+ fs14.rmSync(partialDir, { recursive: true, force: true });
7751
8984
  } catch {
7752
8985
  }
7753
8986
  throw copyErr;
7754
8987
  }
7755
8988
  }
7756
- if (hadPrevious) fs12.rmSync(backupDir, { recursive: true, force: true });
8989
+ if (hadPrevious) fs14.rmSync(backupDir, { recursive: true, force: true });
7757
8990
  } catch (err) {
7758
- if (hadPrevious && fs12.existsSync(backupDir)) {
7759
- if (fs12.existsSync(outDir)) fs12.rmSync(outDir, { recursive: true, force: true });
7760
- fs12.renameSync(backupDir, outDir);
8991
+ if (hadPrevious && fs14.existsSync(backupDir)) {
8992
+ if (fs14.existsSync(outDir)) fs14.rmSync(outDir, { recursive: true, force: true });
8993
+ fs14.renameSync(backupDir, outDir);
7761
8994
  }
7762
8995
  throw err;
7763
8996
  }
@@ -7766,7 +8999,7 @@ function sleepSync(milliseconds) {
7766
8999
  const buffer = new Int32Array(new SharedArrayBuffer(4));
7767
9000
  Atomics.wait(buffer, 0, 0, milliseconds);
7768
9001
  }
7769
- function artifactWarnings(artifacts) {
9002
+ function artifactWarnings(artifacts, sarif) {
7770
9003
  const messages = [];
7771
9004
  if (!artifacts.manifest) {
7772
9005
  messages.push("manifest.json is disabled: `validate` and `report` cannot inspect this snapshot.");
@@ -7776,7 +9009,7 @@ function artifactWarnings(artifacts) {
7776
9009
  "parts are disabled but navigation.md is enabled: its links point at part files that were not written."
7777
9010
  );
7778
9011
  }
7779
- if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
9012
+ if (!sarif && ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
7780
9013
  messages.push(
7781
9014
  "every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
7782
9015
  );
@@ -7848,13 +9081,25 @@ function generateSnapshot(configInput, options) {
7848
9081
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
7849
9082
  includeSensitiveFiles: config.includeSensitiveFiles,
7850
9083
  secretRules: config.secretRules,
9084
+ discovery: config.discovery,
7851
9085
  // The output and staging directories this run is about to write, relative to the root. A
7852
9086
  // first run cannot have marked them yet, so the name alone keeps a run from recording its own
7853
9087
  // output — but only at these exact paths, not at every directory that shares the name.
7854
9088
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
7855
- (directory) => path13.relative(config.cwd, path13.resolve(config.cwd, directory)).replace(/\\/g, "/")
7856
- ).filter((relative7) => relative7.length > 0 && !relative7.startsWith(".."))
9089
+ (directory) => path16.relative(config.cwd, path16.resolve(config.cwd, directory)).replace(/\\/g, "/")
9090
+ ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
7857
9091
  });
9092
+ const changedScope = config.changedSince !== void 0 || config.staged === true ? resolveChangedScope(config.cwd, {
9093
+ ...config.changedSince === void 0 ? {} : { changedSince: config.changedSince },
9094
+ ...config.staged === true ? { staged: true } : {}
9095
+ }) : null;
9096
+ const changedPaths = changedScope === null ? null : new Set(changedScope.paths);
9097
+ const unchangedPaths = [];
9098
+ const candidateFiles = changedPaths !== null && config.withDependents !== true ? collection.files.filter((relativePath) => {
9099
+ if (changedPaths.has(relativePath)) return true;
9100
+ unchangedPaths.push(relativePath);
9101
+ return false;
9102
+ }) : collection.files;
7858
9103
  const files = [];
7859
9104
  const monolithBodies = /* @__PURE__ */ new Map();
7860
9105
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
@@ -7865,6 +9110,7 @@ function generateSnapshot(configInput, options) {
7865
9110
  return files.some((file) => file.isPreviewTruncated && !file.isLarge);
7866
9111
  };
7867
9112
  const encodingNotes = [];
9113
+ const fileWarnings = [];
7868
9114
  const oversizeSummaryPaths = [];
7869
9115
  const previewSummaryPaths = [];
7870
9116
  const layerCounts = {};
@@ -7872,13 +9118,13 @@ function generateSnapshot(configInput, options) {
7872
9118
  let totalLines = 0;
7873
9119
  let totalSizeBytes = 0;
7874
9120
  let largeFileCount = 0;
7875
- for (const relativePath of collection.files) {
7876
- const absolutePath = path13.join(config.cwd, relativePath);
9121
+ for (const relativePath of candidateFiles) {
9122
+ const absolutePath = path16.join(config.cwd, relativePath);
7877
9123
  let stats;
7878
9124
  try {
7879
- stats = fs12.statSync(absolutePath);
9125
+ stats = fs14.statSync(absolutePath);
7880
9126
  if (!stats.isFile()) {
7881
- const nestedRepository = stats.isDirectory() && fs12.existsSync(path13.join(absolutePath, ".git"));
9127
+ const nestedRepository = stats.isDirectory() && fs14.existsSync(path16.join(absolutePath, ".git"));
7882
9128
  skipped.push(
7883
9129
  nestedRepository ? {
7884
9130
  path: relativePath,
@@ -7892,13 +9138,17 @@ function generateSnapshot(configInput, options) {
7892
9138
  skipped.push({ path: relativePath, reason: "read-error" });
7893
9139
  continue;
7894
9140
  }
7895
- if (isBinaryFile(absolutePath)) {
9141
+ if (isBinaryExtension(absolutePath)) {
9142
+ skipped.push({ path: relativePath, reason: "binary", detail: `Size: ${stats.size} B` });
9143
+ continue;
9144
+ }
9145
+ if (stats.size > BINARY_SCAN_BYTES && isBinaryFile(absolutePath)) {
7896
9146
  skipped.push({ path: relativePath, reason: "binary", detail: `Size: ${stats.size} B` });
7897
9147
  continue;
7898
9148
  }
7899
9149
  let buffer;
7900
9150
  try {
7901
- buffer = fs12.readFileSync(absolutePath);
9151
+ buffer = fs14.readFileSync(absolutePath);
7902
9152
  } catch {
7903
9153
  skipped.push({ path: relativePath, reason: "read-error" });
7904
9154
  continue;
@@ -7953,7 +9203,16 @@ function generateSnapshot(configInput, options) {
7953
9203
  let finalContent = redacted;
7954
9204
  let isPreviewTruncated = false;
7955
9205
  let includedLines = lineCount;
7956
- if (isLarge) {
9206
+ let inclusionMode = "full";
9207
+ if (config.skeleton === true && isSkeletonizable(relativePath)) {
9208
+ const skeleton = skeletonizeTypeScript(redacted, relativePath);
9209
+ if (skeleton.collapsed > 0) {
9210
+ finalContent = skeleton.text;
9211
+ inclusionMode = "signatures";
9212
+ includedLines = countLines(skeleton.text);
9213
+ }
9214
+ }
9215
+ if (isLarge && inclusionMode !== "signatures") {
7957
9216
  const preview = truncatePreview(
7958
9217
  redacted,
7959
9218
  config.previewHeadLines,
@@ -7962,8 +9221,9 @@ function generateSnapshot(configInput, options) {
7962
9221
  finalContent = preview.content;
7963
9222
  isPreviewTruncated = preview.isTruncated;
7964
9223
  includedLines = preview.includedLines;
9224
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
7965
9225
  }
7966
- if (config.includeMonolith && isPreviewTruncated) {
9226
+ if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
7967
9227
  monolithBodies.set(relativePath, redacted);
7968
9228
  }
7969
9229
  let summary;
@@ -7978,14 +9238,19 @@ function generateSnapshot(configInput, options) {
7978
9238
  summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
7979
9239
  summaryQuality = outcome?.quality;
7980
9240
  if (isSourceSummary(summary) && summary.parseErrors) {
7981
- warnings.push(
7982
- `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
7983
- );
9241
+ fileWarnings.push({
9242
+ path: relativePath,
9243
+ message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
9244
+ });
7984
9245
  }
7985
9246
  } catch (err) {
7986
- warnings.push(
7987
- `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
7988
- );
9247
+ fileWarnings.push({
9248
+ path: relativePath,
9249
+ message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
9250
+ /\s+/g,
9251
+ " "
9252
+ )
9253
+ });
7989
9254
  }
7990
9255
  }
7991
9256
  files.push({
@@ -7997,7 +9262,7 @@ function generateSnapshot(configInput, options) {
7997
9262
  layer: layer2,
7998
9263
  kind,
7999
9264
  language,
8000
- inclusionMode: isPreviewTruncated ? "preview" : "full",
9265
+ inclusionMode,
8001
9266
  isLarge,
8002
9267
  isPreviewTruncated,
8003
9268
  originalLines: lineCount,
@@ -8009,6 +9274,58 @@ function generateSnapshot(configInput, options) {
8009
9274
  content: finalContent
8010
9275
  });
8011
9276
  }
9277
+ if (changedScope !== null && changedPaths !== null && config.withDependents === true) {
9278
+ const keep = new Set(changedPaths);
9279
+ const resolver = buildImportResolver(files);
9280
+ for (const file of files) {
9281
+ if (!isSourceSummary(file.summary)) continue;
9282
+ if (importsChangedFile(file.relativePath, file.summary.imports, changedPaths, resolver)) {
9283
+ keep.add(file.relativePath);
9284
+ }
9285
+ }
9286
+ for (const file of files) {
9287
+ if (!keep.has(file.relativePath)) unchangedPaths.push(file.relativePath);
9288
+ }
9289
+ const kept = files.filter((file) => keep.has(file.relativePath));
9290
+ files.length = 0;
9291
+ files.push(...kept);
9292
+ totalLines = files.reduce((total, file) => total + file.lines, 0);
9293
+ totalSizeBytes = files.reduce((total, file) => total + file.sizeBytes, 0);
9294
+ largeFileCount = 0;
9295
+ for (const key of Object.keys(layerCounts)) delete layerCounts[key];
9296
+ for (const key of Object.keys(kindCounts)) delete kindCounts[key];
9297
+ for (const file of files) {
9298
+ bump(layerCounts, file.layer);
9299
+ bump(kindCounts, file.kind);
9300
+ if (file.isLarge) largeFileCount += 1;
9301
+ }
9302
+ retain(
9303
+ secretFindings,
9304
+ (finding) => finding.path === void 0 || keep.has(finding.path)
9305
+ );
9306
+ retain(encodingNotes, (note) => keep.has(note.path));
9307
+ retain(oversizeSummaryPaths, (path17) => keep.has(path17));
9308
+ retain(previewSummaryPaths, (path17) => keep.has(path17));
9309
+ retain(fileWarnings, (warning) => keep.has(warning.path));
9310
+ for (const path17 of [...monolithBodies.keys()]) {
9311
+ if (!keep.has(path17)) monolithBodies.delete(path17);
9312
+ }
9313
+ }
9314
+ if (changedScope !== null) {
9315
+ for (const relativePath of unchangedPaths) {
9316
+ skipped.push({
9317
+ path: relativePath,
9318
+ reason: "unchanged",
9319
+ detail: `not ${changedScope.detail}`
9320
+ });
9321
+ }
9322
+ }
9323
+ for (const warning of fileWarnings) warnings.push(warning.message);
9324
+ if (changedScope !== null && files.length === 0) {
9325
+ warnings.push(
9326
+ `No file is ${changedScope.detail}, so the snapshot is empty. Check the ref, or run without --changed-since to scan the whole repository.`
9327
+ );
9328
+ }
8012
9329
  const encodingBuckets = /* @__PURE__ */ new Map();
8013
9330
  for (const note of encodingNotes) {
8014
9331
  const bucket = encodingBuckets.get(note.encoding) ?? { paths: [], invalidBytes: 0 };
@@ -8059,13 +9376,8 @@ function generateSnapshot(configInput, options) {
8059
9376
  );
8060
9377
  }
8061
9378
  const parts = partitionByBudget(files, config, fingerprint);
8062
- const maxEffectiveBudget = Math.max(
8063
- config.maxPartBytes,
8064
- config.budgets.default,
8065
- ...Object.values(config.budgets.byLayer)
8066
- );
8067
9379
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
8068
- const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
9380
+ const validationErrors = verifyInMemory(files, parts);
8069
9381
  const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
8070
9382
  const maskName = (value) => maskPathText(value, config.secretRules);
8071
9383
  const manifest = {
@@ -8085,12 +9397,26 @@ function generateSnapshot(configInput, options) {
8085
9397
  config: {
8086
9398
  digest: configDigest(config),
8087
9399
  maxPartBytes: config.maxPartBytes,
9400
+ // The ceiling the user named, in their own unit. `maxPartBytes` above is what the
9401
+ // partitioner enforced; both are recorded so the manifest can explain the gap.
9402
+ ...config.maxPartTokens === void 0 ? {} : { maxPartTokens: config.maxPartTokens },
8088
9403
  largeFileBytes: config.largeFileBytes,
8089
9404
  previewHeadLines: config.previewHeadLines,
8090
9405
  previewTailLines: config.previewTailLines,
8091
9406
  includeMonolith: config.includeMonolith,
9407
+ // A ceiling that decides whether `full-codebase.*` is written at all, so it belongs
9408
+ // in the record of what the run enforced. Omitting it left `--max-monolith-bytes`
9409
+ // invisible in the manifest that is supposed to describe the run.
9410
+ maxMonolithBytes: config.maxMonolithBytes ?? null,
8092
9411
  includeSensitiveFiles: config.includeSensitiveFiles,
8093
9412
  discovery: config.discovery ?? "auto",
9413
+ // A changed-files scope decides which files are in the snapshot, so the manifest that
9414
+ // describes the run has to name it.
9415
+ ...config.changedSince === void 0 ? {} : { changedSince: config.changedSince },
9416
+ ...config.staged === true ? { staged: true } : {},
9417
+ ...config.withDependents === true ? { withDependents: true } : {},
9418
+ ...config.skeleton === true ? { skeleton: true } : {},
9419
+ ...config.sarif === true ? { sarif: true } : {},
8094
9420
  artifacts: { ...config.artifacts },
8095
9421
  include: [...config.include].map(maskName),
8096
9422
  preset: config.preset,
@@ -8122,6 +9448,9 @@ function generateSnapshot(configInput, options) {
8122
9448
  skippedFiles: skipped.length,
8123
9449
  totalSourceBytes: totalSizeBytes,
8124
9450
  totalContentBytes,
9451
+ // The parts are what a model is handed, so the estimate is over the parts rather than
9452
+ // over the sources: previews and the byte budget both shrink the number.
9453
+ estimatedTokens: parts.reduce((total, part) => total + part.metadata.estimatedTokens, 0),
8125
9454
  partCount: parts.length,
8126
9455
  warningCount: 0,
8127
9456
  redactedLines: 0
@@ -8160,7 +9489,9 @@ function generateSnapshot(configInput, options) {
8160
9489
  );
8161
9490
  manifest.totals.warningCount = manifest.warnings.length;
8162
9491
  }
8163
- for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
9492
+ for (const message of artifactWarnings(config.artifacts, config.sarif === true)) {
9493
+ manifest.warnings.push(message);
9494
+ }
8164
9495
  manifest.totals.warningCount = manifest.warnings.length;
8165
9496
  let projectTree = null;
8166
9497
  if (config.artifacts.projectTree) {
@@ -8173,6 +9504,13 @@ function generateSnapshot(configInput, options) {
8173
9504
  manifest.totals.warningCount = manifest.warnings.length;
8174
9505
  }
8175
9506
  }
9507
+ const sarifText = config.sarif === true ? generateSarifReport(manifest, secretFindings, config.secretRules, GENERATOR_VERSION) : null;
9508
+ if (sarifText !== null) {
9509
+ manifest.artifacts.sarif = {
9510
+ byteSize: Buffer.byteLength(sarifText, "utf8"),
9511
+ sha256: sha256Hex2(sarifText)
9512
+ };
9513
+ }
8176
9514
  let monolithJson = null;
8177
9515
  let monolithMarkdown = null;
8178
9516
  if (config.includeMonolith) {
@@ -8223,17 +9561,17 @@ function generateSnapshot(configInput, options) {
8223
9561
  const lockKeys = Array.from(/* @__PURE__ */ new Set([tmpDir, outDir])).sort();
8224
9562
  const releases = lockKeys.map((key) => acquireRunLock(key));
8225
9563
  const releaseLock = () => {
8226
- for (const release of releases.reverse()) release();
9564
+ for (const release of [...releases].reverse()) release();
8227
9565
  };
8228
9566
  let stagingDir = null;
8229
9567
  try {
8230
9568
  ensureParentDirectory(outDir, "outDir");
8231
- fs12.mkdirSync(tmpDir, { recursive: true });
9569
+ fs14.mkdirSync(tmpDir, { recursive: true });
8232
9570
  cleanStaleRunDirs(tmpDir);
8233
- const snapshotId = crypto.randomUUID();
9571
+ const snapshotId = crypto2.randomUUID();
8234
9572
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8235
- stagingDir = path13.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
8236
- fs12.mkdirSync(stagingDir);
9573
+ stagingDir = path16.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
9574
+ fs14.mkdirSync(stagingDir);
8237
9575
  writeOwnershipMarker(stagingDir, {
8238
9576
  generator: GENERATOR_NAME,
8239
9577
  version: GENERATOR_VERSION,
@@ -8243,41 +9581,44 @@ function generateSnapshot(configInput, options) {
8243
9581
  id: snapshotId,
8244
9582
  pid: process.pid
8245
9583
  });
8246
- const partsDir = path13.join(stagingDir, "parts");
8247
- const reportsDir = path13.join(stagingDir, "reports");
8248
- if (artifacts.parts) fs12.mkdirSync(partsDir, { recursive: true });
8249
- if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
8250
- fs12.mkdirSync(reportsDir, { recursive: true });
9584
+ const partsDir = path16.join(stagingDir, "parts");
9585
+ const reportsDir = path16.join(stagingDir, "reports");
9586
+ if (artifacts.parts) fs14.mkdirSync(partsDir, { recursive: true });
9587
+ if (artifacts.navigation || artifacts.projectTree || artifacts.audit || config.sarif === true) {
9588
+ fs14.mkdirSync(reportsDir, { recursive: true });
8251
9589
  }
8252
9590
  if (artifacts.parts) {
8253
9591
  for (const part of parts) {
8254
- fs12.writeFileSync(path13.join(partsDir, part.filename), part.markdown, "utf8");
9592
+ fs14.writeFileSync(path16.join(partsDir, part.filename), part.markdown, "utf8");
8255
9593
  }
8256
9594
  }
8257
9595
  if (projectTree !== null) {
8258
- fs12.writeFileSync(path13.join(reportsDir, "project-tree.md"), projectTree, "utf8");
9596
+ fs14.writeFileSync(path16.join(reportsDir, "project-tree.md"), projectTree, "utf8");
8259
9597
  }
8260
9598
  if (artifacts.navigation) {
8261
- fs12.writeFileSync(path13.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
9599
+ fs14.writeFileSync(path16.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
8262
9600
  }
8263
9601
  if (artifacts.audit) {
8264
- fs12.writeFileSync(
8265
- path13.join(reportsDir, "audit.md"),
9602
+ fs14.writeFileSync(
9603
+ path16.join(reportsDir, "audit.md"),
8266
9604
  generateAuditReport(manifest, secretFindings, config.secretRules),
8267
9605
  "utf8"
8268
9606
  );
8269
9607
  }
9608
+ if (sarifText !== null) {
9609
+ fs14.writeFileSync(path16.join(reportsDir, "audit.sarif"), sarifText, "utf8");
9610
+ }
8270
9611
  if (artifacts.readme) {
8271
- fs12.writeFileSync(path13.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
9612
+ fs14.writeFileSync(path16.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
8272
9613
  }
8273
9614
  if (artifacts.manifest) {
8274
- fs12.writeFileSync(path13.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
9615
+ fs14.writeFileSync(path16.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
8275
9616
  }
8276
9617
  if (monolithJson !== null) {
8277
- fs12.writeFileSync(path13.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
9618
+ fs14.writeFileSync(path16.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
8278
9619
  }
8279
9620
  if (monolithMarkdown !== null) {
8280
- fs12.writeFileSync(path13.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
9621
+ fs14.writeFileSync(path16.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
8281
9622
  }
8282
9623
  writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
8283
9624
  swapIntoPlace(stagingDir, outDir);
@@ -8285,31 +9626,41 @@ function generateSnapshot(configInput, options) {
8285
9626
  const warningsBeforeHook = manifest.warnings.length;
8286
9627
  const pendingHook = runAfterGenerate(config, manifest);
8287
9628
  const shipHookState = () => {
9629
+ if (!markerBelongsTo(outDir, snapshotId)) return;
8288
9630
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
8289
- writeManifestAtomically(path13.join(outDir, "manifest.json"), manifest);
9631
+ writeManifestAtomically(path16.join(outDir, "manifest.json"), manifest);
8290
9632
  }
8291
9633
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
8292
9634
  };
8293
9635
  shipHookState();
8294
9636
  if (pendingHook) {
8295
- pendingHook.catch((err) => {
8296
- const message = maskPathText(
8297
- err instanceof Error ? err.message : String(err),
8298
- config.secretRules
8299
- );
8300
- manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
8301
- manifest.totals.warningCount = manifest.warnings.length;
8302
- try {
8303
- shipHookState();
8304
- } catch {
9637
+ pendingHook.then(
9638
+ () => {
9639
+ try {
9640
+ shipHookState();
9641
+ } catch {
9642
+ }
9643
+ },
9644
+ (err) => {
9645
+ const message = maskPathText(
9646
+ err instanceof Error ? err.message : String(err),
9647
+ config.secretRules
9648
+ );
9649
+ manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
9650
+ manifest.totals.warningCount = manifest.warnings.length;
9651
+ try {
9652
+ shipHookState();
9653
+ } catch {
9654
+ }
8305
9655
  }
8306
- });
9656
+ );
9657
+ pendingHooks.set(outDir, pendingHook);
8307
9658
  }
8308
9659
  return manifest;
8309
9660
  } catch (err) {
8310
9661
  if (stagingDir !== null) {
8311
9662
  try {
8312
- fs12.rmSync(stagingDir, { recursive: true, force: true });
9663
+ fs14.rmSync(stagingDir, { recursive: true, force: true });
8313
9664
  } catch {
8314
9665
  }
8315
9666
  }
@@ -8319,10 +9670,17 @@ function generateSnapshot(configInput, options) {
8319
9670
  releaseLock();
8320
9671
  }
8321
9672
  }
9673
+ var pendingHooks = /* @__PURE__ */ new Map();
8322
9674
  async function generateSnapshotAsync(configInput, options) {
8323
- return generateSnapshot(configInput, options);
9675
+ const manifest = generateSnapshot(configInput, options);
9676
+ const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput ?? null);
9677
+ const { outDir } = resolveSafeOutputDirs(config);
9678
+ const pending = pendingHooks.get(outDir);
9679
+ if (pending !== void 0) {
9680
+ pendingHooks.delete(outDir);
9681
+ await pending.catch(() => void 0);
9682
+ }
9683
+ return manifest;
8324
9684
  }
8325
9685
 
8326
- export { ARTIFACT_NAMES, CHECK_INTERVAL_MS, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, FAILURE_INTERVAL_MS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PACKAGE_LATEST_URL, PRESETS, PromptBack, PromptCancelled, REQUEST_TIMEOUT_MS, SCHEMA_VERSION, anchorForPath, appendGitignoreEntries, assertReplaceable, assertUsableConfigInput, cacheDirectory, cacheFilePath, checkForUpdate, collectFiles, compareManifests, countInvalidUtf8Bytes, countLines, decodeText, defaultIO, defineConfig, describeManifestShapeProblem, detectLanguage, detectPreset, displayWidth, escapeControls, explainPath, fenceForContent, fenceForText, fetchLatestVersion, findConfigFile, firstRunMessage, formatNumber, formatUpdateNotice, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, gitignorePath, globToRegExp, hasPriorSnapshot, isBinaryBuffer, isBinaryExtension, isBinaryFile, isDiscoveryMode, isEffectivelyEmpty, isInteractive, isNewer, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, planFirstRun, profileForPath, renderConfig, renderDiffText, renderExplainText, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, shouldCheck, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot, wrapLines };
8327
- //# sourceMappingURL=chunk-C24OAT6T.js.map
8328
- //# sourceMappingURL=chunk-C24OAT6T.js.map
9686
+ export { ARTIFACT_NAMES, CHECK_INTERVAL_MS, CONFIG_KEYS, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, FAILURE_INTERVAL_MS, GENERATOR_NAME, GENERATOR_VERSION, HOOK_MARKER, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, MIN_MAX_PART_TOKENS, PACKAGE_LATEST_URL, PRESETS, PromptBack, PromptCancelled, REQUEST_TIMEOUT_MS, SCHEMA_VERSION, anchorForPath, appendGitignoreEntries, assertReplaceable, assertUsableConfigInput, buildImportResolver, bytesForTokens, cacheDirectory, cacheFilePath, checkForUpdate, classifyKind, classifyLayer, collectFiles, compareManifests, configDigest, countInvalidUtf8Bytes, countLines, decodeText, defaultIO, defineConfig, describeManifestShapeProblem, detectLanguage, detectPreset, displayPath, displayWidth, enforceBudget, escapeControls, estimateTokens, explainPath, fenceForContent, fenceForText, fetchLatestVersion, findConfigFile, findSecretInPath, firstRunMessage, formatNumber, formatTokens, formatUpdateNotice, generateInWorker, generateProjectTree, generateSarifReport, generateSnapshot, generateSnapshotAsync, getGitInfo, getPreset, gitignorePath, globToRegExp, hasPriorSnapshot, importsChangedFile, importsOfFile, importsOfText, installKind, installPreCommitHook, isBinaryBuffer, isBinaryExtension, isBinaryFile, isDiscoveryMode, isEffectivelyEmpty, isInteractive, isNewer, isResolvedConfig, isSensitivePath, isSkeletonizable, isTsxFileName, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, lockPathFor, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, planFirstRun, profileForPath, renderConfig, renderDiffText, renderExplainText, renumberParts, resolveChangedScope, resolveConfig, resolveSafeOutputDirs, resolveSpecifier, runInit, scanAndRedactSecrets, shouldCheck, skeletonizeTypeScript, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, updateCommand, validateSnapshot, workerEntry, wrapLines };