codebase-snapshot 0.9.9 → 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.9";
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;
@@ -5521,17 +6498,17 @@ function isNewer(candidate, current) {
5521
6498
  }
5522
6499
  function cacheDirectory(env = process.env, platform = process.platform) {
5523
6500
  if (platform === "win32") {
5524
- const base2 = env.LOCALAPPDATA ?? path13.join(os.homedir(), "AppData", "Local");
5525
- return path13.join(base2, "codebase-snapshot");
6501
+ const base2 = env.LOCALAPPDATA ?? path16.join(os.homedir(), "AppData", "Local");
6502
+ return path16.join(base2, "codebase-snapshot");
5526
6503
  }
5527
6504
  if (platform === "darwin") {
5528
- return path13.join(os.homedir(), "Library", "Caches", "codebase-snapshot");
6505
+ return path16.join(os.homedir(), "Library", "Caches", "codebase-snapshot");
5529
6506
  }
5530
- const base = env.XDG_CACHE_HOME ?? path13.join(os.homedir(), ".cache");
5531
- 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");
5532
6509
  }
5533
6510
  function cacheFilePath(env = process.env) {
5534
- return path13.join(cacheDirectory(env), "update-check.json");
6511
+ return path16.join(cacheDirectory(env), "update-check.json");
5535
6512
  }
5536
6513
  function shouldCheck(argv, env) {
5537
6514
  const disabled = ["1", "true", "yes"].includes(
@@ -5544,13 +6521,23 @@ function shouldCheck(argv, env) {
5544
6521
  for (const skip of ["mcp", "describe", "skill", "--version", "-v", "--help", "-h"]) {
5545
6522
  if (args.includes(skip)) return false;
5546
6523
  }
5547
- 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
+ }
5548
6535
  return true;
5549
6536
  }
5550
6537
  function readCache(file) {
5551
6538
  let raw;
5552
6539
  try {
5553
- raw = fs12.readFileSync(file, "utf8");
6540
+ raw = fs14.readFileSync(file, "utf8");
5554
6541
  } catch {
5555
6542
  return null;
5556
6543
  }
@@ -5567,8 +6554,8 @@ function readCache(file) {
5567
6554
  }
5568
6555
  function writeCache(file, entry) {
5569
6556
  try {
5570
- fs12.mkdirSync(path13.dirname(file), { recursive: true });
5571
- fs12.writeFileSync(file, `${JSON.stringify(entry)}
6557
+ fs14.mkdirSync(path16.dirname(file), { recursive: true });
6558
+ fs14.writeFileSync(file, `${JSON.stringify(entry)}
5572
6559
  `, "utf8");
5573
6560
  } catch {
5574
6561
  }
@@ -5662,42 +6649,42 @@ function describeMatcher(matcher) {
5662
6649
  return String(matcher);
5663
6650
  }
5664
6651
  function normalizeTarget(cwd, target) {
5665
- const absolute = path13.resolve(cwd, target);
5666
- const relative7 = path13.relative(cwd, absolute);
5667
- if (relative7.startsWith("..") || path13.isAbsolute(relative7)) return target;
5668
- 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("/");
5669
6656
  }
5670
6657
  function findRuleMatch(rules, relativePath) {
5671
6658
  return rules.find(
5672
6659
  (rule) => matchRule(relativePath, rule.match)
5673
6660
  );
5674
6661
  }
5675
- function findRecordedAncestor(skipped, relative7, maskedPath2) {
6662
+ function findRecordedAncestor(skipped, relative8, maskedPath2) {
5676
6663
  let nearest;
5677
6664
  for (const record of skipped) {
5678
6665
  const ancestorPath = record.path;
5679
6666
  if (ancestorPath.length === 0) continue;
5680
- const isAncestor = relative7.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
6667
+ const isAncestor = relative8.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
5681
6668
  if (!isAncestor) continue;
5682
6669
  if (!nearest || ancestorPath.length > nearest.path.length) nearest = record;
5683
6670
  }
5684
6671
  return nearest;
5685
6672
  }
5686
6673
  function readManifest2(config) {
5687
- const manifestPath = path13.resolve(config.cwd, config.outDir, "manifest.json");
6674
+ const manifestPath = path16.resolve(config.cwd, config.outDir, "manifest.json");
5688
6675
  try {
5689
- return JSON.parse(fs12.readFileSync(manifestPath, "utf8"));
6676
+ return JSON.parse(fs14.readFileSync(manifestPath, "utf8"));
5690
6677
  } catch {
5691
6678
  return null;
5692
6679
  }
5693
6680
  }
5694
6681
  function explainPath(config, target) {
5695
- const relative7 = normalizeTarget(config.cwd, target);
5696
- const absolute = path13.join(config.cwd, relative7);
5697
- 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;
5698
6685
  const result = {
5699
6686
  path: maskedPath2,
5700
- exists: fs12.existsSync(absolute),
6687
+ exists: fs14.existsSync(absolute),
5701
6688
  included: false
5702
6689
  };
5703
6690
  const collection = collectFiles(config.cwd, {
@@ -5705,15 +6692,18 @@ function explainPath(config, target) {
5705
6692
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5706
6693
  includeSensitiveFiles: config.includeSensitiveFiles,
5707
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,
5708
6698
  // Same exclusion `generate` applies, so `explain` never says "not-discovered" about a file
5709
6699
  // that is only missing because the walker dropped the tool's own output directory.
5710
6700
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
5711
- (directory) => path13.relative(config.cwd, path13.resolve(config.cwd, directory)).replace(/\\/g, "/")
5712
- ).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(".."))
5713
6703
  });
5714
- if (!collection.files.includes(relative7)) {
6704
+ if (!collection.files.includes(relative8)) {
5715
6705
  const record = collection.skipped.find(
5716
- (entry2) => entry2.path === relative7 || entry2.path === maskedPath2
6706
+ (entry2) => entry2.path === relative8 || entry2.path === maskedPath2
5717
6707
  );
5718
6708
  if (record) {
5719
6709
  result.reason = record.reason;
@@ -5722,7 +6712,7 @@ function explainPath(config, target) {
5722
6712
  result.reason = "missing";
5723
6713
  result.detail = "no such file or directory, relative to the configured cwd";
5724
6714
  } else {
5725
- const ancestor = findRecordedAncestor(collection.skipped, relative7, maskedPath2);
6715
+ const ancestor = findRecordedAncestor(collection.skipped, relative8, maskedPath2);
5726
6716
  if (ancestor) {
5727
6717
  result.reason = ancestor.reason;
5728
6718
  result.detail = ancestor.detail ? `${ancestor.detail} \u2014 inherited from the skipped directory \`${ancestor.path}\`` : `inside the skipped directory \`${ancestor.path}\``;
@@ -5734,21 +6724,58 @@ function explainPath(config, target) {
5734
6724
  return result;
5735
6725
  }
5736
6726
  result.included = true;
5737
- 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)) {
5738
6765
  result.included = false;
5739
6766
  result.reason = "binary";
5740
6767
  result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
5741
6768
  return result;
5742
6769
  }
5743
- result.layer = classifyLayer(relative7, config.layers);
5744
- result.kind = classifyKind(relative7, config.kinds);
5745
- result.language = detectLanguage(relative7);
5746
- 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);
5747
6774
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
5748
- const kindRule = findRuleMatch(config.kinds, relative7);
6775
+ const kindRule = findRuleMatch(config.kinds, relative8);
5749
6776
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
5750
- if (fs12.existsSync(absolute) && fs12.statSync(absolute).isFile()) {
5751
- const buffer = fs12.readFileSync(absolute);
6777
+ if (fs14.existsSync(absolute) && fs14.statSync(absolute).isFile()) {
6778
+ const buffer = fs14.readFileSync(absolute);
5752
6779
  const decoded = decodeText(buffer);
5753
6780
  if (decoded.binary) {
5754
6781
  result.included = false;
@@ -5760,12 +6787,12 @@ function explainPath(config, target) {
5760
6787
  result.bytes = buffer.length;
5761
6788
  result.lines = countLines(text2);
5762
6789
  if (decoded.encoding !== "utf-8") result.encoding = decoded.encoding;
5763
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative7);
6790
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative8);
5764
6791
  result.secretCount = findings.length;
5765
6792
  result.secrets = findings.slice(0, 20).map((finding) => ({ rules: finding.rules, line: finding.line }));
5766
6793
  }
5767
6794
  const manifest = readManifest2(config);
5768
- 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;
5769
6796
  result.part = entry?.part ?? null;
5770
6797
  result.inclusionMode = entry?.inclusionMode;
5771
6798
  return result;
@@ -5996,14 +7023,14 @@ function compareManifests(before, after) {
5996
7023
  function loadManifest(target) {
5997
7024
  let isDirectory;
5998
7025
  try {
5999
- isDirectory = fs12.statSync(target).isDirectory();
7026
+ isDirectory = fs14.statSync(target).isDirectory();
6000
7027
  } catch {
6001
7028
  throw new Error(`No snapshot found at ${target}: the path does not exist.`);
6002
7029
  }
6003
- const manifestPath = isDirectory ? path13.join(target, "manifest.json") : target;
7030
+ const manifestPath = isDirectory ? path16.join(target, "manifest.json") : target;
6004
7031
  let raw;
6005
7032
  try {
6006
- raw = fs12.readFileSync(manifestPath, "utf8");
7033
+ raw = fs14.readFileSync(manifestPath, "utf8");
6007
7034
  } catch {
6008
7035
  throw new Error(`Could not read a manifest at ${manifestPath}.`);
6009
7036
  }
@@ -6060,15 +7087,15 @@ function renderDiffText(diff) {
6060
7087
  function appendGitignoreEntries(cwd, entries) {
6061
7088
  const wanted = [...new Set(entries)].filter((entry) => entry.trim().length > 0);
6062
7089
  if (wanted.length === 0) return { added: [], note: null };
6063
- const ignorePath = path13.join(cwd, ".gitignore");
7090
+ const ignorePath = path16.join(cwd, ".gitignore");
6064
7091
  const manual = `Add ${wanted.map((entry) => `"${entry}"`).join(" and ")} to your .gitignore yourself.`;
6065
7092
  let existing = "";
6066
7093
  try {
6067
- if (fs12.existsSync(ignorePath)) {
6068
- if (fs12.statSync(ignorePath).isDirectory()) {
7094
+ if (fs14.existsSync(ignorePath)) {
7095
+ if (fs14.statSync(ignorePath).isDirectory()) {
6069
7096
  return { added: [], note: `${ignorePath} is a directory, so it was not updated. ${manual}` };
6070
7097
  }
6071
- existing = fs12.readFileSync(ignorePath, "utf8");
7098
+ existing = fs14.readFileSync(ignorePath, "utf8");
6072
7099
  }
6073
7100
  } catch (err) {
6074
7101
  return {
@@ -6081,7 +7108,7 @@ function appendGitignoreEntries(cwd, entries) {
6081
7108
  if (added.length === 0) return { added: [], note: null };
6082
7109
  const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
6083
7110
  try {
6084
- fs12.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
7111
+ fs14.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
6085
7112
  `, "utf8");
6086
7113
  } catch (err) {
6087
7114
  return { added: [], note: `${err.message}. ${manual}` };
@@ -6089,7 +7116,7 @@ function appendGitignoreEntries(cwd, entries) {
6089
7116
  return { added, note: null };
6090
7117
  }
6091
7118
  function gitignorePath(cwd, filePath) {
6092
- return path13.relative(cwd, filePath).split(path13.sep).join("/");
7119
+ return path16.relative(cwd, filePath).split(path16.sep).join("/");
6093
7120
  }
6094
7121
 
6095
7122
  // src/wizard/config-template.ts
@@ -6097,7 +7124,12 @@ var DEFAULT_ANSWERS = {
6097
7124
  preset: "generic",
6098
7125
  branch: "main",
6099
7126
  maxPartBytes: "300k",
7127
+ maxPartTokens: null,
6100
7128
  largeFileBytes: "200k",
7129
+ maxMonolithBytes: null,
7130
+ discovery: "auto",
7131
+ skeleton: false,
7132
+ sarif: false,
6101
7133
  include: [],
6102
7134
  excludePatterns: [],
6103
7135
  artifacts: {
@@ -6150,6 +7182,9 @@ function renderConfig(answers) {
6150
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
6151
7183
  // Hard ceiling per part. "300k", 307200 and "1.5mb" are all accepted.
6152
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)},
6153
7188
  // Files above this size get a head/tail preview instead of the full body.
6154
7189
  largeFileBytes: ${size(answers.largeFileBytes)},
6155
7190
  previewHeadLines: 80,
@@ -6160,6 +7195,22 @@ function renderConfig(answers) {
6160
7195
  byLayer: {},
6161
7196
  },
6162
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
+
6163
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
6164
7215
  // Allow-list. Empty means "everything that is not ignored".
6165
7216
  // Example: ["src/**", "packages/*/src/**"]
@@ -6169,7 +7220,7 @@ function renderConfig(answers) {
6169
7220
  // How candidate files are found: "auto" lists them through git when git can
6170
7221
  // and walks the tree otherwise; "git" and "walk" force one path, and "walk" is
6171
7222
  // the escape hatch when .git exists but git cannot list it.
6172
- discovery: "auto",
7223
+ discovery: ${JSON.stringify(answers.discovery)},
6173
7224
 
6174
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
6175
7226
  // "merge" = the preset's rules plus yours (yours win). "replace" = yours only.
@@ -6200,7 +7251,7 @@ function renderConfig(answers) {
6200
7251
  monolith: ${artifacts.monolith}, // full-codebase.json + .md (roughly doubles output)
6201
7252
  },
6202
7253
  // Skip the monolith above this size. null = no limit.
6203
- maxMonolithBytes: null,
7254
+ maxMonolithBytes: ${answers.maxMonolithBytes === null ? "null" : size(answers.maxMonolithBytes)},
6204
7255
  // Top-level switch for the monolith, equivalent to artifacts.monolith.
6205
7256
  // When both are present the artifact entry wins; this one exists so the
6206
7257
  // generated file lists every setting the tool accepts.
@@ -6577,10 +7628,10 @@ var CONFIG_CANDIDATES = [
6577
7628
  ];
6578
7629
  function chooseConfigFileName(cwd) {
6579
7630
  if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
6580
- const pkgPath = path13.join(cwd, "package.json");
6581
- 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";
6582
7633
  try {
6583
- const parsed = JSON.parse(fs12.readFileSync(pkgPath, "utf8"));
7634
+ const parsed = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
6584
7635
  return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
6585
7636
  } catch {
6586
7637
  return "snapshot.config.mjs";
@@ -6594,7 +7645,7 @@ var SIZE_CHOICES = [
6594
7645
  ];
6595
7646
  function packageJsonHasConfig(cwd) {
6596
7647
  try {
6597
- 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"));
6598
7649
  return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
6599
7650
  } catch {
6600
7651
  return false;
@@ -6611,11 +7662,12 @@ function artifactAnswers(options) {
6611
7662
  const keep = new Set(options.onlyArtifacts);
6612
7663
  for (const name of ARTIFACT_NAMES) artifacts[name] = keep.has(name);
6613
7664
  }
7665
+ if (options.includeMonolith === true) artifacts.monolith = true;
6614
7666
  return artifacts;
6615
7667
  }
6616
7668
  function topLevelDirs(cwd) {
6617
7669
  try {
6618
- 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();
6619
7671
  } catch {
6620
7672
  return [];
6621
7673
  }
@@ -6628,7 +7680,7 @@ function describeJsonValue(value) {
6628
7680
  }
6629
7681
  function writeFileOrExplain(target, content) {
6630
7682
  try {
6631
- fs12.writeFileSync(target, content, "utf8");
7683
+ fs14.writeFileSync(target, content, "utf8");
6632
7684
  } catch (err) {
6633
7685
  const code = err.code;
6634
7686
  const reason = code === "EACCES" || code === "EPERM" || code === "EROFS" ? `permission denied (${code})` : code === "EISDIR" ? "it is a directory" : `${err.message}`;
@@ -6639,7 +7691,7 @@ function canWriteModuleConfig(name, cwd) {
6639
7691
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
6640
7692
  if (!name.endsWith(".js")) return false;
6641
7693
  try {
6642
- 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"));
6643
7695
  return parsed.type === "module";
6644
7696
  } catch {
6645
7697
  return false;
@@ -6674,19 +7726,19 @@ function serializePackageJson(raw, value) {
6674
7726
  return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
6675
7727
  }
6676
7728
  function updatePackageJson(cwd, notes) {
6677
- const packagePath = path13.join(cwd, "package.json");
7729
+ const packagePath = path16.join(cwd, "package.json");
6678
7730
  const unchanged = {
6679
7731
  scripts: [],
6680
7732
  dependency: { name: GENERATOR_NAME, range: null, added: false }
6681
7733
  };
6682
- if (!fs12.existsSync(packagePath)) {
7734
+ if (!fs14.existsSync(packagePath)) {
6683
7735
  notes.push("No package.json found, so npm scripts and the devDependency were not added.");
6684
7736
  return unchanged;
6685
7737
  }
6686
7738
  let raw;
6687
7739
  let parsed;
6688
7740
  try {
6689
- raw = fs12.readFileSync(packagePath, "utf8");
7741
+ raw = fs14.readFileSync(packagePath, "utf8");
6690
7742
  parsed = JSON.parse(raw);
6691
7743
  } catch {
6692
7744
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
@@ -6711,17 +7763,26 @@ function updatePackageJson(cwd, notes) {
6711
7763
  scriptsWereBroken = true;
6712
7764
  }
6713
7765
  const added = [];
7766
+ const isSelfRepository = parsed.name === GENERATOR_NAME;
7767
+ const scriptPrefix = isSelfRepository ? "node ./dist/bin.js" : `npx ${GENERATOR_NAME}`;
6714
7768
  if (!scripts.snapshot) {
6715
- scripts.snapshot = `npx ${GENERATOR_NAME} generate`;
7769
+ scripts.snapshot = `${scriptPrefix} generate`;
6716
7770
  added.push("snapshot");
6717
7771
  }
6718
7772
  if (!scripts["snapshot:validate"]) {
6719
- scripts["snapshot:validate"] = `npx ${GENERATOR_NAME} validate`;
7773
+ scripts["snapshot:validate"] = `${scriptPrefix} validate`;
6720
7774
  added.push("snapshot:validate");
6721
7775
  }
6722
7776
  const declared = declaredRange(parsed);
6723
7777
  let dependency;
6724
- 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) {
6725
7786
  dependency = { name: GENERATOR_NAME, range: declared, added: false };
6726
7787
  } else {
6727
7788
  const range = `^${GENERATOR_VERSION}`;
@@ -6771,7 +7832,15 @@ async function runInit(options) {
6771
7832
  // `validate` warn about a mismatch the user never caused.
6772
7833
  branch: getGitInfo(options.cwd).branch,
6773
7834
  maxPartBytes: options.maxPartBytes ?? detected.recommendedBudget ?? DEFAULT_ANSWERS.maxPartBytes,
6774
- 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,
6775
7844
  include: options.include ?? [],
6776
7845
  excludePatterns: options.exclude ?? [],
6777
7846
  artifacts: artifactAnswers(options),
@@ -6978,7 +8047,7 @@ async function runInit(options) {
6978
8047
  const configText = renderConfig(answers);
6979
8048
  const preferredName = chooseConfigFileName(options.cwd);
6980
8049
  const presentConfigs = CONFIG_CANDIDATES.filter(
6981
- (name) => fs12.existsSync(path13.join(options.cwd, name))
8050
+ (name) => fs14.existsSync(path16.join(options.cwd, name))
6982
8051
  );
6983
8052
  const present = presentConfigs[0] ?? null;
6984
8053
  if (presentConfigs.length > 1) {
@@ -6987,7 +8056,7 @@ async function runInit(options) {
6987
8056
  );
6988
8057
  }
6989
8058
  const configName = present !== null && options.force ? present : preferredName;
6990
- const configPath = path13.join(options.cwd, configName);
8059
+ const configPath = path16.join(options.cwd, configName);
6991
8060
  const reused = present !== null && options.force !== true && options.reuseConfig === true;
6992
8061
  if (reused) notes.push(`Kept the existing ${present} instead of writing a new one.`);
6993
8062
  if (options.dryRun) {
@@ -7002,7 +8071,8 @@ async function runInit(options) {
7002
8071
  scripts: [],
7003
8072
  gitignoreEntries: [],
7004
8073
  dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
7005
- notes
8074
+ notes,
8075
+ outDir: answers.outDir
7006
8076
  };
7007
8077
  }
7008
8078
  if (present !== null && present !== preferredName && !options.force && !reused) {
@@ -7020,7 +8090,7 @@ async function runInit(options) {
7020
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.'
7021
8091
  );
7022
8092
  }
7023
- if (fs12.existsSync(configPath) && !options.force && !reused) {
8093
+ if (fs14.existsSync(configPath) && !options.force && !reused) {
7024
8094
  throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
7025
8095
  }
7026
8096
  if (!reused) writeFileOrExplain(configPath, configText);
@@ -7038,14 +8108,15 @@ async function runInit(options) {
7038
8108
  scripts,
7039
8109
  gitignoreEntries: ignore.added,
7040
8110
  dependency,
7041
- notes
8111
+ notes,
8112
+ outDir: answers.outDir
7042
8113
  };
7043
8114
  }
7044
8115
  function describeTarget(cwd) {
7045
- const relative7 = path13.relative(process.cwd(), cwd);
7046
- if (relative7 === "") return "this directory";
7047
- if (relative7.startsWith("..") || path13.isAbsolute(relative7)) return cwd;
7048
- 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;
7049
8120
  }
7050
8121
  function firstRunMessage(reason, cwd) {
7051
8122
  const where = describeTarget(cwd);
@@ -7068,7 +8139,7 @@ function planFirstRun(facts) {
7068
8139
  var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
7069
8140
  function isEffectivelyEmpty(cwd) {
7070
8141
  try {
7071
- return fs12.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
8142
+ return fs14.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
7072
8143
  } catch {
7073
8144
  return false;
7074
8145
  }
@@ -7076,19 +8147,203 @@ function isEffectivelyEmpty(cwd) {
7076
8147
  function hasPriorSnapshot(cwd, outDir) {
7077
8148
  let parsed;
7078
8149
  try {
7079
- 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"));
7080
8151
  } catch {
7081
8152
  return false;
7082
8153
  }
7083
8154
  if (parsed === null || typeof parsed !== "object") return false;
7084
8155
  return isOurGeneratorName(parsed.generator);
7085
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
+ }
7086
8341
 
7087
8342
  // src/index.ts
7088
8343
  var SCHEMA_VERSION = "3.0.0";
7089
8344
  var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
7090
8345
  function sha256Hex2(input) {
7091
- return crypto.createHash("sha256").update(input).digest("hex");
8346
+ return crypto2.createHash("sha256").update(input).digest("hex");
7092
8347
  }
7093
8348
  var TS_EXTENSIONS = [
7094
8349
  ".ts",
@@ -7127,71 +8382,15 @@ function hasStructuralSummary(relativePath) {
7127
8382
  function bump(counts, key) {
7128
8383
  counts[key] = (counts[key] ?? 0) + 1;
7129
8384
  }
7130
- var RELATIVE_SPECIFIER = /^\.{1,2}\//;
7131
- var IMPORT_CANDIDATE_SUFFIXES = [
7132
- ".ts",
7133
- ".tsx",
7134
- ".mts",
7135
- ".cts",
7136
- ".js",
7137
- ".jsx",
7138
- ".mjs",
7139
- ".cjs",
7140
- "/index.ts",
7141
- "/index.tsx",
7142
- "/index.js",
7143
- "/index.mjs"
7144
- ];
7145
- var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
7146
- ".ts",
7147
- ".tsx",
7148
- ".mts",
7149
- ".cts",
7150
- ".js",
7151
- ".jsx",
7152
- ".mjs",
7153
- ".cjs"
7154
- ]);
7155
- function buildImportResolver(files) {
7156
- const byPath = /* @__PURE__ */ new Map();
7157
- const byPathFolded = /* @__PURE__ */ new Map();
7158
- const byStem = /* @__PURE__ */ new Map();
7159
- const byStemFolded = /* @__PURE__ */ new Map();
7160
- const remember = (map, key, value) => {
7161
- if (!map.has(key)) map.set(key, value);
7162
- };
7163
- for (const file of files) {
7164
- const relativePath = file.relativePath;
7165
- remember(byPath, relativePath, relativePath);
7166
- remember(byPathFolded, relativePath.toLowerCase(), relativePath);
7167
- const extension = path13.posix.extname(relativePath);
7168
- if (!SOURCE_EXTENSIONS.has(extension)) continue;
7169
- const stem = relativePath.slice(0, -extension.length);
7170
- remember(byStem, stem, relativePath);
7171
- remember(byStemFolded, stem.toLowerCase(), relativePath);
7172
- if (path13.posix.basename(stem) === "index") {
7173
- const directory = path13.posix.dirname(stem);
7174
- remember(byStem, directory, relativePath);
7175
- 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;
7176
8391
  }
7177
8392
  }
7178
- return { byPath, byPathFolded, byStem, byStemFolded };
7179
- }
7180
- function resolveSpecifier(fromFile, specifier, resolver) {
7181
- if (!RELATIVE_SPECIFIER.test(specifier)) return null;
7182
- const joined = path13.posix.normalize(
7183
- path13.posix.join(path13.posix.dirname(fromFile), specifier)
7184
- );
7185
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
7186
- const direct = exact(joined);
7187
- if (direct) return direct;
7188
- for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
7189
- const hit = exact(`${joined}${suffix}`);
7190
- if (hit) return hit;
7191
- }
7192
- const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
7193
- const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
7194
- return byStem(joined) ?? byStem(stem);
8393
+ items.length = write;
7195
8394
  }
7196
8395
  function buildIndexes(files) {
7197
8396
  const resolver = buildImportResolver(files);
@@ -7243,7 +8442,7 @@ function buildIndexes(files) {
7243
8442
  }))
7244
8443
  };
7245
8444
  }
7246
- function verifyInMemory(files, parts, maxPartBytes) {
8445
+ function verifyInMemory(files, parts) {
7247
8446
  const errors = [];
7248
8447
  const partNames = new Set(parts.map((part) => part.filename));
7249
8448
  for (const file of files) {
@@ -7259,8 +8458,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
7259
8458
  if (actualBytes !== part.metadata.byteSize) {
7260
8459
  errors.push(`${part.filename} byteSize ${part.metadata.byteSize} != actual ${actualBytes}.`);
7261
8460
  }
7262
- if (actualBytes > maxPartBytes) {
7263
- 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
+ );
7264
8466
  }
7265
8467
  if (part.metadata.includedFiles.length !== part.metadata.fileCount) {
7266
8468
  errors.push(`${part.filename} fileCount does not match its included files.`);
@@ -7273,11 +8475,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
7273
8475
  return errors;
7274
8476
  }
7275
8477
  function resolveSafeOutputDirs(config) {
7276
- const cwd = path13.resolve(config.cwd);
7277
- const outDir = path13.resolve(cwd, config.outDir);
7278
- const tmpDir = path13.resolve(cwd, config.tmpDir);
7279
- const root = path13.parse(cwd).root;
7280
- 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}`);
7281
8483
  const reject = (reason) => {
7282
8484
  throw new Error(
7283
8485
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -7333,14 +8535,14 @@ function listEntries(dir) {
7333
8535
  const walk = (absolute, prefix) => {
7334
8536
  let children;
7335
8537
  try {
7336
- children = fs12.readdirSync(absolute, { withFileTypes: true });
8538
+ children = fs14.readdirSync(absolute, { withFileTypes: true });
7337
8539
  } catch {
7338
8540
  return;
7339
8541
  }
7340
8542
  for (const child of children) {
7341
- const relative7 = prefix === "" ? child.name : `${prefix}/${child.name}`;
7342
- entries.push(relative7);
7343
- 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);
7344
8546
  }
7345
8547
  };
7346
8548
  walk(dir, "");
@@ -7357,28 +8559,33 @@ function markerPayload(dir, state, id, createdAt) {
7357
8559
  pid: process.pid,
7358
8560
  entries: listEntries(dir)
7359
8561
  };
7360
- const manifestPath = path13.join(dir, "manifest.json");
7361
- if (fs12.existsSync(manifestPath)) {
7362
- 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));
7363
8565
  }
7364
8566
  return marker2;
7365
8567
  }
7366
8568
  function writeOwnershipMarker(dir, marker2) {
7367
- fs12.writeFileSync(
7368
- path13.join(dir, OWNERSHIP_MARKER),
8569
+ fs14.writeFileSync(
8570
+ path16.join(dir, OWNERSHIP_MARKER),
7369
8571
  `${JSON.stringify(marker2, null, 2)}
7370
8572
  `,
7371
8573
  "utf8"
7372
8574
  );
7373
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
+ }
7374
8581
  function readOwnershipMarker(dir) {
7375
- const markerPath = path13.join(dir, OWNERSHIP_MARKER);
7376
- if (!fs12.existsSync(markerPath)) {
8582
+ const markerPath = path16.join(dir, OWNERSHIP_MARKER);
8583
+ if (!fs14.existsSync(markerPath)) {
7377
8584
  return { marker: null, problem: "it holds no .codebase-snapshot marker" };
7378
8585
  }
7379
8586
  let raw;
7380
8587
  try {
7381
- raw = fs12.readFileSync(markerPath, "utf8");
8588
+ raw = fs14.readFileSync(markerPath, "utf8");
7382
8589
  } catch {
7383
8590
  return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
7384
8591
  }
@@ -7446,11 +8653,11 @@ function assessSnapshotDirectory(dir) {
7446
8653
  marker: marker2
7447
8654
  };
7448
8655
  }
7449
- const manifestPath = path13.join(dir, "manifest.json");
7450
- if (fs12.existsSync(manifestPath)) {
8656
+ const manifestPath = path16.join(dir, "manifest.json");
8657
+ if (fs14.existsSync(manifestPath)) {
7451
8658
  let raw;
7452
8659
  try {
7453
- raw = fs12.readFileSync(manifestPath, "utf8");
8660
+ raw = fs14.readFileSync(manifestPath, "utf8");
7454
8661
  } catch {
7455
8662
  return {
7456
8663
  owned: false,
@@ -7532,13 +8739,13 @@ function containsNoFiles(dir) {
7532
8739
  const current = pending.pop();
7533
8740
  let entries;
7534
8741
  try {
7535
- entries = fs12.readdirSync(current, { withFileTypes: true });
8742
+ entries = fs14.readdirSync(current, { withFileTypes: true });
7536
8743
  } catch {
7537
8744
  return false;
7538
8745
  }
7539
8746
  for (const entry of entries) {
7540
8747
  if (entry.isDirectory()) {
7541
- pending.push(path13.join(current, entry.name));
8748
+ pending.push(path16.join(current, entry.name));
7542
8749
  } else if (!IGNORABLE_ENTRIES.has(entry.name)) {
7543
8750
  return false;
7544
8751
  }
@@ -7547,13 +8754,13 @@ function containsNoFiles(dir) {
7547
8754
  return true;
7548
8755
  }
7549
8756
  function assertReplaceable(target, label, secretRules) {
7550
- if (!fs12.existsSync(target)) return;
7551
- if (!fs12.statSync(target).isDirectory()) {
8757
+ if (!fs14.existsSync(target)) return;
8758
+ if (!fs14.statSync(target).isDirectory()) {
7552
8759
  throw new Error(
7553
8760
  `Refusing to write into ${label} ${target}: it exists but is not a directory.`
7554
8761
  );
7555
8762
  }
7556
- const entries = fs12.readdirSync(target);
8763
+ const entries = fs14.readdirSync(target);
7557
8764
  if (entries.length === 0) return;
7558
8765
  if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
7559
8766
  if (containsNoFiles(target)) return;
@@ -7573,18 +8780,18 @@ function assertReplaceable(target, label, secretRules) {
7573
8780
  );
7574
8781
  }
7575
8782
  function assertStagingRootUsable(tmpDir, secretRules) {
7576
- if (!fs12.existsSync(tmpDir)) return;
7577
- if (!fs12.statSync(tmpDir).isDirectory()) {
8783
+ if (!fs14.existsSync(tmpDir)) return;
8784
+ if (!fs14.statSync(tmpDir).isDirectory()) {
7578
8785
  throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
7579
8786
  }
7580
- const entries = fs12.readdirSync(tmpDir);
8787
+ const entries = fs14.readdirSync(tmpDir);
7581
8788
  if (entries.length === 0) return;
7582
8789
  const foreign = entries.filter((name) => {
7583
- const full = path13.join(tmpDir, name);
8790
+ const full = path16.join(tmpDir, name);
7584
8791
  if (!name.startsWith(RUN_DIR_PREFIX)) return true;
7585
8792
  let stats;
7586
8793
  try {
7587
- stats = fs12.statSync(full);
8794
+ stats = fs14.statSync(full);
7588
8795
  } catch {
7589
8796
  return true;
7590
8797
  }
@@ -7592,7 +8799,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7592
8799
  const { marker: marker2 } = readOwnershipMarker(full);
7593
8800
  if (marker2) return false;
7594
8801
  try {
7595
- return fs12.readdirSync(full).length > 0;
8802
+ return fs14.readdirSync(full).length > 0;
7596
8803
  } catch {
7597
8804
  return true;
7598
8805
  }
@@ -7607,13 +8814,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7607
8814
  function cleanStaleRunDirs(tmpDir) {
7608
8815
  let entries;
7609
8816
  try {
7610
- entries = fs12.readdirSync(tmpDir);
8817
+ entries = fs14.readdirSync(tmpDir);
7611
8818
  } catch {
7612
8819
  return;
7613
8820
  }
7614
8821
  for (const name of entries) {
7615
8822
  if (!name.startsWith(RUN_DIR_PREFIX)) continue;
7616
- const full = path13.join(tmpDir, name);
8823
+ const full = path16.join(tmpDir, name);
7617
8824
  const { marker: marker2 } = readOwnershipMarker(full);
7618
8825
  if (marker2) {
7619
8826
  const created = Date.parse(marker2.createdAt);
@@ -7621,33 +8828,33 @@ function cleanStaleRunDirs(tmpDir) {
7621
8828
  const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
7622
8829
  if (aged || abandoned) {
7623
8830
  try {
7624
- fs12.rmSync(full, { recursive: true, force: true });
8831
+ fs14.rmSync(full, { recursive: true, force: true });
7625
8832
  } catch {
7626
8833
  }
7627
8834
  }
7628
8835
  continue;
7629
8836
  }
7630
8837
  try {
7631
- 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 });
7632
8839
  } catch {
7633
8840
  }
7634
8841
  }
7635
8842
  }
7636
8843
  function removeIfEmpty(dir) {
7637
8844
  try {
7638
- 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 });
7639
8846
  } catch {
7640
8847
  }
7641
8848
  }
7642
8849
  function ensureParentDirectory(target, label) {
7643
- const parent = path13.dirname(target);
7644
- if (fs12.existsSync(parent) && !fs12.statSync(parent).isDirectory()) {
8850
+ const parent = path16.dirname(target);
8851
+ if (fs14.existsSync(parent) && !fs14.statSync(parent).isDirectory()) {
7645
8852
  throw new Error(
7646
8853
  `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
7647
8854
  );
7648
8855
  }
7649
8856
  try {
7650
- fs12.mkdirSync(parent, { recursive: true });
8857
+ fs14.mkdirSync(parent, { recursive: true });
7651
8858
  } catch (err) {
7652
8859
  throw new Error(
7653
8860
  `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
@@ -7655,8 +8862,7 @@ function ensureParentDirectory(target, label) {
7655
8862
  }
7656
8863
  }
7657
8864
  function acquireRunLock(stagingRoot) {
7658
- const key = lockKeyFor(stagingRoot);
7659
- const lockPath = path13.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
8865
+ const lockPath = lockPathFor(stagingRoot);
7660
8866
  const payload = `${JSON.stringify({
7661
8867
  pid: process.pid,
7662
8868
  stagingRoot,
@@ -7665,22 +8871,22 @@ function acquireRunLock(stagingRoot) {
7665
8871
  `;
7666
8872
  for (let attempt = 0; attempt < 2; attempt += 1) {
7667
8873
  try {
7668
- const handle = fs12.openSync(lockPath, "wx");
8874
+ const handle = fs14.openSync(lockPath, "wx");
7669
8875
  try {
7670
- fs12.writeSync(handle, payload);
8876
+ fs14.writeSync(handle, payload);
7671
8877
  } finally {
7672
- fs12.closeSync(handle);
8878
+ fs14.closeSync(handle);
7673
8879
  }
7674
8880
  return () => {
7675
8881
  try {
7676
- fs12.rmSync(lockPath, { force: true });
8882
+ fs14.rmSync(lockPath, { force: true });
7677
8883
  } catch {
7678
8884
  }
7679
8885
  };
7680
8886
  } catch (err) {
7681
8887
  if (err.code !== "EEXIST") throw err;
7682
8888
  if (attempt === 0 && lockOwnerIsGone(lockPath)) {
7683
- fs12.rmSync(lockPath, { force: true });
8889
+ fs14.rmSync(lockPath, { force: true });
7684
8890
  continue;
7685
8891
  }
7686
8892
  throw new Error(
@@ -7693,16 +8899,19 @@ function acquireRunLock(stagingRoot) {
7693
8899
  function lockKeyFor(stagingRoot) {
7694
8900
  let key = stagingRoot;
7695
8901
  try {
7696
- key = fs12.realpathSync(stagingRoot);
8902
+ key = fs14.realpathSync(stagingRoot);
7697
8903
  } catch {
7698
8904
  }
7699
8905
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
7700
8906
  return sha256Hex2(key).slice(0, 16);
7701
8907
  }
8908
+ function lockPathFor(stagingRoot) {
8909
+ return path16.join(os.tmpdir(), `codebase-snapshot-${lockKeyFor(stagingRoot)}.lock`);
8910
+ }
7702
8911
  function lockOwnerIsGone(lockPath) {
7703
8912
  let raw;
7704
8913
  try {
7705
- raw = fs12.readFileSync(lockPath, "utf8");
8914
+ raw = fs14.readFileSync(lockPath, "utf8");
7706
8915
  } catch {
7707
8916
  return false;
7708
8917
  }
@@ -7710,9 +8919,9 @@ function lockOwnerIsGone(lockPath) {
7710
8919
  try {
7711
8920
  pid = JSON.parse(raw).pid;
7712
8921
  } catch {
7713
- return false;
8922
+ return lockFileIsAncient(lockPath);
7714
8923
  }
7715
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
8924
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return lockFileIsAncient(lockPath);
7716
8925
  try {
7717
8926
  process.kill(pid, 0);
7718
8927
  return false;
@@ -7720,14 +8929,21 @@ function lockOwnerIsGone(lockPath) {
7720
8929
  return err.code === "ESRCH";
7721
8930
  }
7722
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
+ }
7723
8939
  function writeManifestAtomically(manifestPath, manifest) {
7724
8940
  const tempPath = `${manifestPath}.${process.pid}.tmp`;
7725
- fs12.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
8941
+ fs14.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7726
8942
  try {
7727
- fs12.renameSync(tempPath, manifestPath);
8943
+ fs14.renameSync(tempPath, manifestPath);
7728
8944
  } catch (err) {
7729
8945
  try {
7730
- fs12.rmSync(tempPath, { force: true });
8946
+ fs14.rmSync(tempPath, { force: true });
7731
8947
  } catch {
7732
8948
  }
7733
8949
  throw err;
@@ -7750,31 +8966,31 @@ function swapIntoPlace(stagingDir, outDir) {
7750
8966
  }
7751
8967
  function swapOnce(stagingDir, outDir) {
7752
8968
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
7753
- const hadPrevious = fs12.existsSync(outDir);
7754
- if (hadPrevious) fs12.renameSync(outDir, backupDir);
8969
+ const hadPrevious = fs14.existsSync(outDir);
8970
+ if (hadPrevious) fs14.renameSync(outDir, backupDir);
7755
8971
  try {
7756
8972
  try {
7757
- fs12.renameSync(stagingDir, outDir);
8973
+ fs14.renameSync(stagingDir, outDir);
7758
8974
  } catch (err) {
7759
8975
  if (err.code !== "EXDEV") throw err;
7760
8976
  const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
7761
8977
  try {
7762
- fs12.cpSync(stagingDir, partialDir, { recursive: true });
7763
- fs12.renameSync(partialDir, outDir);
7764
- 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 });
7765
8981
  } catch (copyErr) {
7766
8982
  try {
7767
- fs12.rmSync(partialDir, { recursive: true, force: true });
8983
+ fs14.rmSync(partialDir, { recursive: true, force: true });
7768
8984
  } catch {
7769
8985
  }
7770
8986
  throw copyErr;
7771
8987
  }
7772
8988
  }
7773
- if (hadPrevious) fs12.rmSync(backupDir, { recursive: true, force: true });
8989
+ if (hadPrevious) fs14.rmSync(backupDir, { recursive: true, force: true });
7774
8990
  } catch (err) {
7775
- if (hadPrevious && fs12.existsSync(backupDir)) {
7776
- if (fs12.existsSync(outDir)) fs12.rmSync(outDir, { recursive: true, force: true });
7777
- 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);
7778
8994
  }
7779
8995
  throw err;
7780
8996
  }
@@ -7783,7 +8999,7 @@ function sleepSync(milliseconds) {
7783
8999
  const buffer = new Int32Array(new SharedArrayBuffer(4));
7784
9000
  Atomics.wait(buffer, 0, 0, milliseconds);
7785
9001
  }
7786
- function artifactWarnings(artifacts) {
9002
+ function artifactWarnings(artifacts, sarif) {
7787
9003
  const messages = [];
7788
9004
  if (!artifacts.manifest) {
7789
9005
  messages.push("manifest.json is disabled: `validate` and `report` cannot inspect this snapshot.");
@@ -7793,7 +9009,7 @@ function artifactWarnings(artifacts) {
7793
9009
  "parts are disabled but navigation.md is enabled: its links point at part files that were not written."
7794
9010
  );
7795
9011
  }
7796
- if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
9012
+ if (!sarif && ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
7797
9013
  messages.push(
7798
9014
  "every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
7799
9015
  );
@@ -7865,13 +9081,25 @@ function generateSnapshot(configInput, options) {
7865
9081
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
7866
9082
  includeSensitiveFiles: config.includeSensitiveFiles,
7867
9083
  secretRules: config.secretRules,
9084
+ discovery: config.discovery,
7868
9085
  // The output and staging directories this run is about to write, relative to the root. A
7869
9086
  // first run cannot have marked them yet, so the name alone keeps a run from recording its own
7870
9087
  // output — but only at these exact paths, not at every directory that shares the name.
7871
9088
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
7872
- (directory) => path13.relative(config.cwd, path13.resolve(config.cwd, directory)).replace(/\\/g, "/")
7873
- ).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(".."))
7874
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;
7875
9103
  const files = [];
7876
9104
  const monolithBodies = /* @__PURE__ */ new Map();
7877
9105
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
@@ -7882,6 +9110,7 @@ function generateSnapshot(configInput, options) {
7882
9110
  return files.some((file) => file.isPreviewTruncated && !file.isLarge);
7883
9111
  };
7884
9112
  const encodingNotes = [];
9113
+ const fileWarnings = [];
7885
9114
  const oversizeSummaryPaths = [];
7886
9115
  const previewSummaryPaths = [];
7887
9116
  const layerCounts = {};
@@ -7889,13 +9118,13 @@ function generateSnapshot(configInput, options) {
7889
9118
  let totalLines = 0;
7890
9119
  let totalSizeBytes = 0;
7891
9120
  let largeFileCount = 0;
7892
- for (const relativePath of collection.files) {
7893
- const absolutePath = path13.join(config.cwd, relativePath);
9121
+ for (const relativePath of candidateFiles) {
9122
+ const absolutePath = path16.join(config.cwd, relativePath);
7894
9123
  let stats;
7895
9124
  try {
7896
- stats = fs12.statSync(absolutePath);
9125
+ stats = fs14.statSync(absolutePath);
7897
9126
  if (!stats.isFile()) {
7898
- const nestedRepository = stats.isDirectory() && fs12.existsSync(path13.join(absolutePath, ".git"));
9127
+ const nestedRepository = stats.isDirectory() && fs14.existsSync(path16.join(absolutePath, ".git"));
7899
9128
  skipped.push(
7900
9129
  nestedRepository ? {
7901
9130
  path: relativePath,
@@ -7909,13 +9138,17 @@ function generateSnapshot(configInput, options) {
7909
9138
  skipped.push({ path: relativePath, reason: "read-error" });
7910
9139
  continue;
7911
9140
  }
7912
- 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)) {
7913
9146
  skipped.push({ path: relativePath, reason: "binary", detail: `Size: ${stats.size} B` });
7914
9147
  continue;
7915
9148
  }
7916
9149
  let buffer;
7917
9150
  try {
7918
- buffer = fs12.readFileSync(absolutePath);
9151
+ buffer = fs14.readFileSync(absolutePath);
7919
9152
  } catch {
7920
9153
  skipped.push({ path: relativePath, reason: "read-error" });
7921
9154
  continue;
@@ -7970,7 +9203,16 @@ function generateSnapshot(configInput, options) {
7970
9203
  let finalContent = redacted;
7971
9204
  let isPreviewTruncated = false;
7972
9205
  let includedLines = lineCount;
7973
- 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") {
7974
9216
  const preview = truncatePreview(
7975
9217
  redacted,
7976
9218
  config.previewHeadLines,
@@ -7979,8 +9221,9 @@ function generateSnapshot(configInput, options) {
7979
9221
  finalContent = preview.content;
7980
9222
  isPreviewTruncated = preview.isTruncated;
7981
9223
  includedLines = preview.includedLines;
9224
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
7982
9225
  }
7983
- if (config.includeMonolith && isPreviewTruncated) {
9226
+ if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
7984
9227
  monolithBodies.set(relativePath, redacted);
7985
9228
  }
7986
9229
  let summary;
@@ -7995,14 +9238,19 @@ function generateSnapshot(configInput, options) {
7995
9238
  summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
7996
9239
  summaryQuality = outcome?.quality;
7997
9240
  if (isSourceSummary(summary) && summary.parseErrors) {
7998
- warnings.push(
7999
- `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
8000
- );
9241
+ fileWarnings.push({
9242
+ path: relativePath,
9243
+ message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
9244
+ });
8001
9245
  }
8002
9246
  } catch (err) {
8003
- warnings.push(
8004
- `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
8005
- );
9247
+ fileWarnings.push({
9248
+ path: relativePath,
9249
+ message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
9250
+ /\s+/g,
9251
+ " "
9252
+ )
9253
+ });
8006
9254
  }
8007
9255
  }
8008
9256
  files.push({
@@ -8014,7 +9262,7 @@ function generateSnapshot(configInput, options) {
8014
9262
  layer: layer2,
8015
9263
  kind,
8016
9264
  language,
8017
- inclusionMode: isPreviewTruncated ? "preview" : "full",
9265
+ inclusionMode,
8018
9266
  isLarge,
8019
9267
  isPreviewTruncated,
8020
9268
  originalLines: lineCount,
@@ -8026,6 +9274,58 @@ function generateSnapshot(configInput, options) {
8026
9274
  content: finalContent
8027
9275
  });
8028
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
+ }
8029
9329
  const encodingBuckets = /* @__PURE__ */ new Map();
8030
9330
  for (const note of encodingNotes) {
8031
9331
  const bucket = encodingBuckets.get(note.encoding) ?? { paths: [], invalidBytes: 0 };
@@ -8076,13 +9376,8 @@ function generateSnapshot(configInput, options) {
8076
9376
  );
8077
9377
  }
8078
9378
  const parts = partitionByBudget(files, config, fingerprint);
8079
- const maxEffectiveBudget = Math.max(
8080
- config.maxPartBytes,
8081
- config.budgets.default,
8082
- ...Object.values(config.budgets.byLayer)
8083
- );
8084
9379
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
8085
- const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
9380
+ const validationErrors = verifyInMemory(files, parts);
8086
9381
  const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
8087
9382
  const maskName = (value) => maskPathText(value, config.secretRules);
8088
9383
  const manifest = {
@@ -8102,12 +9397,26 @@ function generateSnapshot(configInput, options) {
8102
9397
  config: {
8103
9398
  digest: configDigest(config),
8104
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 },
8105
9403
  largeFileBytes: config.largeFileBytes,
8106
9404
  previewHeadLines: config.previewHeadLines,
8107
9405
  previewTailLines: config.previewTailLines,
8108
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,
8109
9411
  includeSensitiveFiles: config.includeSensitiveFiles,
8110
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 } : {},
8111
9420
  artifacts: { ...config.artifacts },
8112
9421
  include: [...config.include].map(maskName),
8113
9422
  preset: config.preset,
@@ -8139,6 +9448,9 @@ function generateSnapshot(configInput, options) {
8139
9448
  skippedFiles: skipped.length,
8140
9449
  totalSourceBytes: totalSizeBytes,
8141
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),
8142
9454
  partCount: parts.length,
8143
9455
  warningCount: 0,
8144
9456
  redactedLines: 0
@@ -8177,7 +9489,9 @@ function generateSnapshot(configInput, options) {
8177
9489
  );
8178
9490
  manifest.totals.warningCount = manifest.warnings.length;
8179
9491
  }
8180
- 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
+ }
8181
9495
  manifest.totals.warningCount = manifest.warnings.length;
8182
9496
  let projectTree = null;
8183
9497
  if (config.artifacts.projectTree) {
@@ -8190,6 +9504,13 @@ function generateSnapshot(configInput, options) {
8190
9504
  manifest.totals.warningCount = manifest.warnings.length;
8191
9505
  }
8192
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
+ }
8193
9514
  let monolithJson = null;
8194
9515
  let monolithMarkdown = null;
8195
9516
  if (config.includeMonolith) {
@@ -8240,17 +9561,17 @@ function generateSnapshot(configInput, options) {
8240
9561
  const lockKeys = Array.from(/* @__PURE__ */ new Set([tmpDir, outDir])).sort();
8241
9562
  const releases = lockKeys.map((key) => acquireRunLock(key));
8242
9563
  const releaseLock = () => {
8243
- for (const release of releases.reverse()) release();
9564
+ for (const release of [...releases].reverse()) release();
8244
9565
  };
8245
9566
  let stagingDir = null;
8246
9567
  try {
8247
9568
  ensureParentDirectory(outDir, "outDir");
8248
- fs12.mkdirSync(tmpDir, { recursive: true });
9569
+ fs14.mkdirSync(tmpDir, { recursive: true });
8249
9570
  cleanStaleRunDirs(tmpDir);
8250
- const snapshotId = crypto.randomUUID();
9571
+ const snapshotId = crypto2.randomUUID();
8251
9572
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8252
- stagingDir = path13.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
8253
- fs12.mkdirSync(stagingDir);
9573
+ stagingDir = path16.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
9574
+ fs14.mkdirSync(stagingDir);
8254
9575
  writeOwnershipMarker(stagingDir, {
8255
9576
  generator: GENERATOR_NAME,
8256
9577
  version: GENERATOR_VERSION,
@@ -8260,41 +9581,44 @@ function generateSnapshot(configInput, options) {
8260
9581
  id: snapshotId,
8261
9582
  pid: process.pid
8262
9583
  });
8263
- const partsDir = path13.join(stagingDir, "parts");
8264
- const reportsDir = path13.join(stagingDir, "reports");
8265
- if (artifacts.parts) fs12.mkdirSync(partsDir, { recursive: true });
8266
- if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
8267
- 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 });
8268
9589
  }
8269
9590
  if (artifacts.parts) {
8270
9591
  for (const part of parts) {
8271
- fs12.writeFileSync(path13.join(partsDir, part.filename), part.markdown, "utf8");
9592
+ fs14.writeFileSync(path16.join(partsDir, part.filename), part.markdown, "utf8");
8272
9593
  }
8273
9594
  }
8274
9595
  if (projectTree !== null) {
8275
- fs12.writeFileSync(path13.join(reportsDir, "project-tree.md"), projectTree, "utf8");
9596
+ fs14.writeFileSync(path16.join(reportsDir, "project-tree.md"), projectTree, "utf8");
8276
9597
  }
8277
9598
  if (artifacts.navigation) {
8278
- fs12.writeFileSync(path13.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
9599
+ fs14.writeFileSync(path16.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
8279
9600
  }
8280
9601
  if (artifacts.audit) {
8281
- fs12.writeFileSync(
8282
- path13.join(reportsDir, "audit.md"),
9602
+ fs14.writeFileSync(
9603
+ path16.join(reportsDir, "audit.md"),
8283
9604
  generateAuditReport(manifest, secretFindings, config.secretRules),
8284
9605
  "utf8"
8285
9606
  );
8286
9607
  }
9608
+ if (sarifText !== null) {
9609
+ fs14.writeFileSync(path16.join(reportsDir, "audit.sarif"), sarifText, "utf8");
9610
+ }
8287
9611
  if (artifacts.readme) {
8288
- fs12.writeFileSync(path13.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
9612
+ fs14.writeFileSync(path16.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
8289
9613
  }
8290
9614
  if (artifacts.manifest) {
8291
- 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");
8292
9616
  }
8293
9617
  if (monolithJson !== null) {
8294
- fs12.writeFileSync(path13.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
9618
+ fs14.writeFileSync(path16.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
8295
9619
  }
8296
9620
  if (monolithMarkdown !== null) {
8297
- fs12.writeFileSync(path13.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
9621
+ fs14.writeFileSync(path16.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
8298
9622
  }
8299
9623
  writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
8300
9624
  swapIntoPlace(stagingDir, outDir);
@@ -8302,31 +9626,41 @@ function generateSnapshot(configInput, options) {
8302
9626
  const warningsBeforeHook = manifest.warnings.length;
8303
9627
  const pendingHook = runAfterGenerate(config, manifest);
8304
9628
  const shipHookState = () => {
9629
+ if (!markerBelongsTo(outDir, snapshotId)) return;
8305
9630
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
8306
- writeManifestAtomically(path13.join(outDir, "manifest.json"), manifest);
9631
+ writeManifestAtomically(path16.join(outDir, "manifest.json"), manifest);
8307
9632
  }
8308
9633
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
8309
9634
  };
8310
9635
  shipHookState();
8311
9636
  if (pendingHook) {
8312
- pendingHook.catch((err) => {
8313
- const message = maskPathText(
8314
- err instanceof Error ? err.message : String(err),
8315
- config.secretRules
8316
- );
8317
- manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
8318
- manifest.totals.warningCount = manifest.warnings.length;
8319
- try {
8320
- shipHookState();
8321
- } 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
+ }
8322
9655
  }
8323
- });
9656
+ );
9657
+ pendingHooks.set(outDir, pendingHook);
8324
9658
  }
8325
9659
  return manifest;
8326
9660
  } catch (err) {
8327
9661
  if (stagingDir !== null) {
8328
9662
  try {
8329
- fs12.rmSync(stagingDir, { recursive: true, force: true });
9663
+ fs14.rmSync(stagingDir, { recursive: true, force: true });
8330
9664
  } catch {
8331
9665
  }
8332
9666
  }
@@ -8336,10 +9670,17 @@ function generateSnapshot(configInput, options) {
8336
9670
  releaseLock();
8337
9671
  }
8338
9672
  }
9673
+ var pendingHooks = /* @__PURE__ */ new Map();
8339
9674
  async function generateSnapshotAsync(configInput, options) {
8340
- 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;
8341
9684
  }
8342
9685
 
8343
- 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, installKind, 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, updateCommand, validateSnapshot, wrapLines };
8344
- //# sourceMappingURL=chunk-KUVFTJHK.js.map
8345
- //# sourceMappingURL=chunk-KUVFTJHK.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 };