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.
package/dist/index.cjs CHANGED
@@ -1,16 +1,17 @@
1
1
  'use strict';
2
2
 
3
- var crypto = require('crypto');
3
+ var crypto2 = require('crypto');
4
4
  var async_hooks = require('async_hooks');
5
- var fs12 = require('fs');
5
+ var fs14 = require('fs');
6
6
  var os = require('os');
7
- var path13 = require('path');
7
+ var path16 = require('path');
8
8
  var ts = require('typescript');
9
9
  var module$1 = require('module');
10
10
  var url = require('url');
11
11
  var childProcess = require('child_process');
12
12
  var https = require('https');
13
13
  var readline = require('readline');
14
+ require('worker_threads');
14
15
 
15
16
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
17
 
@@ -32,10 +33,10 @@ function _interopNamespace(e) {
32
33
  return Object.freeze(n);
33
34
  }
34
35
 
35
- var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
36
- var fs12__namespace = /*#__PURE__*/_interopNamespace(fs12);
36
+ var crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
37
+ var fs14__namespace = /*#__PURE__*/_interopNamespace(fs14);
37
38
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
38
- var path13__namespace = /*#__PURE__*/_interopNamespace(path13);
39
+ var path16__namespace = /*#__PURE__*/_interopNamespace(path16);
39
40
  var ts__default = /*#__PURE__*/_interopDefault(ts);
40
41
  var childProcess__namespace = /*#__PURE__*/_interopNamespace(childProcess);
41
42
  var https__namespace = /*#__PURE__*/_interopNamespace(https);
@@ -155,6 +156,59 @@ var cfamilyProfile = {
155
156
  }
156
157
  };
157
158
 
159
+ // src/ast/profiles/dart.ts
160
+ var IMPORT = /^import\s+['"]([^'"]+)['"]/;
161
+ var PART = /^part(?:\s+of)?\s+['"]?([\w./]+)['"]?/;
162
+ var TYPE = /^(?:abstract\s+|base\s+|final\s+|sealed\s+|interface\s+)*(class|mixin|enum|extension|typedef)\s+([A-Za-z_]\w*)/;
163
+ var FUNC = /^(?:Future<[^>]*>|Stream<[^>]*>|void|bool|int|double|num|String|dynamic|var|[A-Z][\w<>, ?]*)\s+([A-Za-z_]\w*)\s*[(<]/;
164
+ var VARIABLE = /^(?:late\s+)?(?:final|const|var)\s+(?:[\w<>?,. ]+?\s+)?([A-Za-z_]\w*)\s*=/;
165
+ var dartProfile = {
166
+ id: "dart",
167
+ extensions: [".dart"],
168
+ summarize(_path, content) {
169
+ const summary = emptySummary();
170
+ eachLine(content, (line) => {
171
+ const trimmed = line.trim();
172
+ if (!trimmed || trimmed.startsWith("//")) return;
173
+ const imported = IMPORT.exec(trimmed);
174
+ if (imported) {
175
+ summary.imports.push(imported[1]);
176
+ return;
177
+ }
178
+ if (/^part\b/.test(trimmed)) {
179
+ const part = PART.exec(trimmed);
180
+ summary.directives.push(part ? `part ${part[1]}` : "part");
181
+ return;
182
+ }
183
+ if (!isTopLevel(line)) return;
184
+ const type = TYPE.exec(trimmed);
185
+ if (type) {
186
+ const [, keyword, name] = type;
187
+ if (keyword === "mixin" || keyword === "extension") summary.types.push(name);
188
+ else if (keyword === "enum") summary.enums.push(name);
189
+ else if (keyword === "typedef") summary.types.push(name);
190
+ else summary.classes.push(name);
191
+ if (isPublicName(name)) summary.exports.push(name);
192
+ return;
193
+ }
194
+ const func = FUNC.exec(trimmed);
195
+ if (func) {
196
+ const name = func[1];
197
+ summary.functions.push(name);
198
+ if (isPublicName(name)) summary.exports.push(name);
199
+ return;
200
+ }
201
+ const variable = VARIABLE.exec(trimmed);
202
+ if (variable) {
203
+ const name = variable[1];
204
+ summary.variables.push(name);
205
+ if (isPublicName(name)) summary.exports.push(name);
206
+ }
207
+ });
208
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
209
+ }
210
+ };
211
+
158
212
  // src/ast/profiles/dotnet.ts
159
213
  var USING2 = /^using\s+([\w.]+)\s*;/;
160
214
  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*)/;
@@ -195,8 +249,8 @@ var dotnetProfile = {
195
249
  };
196
250
 
197
251
  // src/ast/profiles/go.ts
198
- var FUNC = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
199
- var TYPE = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
252
+ var FUNC2 = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
253
+ var TYPE2 = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
200
254
  var VAR_CONST = /^(?:var|const)\s+([A-Za-z_]\w*)/;
201
255
  var IMPORT_SINGLE = /^import\s+(?:[\w.]+\s+)?"([^"]+)"/;
202
256
  var IMPORT_OPEN = /^import\s*\(/;
@@ -229,14 +283,14 @@ var goProfile = {
229
283
  return;
230
284
  }
231
285
  if (!isTopLevel(line)) return;
232
- const method = FUNC.exec(line);
286
+ const method = FUNC2.exec(line);
233
287
  if (method) {
234
288
  const name = method[1];
235
289
  summary.functions.push(name);
236
290
  if (isCapitalizedName(name)) summary.exports.push(name);
237
291
  return;
238
292
  }
239
- const declared = TYPE.exec(line);
293
+ const declared = TYPE2.exec(line);
240
294
  if (declared) {
241
295
  const name = declared[1];
242
296
  const body = declared[2].trim();
@@ -256,8 +310,54 @@ var goProfile = {
256
310
  }
257
311
  };
258
312
 
313
+ // src/ast/profiles/graphql.ts
314
+ var DEFINITION = /^(?:extend\s+)?(type|interface|input|enum|union|scalar|schema)\s+([A-Za-z_]\w*)/;
315
+ var OPERATION = /^(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/;
316
+ var DIRECTIVE = /^directive\s+@([A-Za-z_]\w*)/;
317
+ var IMPORT2 = /^#import\s+"([^"]+)"/;
318
+ var graphqlProfile = {
319
+ id: "graphql",
320
+ extensions: [".graphql", ".gql"],
321
+ summarize(_path, content) {
322
+ const summary = emptySummary();
323
+ eachLine(content, (line) => {
324
+ const trimmed = line.trim();
325
+ if (!trimmed || trimmed.startsWith("#") && !IMPORT2.test(trimmed)) return;
326
+ const imported = IMPORT2.exec(trimmed);
327
+ if (imported) {
328
+ summary.imports.push(imported[1]);
329
+ return;
330
+ }
331
+ const directive = DIRECTIVE.exec(trimmed);
332
+ if (directive) {
333
+ summary.functions.push(`@${directive[1]}`);
334
+ return;
335
+ }
336
+ const definition = DEFINITION.exec(trimmed);
337
+ if (definition) {
338
+ const [, keyword, name] = definition;
339
+ if (keyword === "interface") summary.interfaces.push(name);
340
+ else if (keyword === "enum") summary.enums.push(name);
341
+ else if (keyword === "input" || keyword === "scalar" || keyword === "union") {
342
+ summary.types.push(name);
343
+ } else summary.classes.push(name);
344
+ summary.exports.push(name);
345
+ return;
346
+ }
347
+ if (!isTopLevel(line)) return;
348
+ const operation = OPERATION.exec(trimmed);
349
+ if (operation) {
350
+ const name = operation[1];
351
+ summary.functions.push(name);
352
+ summary.routeHandlers.push(name);
353
+ }
354
+ });
355
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
356
+ }
357
+ };
358
+
259
359
  // src/ast/profiles/jvm.ts
260
- var IMPORT = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
360
+ var IMPORT3 = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
261
361
  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*)/;
262
362
  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*)/;
263
363
  var SCALA_DEF = /^\s*def\s+([A-Za-z_]\w*)/;
@@ -265,9 +365,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
265
365
  var jvmProfile = {
266
366
  id: "jvm",
267
367
  extensions: [".java", ".kt", ".kts", ".scala"],
268
- summarize(path14, content) {
368
+ summarize(path17, content) {
269
369
  const summary = emptySummary();
270
- const lower = path14.toLowerCase();
370
+ const lower = path17.toLowerCase();
271
371
  const isJava = lower.endsWith(".java");
272
372
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
273
373
  eachLine(content, (line) => {
@@ -275,7 +375,7 @@ var jvmProfile = {
275
375
  if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
276
376
  return;
277
377
  }
278
- const imported = IMPORT.exec(trimmed);
378
+ const imported = IMPORT3.exec(trimmed);
279
379
  if (imported) {
280
380
  summary.imports.push(imported[1]);
281
381
  return;
@@ -356,8 +456,50 @@ var phpProfile = {
356
456
  }
357
457
  };
358
458
 
459
+ // src/ast/profiles/protobuf.ts
460
+ var DECLARATION = /^(?:message|enum|service|extend)\s+([A-Za-z_]\w*)/;
461
+ var RPC = /^\s*rpc\s+([A-Za-z_]\w*)/;
462
+ var IMPORT4 = /^\s*import\s+(?:public\s+|weak\s+)?"([^"]+)"/;
463
+ var PACKAGE = /^\s*package\s+([\w.]+)\s*;/;
464
+ var protobufProfile = {
465
+ id: "protobuf",
466
+ extensions: [".proto"],
467
+ summarize(_path, content) {
468
+ const summary = emptySummary();
469
+ eachLine(content, (line) => {
470
+ const trimmed = line.trim();
471
+ if (!trimmed || trimmed.startsWith("//")) return;
472
+ const imported = IMPORT4.exec(line);
473
+ if (imported) {
474
+ summary.imports.push(imported[1]);
475
+ return;
476
+ }
477
+ const pkg = PACKAGE.exec(line);
478
+ if (pkg) {
479
+ summary.directives.push(`package ${pkg[1]}`);
480
+ return;
481
+ }
482
+ const rpc = RPC.exec(line);
483
+ if (rpc) {
484
+ const name2 = rpc[1];
485
+ summary.functions.push(name2);
486
+ return;
487
+ }
488
+ const declaration = DECLARATION.exec(trimmed);
489
+ if (!declaration) return;
490
+ const name = declaration[1];
491
+ const keyword = /^\w+/.exec(trimmed)?.[0] ?? "";
492
+ if (keyword === "service") summary.interfaces.push(name);
493
+ else if (keyword === "enum") summary.enums.push(name);
494
+ else summary.classes.push(name);
495
+ summary.exports.push(name);
496
+ });
497
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
498
+ }
499
+ };
500
+
359
501
  // src/ast/profiles/python.ts
360
- var IMPORT2 = /^import\s+(.+)$/;
502
+ var IMPORT5 = /^import\s+(.+)$/;
361
503
  var FROM_IMPORT = /^from\s+([\w.]+)\s+import\s+(.+)$/;
362
504
  var CLASS = /^class\s+([A-Za-z_]\w*)/;
363
505
  var DEF = /^(?:async\s+)?def\s+([A-Za-z_]\w*)/;
@@ -383,7 +525,7 @@ var pythonProfile = {
383
525
  routePending = false;
384
526
  return;
385
527
  }
386
- const imported = IMPORT2.exec(trimmed);
528
+ const imported = IMPORT5.exec(trimmed);
387
529
  if (imported) {
388
530
  for (const part of imported[1].split(",")) {
389
531
  const name = part.trim().split(/\s+as\s+/)[0].trim();
@@ -467,7 +609,7 @@ var FN = /^(pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(
467
609
  var STRUCT = /^(pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_]\w*)/;
468
610
  var TRAIT = /^(pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/;
469
611
  var ENUM = /^(pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_]\w*)/;
470
- var TYPE2 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
612
+ var TYPE3 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
471
613
  var MOD = /^(pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)/;
472
614
  var IMPL = /^impl(?:<[^>]*>)?\s+(?:[\w:<>]+\s+for\s+)?([A-Za-z_]\w*)/;
473
615
  var rustProfile = {
@@ -509,7 +651,7 @@ var rustProfile = {
509
651
  if (isPublic) summary.exports.push(enumeration[2]);
510
652
  return;
511
653
  }
512
- const alias = TYPE2.exec(line);
654
+ const alias = TYPE3.exec(line);
513
655
  if (alias) {
514
656
  summary.types.push(alias[2]);
515
657
  if (isPublic) summary.exports.push(alias[2]);
@@ -536,6 +678,16 @@ var ROUTE_HANDLER_NAMES = /* @__PURE__ */ new Set([
536
678
  "HEAD",
537
679
  "OPTIONS"
538
680
  ]);
681
+ function bindingNames(name, into) {
682
+ if (ts__default.default.isIdentifier(name)) {
683
+ into.push(name.text);
684
+ return;
685
+ }
686
+ for (const element of name.elements) {
687
+ if (ts__default.default.isOmittedExpression(element)) continue;
688
+ bindingNames(element.name, into);
689
+ }
690
+ }
539
691
  function summarizeTypeScriptSource(relativePath, content) {
540
692
  const summary = {
541
693
  directives: [],
@@ -636,12 +788,15 @@ function summarizeTypeScriptSource(relativePath, content) {
636
788
  }
637
789
  if (node.exportClause && ts__default.default.isNamedExports(node.exportClause)) {
638
790
  for (const element of node.exportClause.elements) {
639
- summary.exports.push(element.name.text);
791
+ const exported = element.name.text;
792
+ summary.exports.push(exported);
793
+ if (ROUTE_HANDLER_NAMES.has(exported)) summary.routeHandlers.push(exported);
640
794
  }
641
795
  }
642
796
  }
643
797
  if (ts__default.default.isExportAssignment(node)) {
644
798
  summary.exports.push("default");
799
+ if (rendersJsx(node.expression)) summary.reactComponents.push("default");
645
800
  }
646
801
  if (ts__default.default.isFunctionDeclaration(node)) {
647
802
  const exported = isExported(node);
@@ -679,16 +834,18 @@ function summarizeTypeScriptSource(relativePath, content) {
679
834
  if (ts__default.default.isVariableStatement(node)) {
680
835
  const exported = isExported(node);
681
836
  for (const declaration of node.declarationList.declarations) {
682
- if (!ts__default.default.isIdentifier(declaration.name)) continue;
683
- const name = declaration.name.text;
684
- summary.variables.push(name);
685
- if (exported) {
686
- summary.exports.push(name);
687
- if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
688
- }
689
- if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
690
- if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
691
- summary.reactComponents.push(name);
837
+ const bound = [];
838
+ bindingNames(declaration.name, bound);
839
+ for (const name of bound) {
840
+ summary.variables.push(name);
841
+ if (exported) {
842
+ summary.exports.push(name);
843
+ if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
844
+ }
845
+ if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
846
+ if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
847
+ summary.reactComponents.push(name);
848
+ }
692
849
  }
693
850
  }
694
851
  }
@@ -720,16 +877,16 @@ function summarizeTypeScriptSource(relativePath, content) {
720
877
  // src/ast/profiles/sfc.ts
721
878
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
722
879
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
723
- function componentName(path14) {
724
- const base = path14.split("/").pop() ?? path14;
880
+ function componentName(path17) {
881
+ const base = path17.split("/").pop() ?? path17;
725
882
  return base.replace(/\.[^.]+$/, "");
726
883
  }
727
884
  var sfcProfile = {
728
885
  id: "sfc",
729
886
  extensions: [".vue", ".svelte", ".astro"],
730
- summarize(path14, content) {
887
+ summarize(path17, content) {
731
888
  let script = "";
732
- if (path14.toLowerCase().endsWith(".astro")) {
889
+ if (path17.toLowerCase().endsWith(".astro")) {
733
890
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
734
891
  if (frontmatter) script += `${frontmatter[1]}
735
892
  `;
@@ -738,7 +895,7 @@ var sfcProfile = {
738
895
  script += `${match[1]}
739
896
  `;
740
897
  }
741
- const name = componentName(path14);
898
+ const name = componentName(path17);
742
899
  if (!script.trim()) {
743
900
  const summary2 = emptySummary();
744
901
  if (name) {
@@ -750,7 +907,158 @@ var sfcProfile = {
750
907
  const summary = summarizeTypeScriptSource(`${name}.ts`, script);
751
908
  summary.reactComponents.push(name);
752
909
  summary.exports.push(name);
753
- return { summary, quality: "ast" };
910
+ return { summary: finalizeSummary(summary), quality: "ast" };
911
+ }
912
+ };
913
+
914
+ // src/ast/profiles/sql.ts
915
+ 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;
916
+ var ALTER = /^\s*ALTER\s+(TABLE|VIEW)\s+(?:IF\s+EXISTS\s+)?([A-Za-z_"][\w."]*)/i;
917
+ function bareName(identifier) {
918
+ const last = identifier.split(".").pop() ?? identifier;
919
+ return last.replace(/["`[\]]/g, "");
920
+ }
921
+ var sqlProfile = {
922
+ id: "sql",
923
+ extensions: [".sql"],
924
+ summarize(_path, content) {
925
+ const summary = emptySummary();
926
+ const seen = /* @__PURE__ */ new Set();
927
+ const declare = (kind, identifier) => {
928
+ const name = bareName(identifier);
929
+ if (!name || seen.has(`${kind}:${name}`)) return;
930
+ seen.add(`${kind}:${name}`);
931
+ const normalized = kind.toUpperCase().replace(/\s+/g, " ");
932
+ if (normalized === "TABLE" || normalized.endsWith("VIEW")) summary.classes.push(name);
933
+ else if (normalized === "TYPE" || normalized === "SCHEMA" || normalized === "SEQUENCE") {
934
+ summary.types.push(name);
935
+ } else summary.functions.push(name);
936
+ summary.exports.push(name);
937
+ };
938
+ eachLine(content, (line) => {
939
+ const trimmed = line.trim();
940
+ if (!trimmed || trimmed.startsWith("--")) return;
941
+ const created = CREATE.exec(trimmed);
942
+ if (created) {
943
+ declare(String(created[1]), String(created[2]));
944
+ return;
945
+ }
946
+ const altered = ALTER.exec(trimmed);
947
+ if (altered) declare(String(altered[1]), String(altered[2]));
948
+ });
949
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
950
+ }
951
+ };
952
+
953
+ // src/ast/profiles/swift.ts
954
+ var MODIFIERS = "(?:@\\w+(?:\\([^)]*\\))?\\s+|(?:public|open|internal|fileprivate|private|final|static|mutating|nonmutating|override|required|convenience|lazy|weak|unowned)\\s+)*";
955
+ var TYPE4 = new RegExp(`^${MODIFIERS}(class|struct|enum|protocol|actor|extension)\\s+([A-Za-z_]\\w*)`);
956
+ var FUNC3 = new RegExp(`^${MODIFIERS}func\\s+([A-Za-z_]\\w*)`);
957
+ var PROPERTY = new RegExp(`^${MODIFIERS}(?:let|var)\\s+([A-Za-z_]\\w*)`);
958
+ var IMPORT6 = /^import\s+([A-Za-z_][\w.]*)/;
959
+ var swiftProfile = {
960
+ id: "swift",
961
+ extensions: [".swift"],
962
+ summarize(_path, content) {
963
+ const summary = emptySummary();
964
+ eachLine(content, (line) => {
965
+ const trimmed = line.trim();
966
+ if (!trimmed || trimmed.startsWith("//")) return;
967
+ const imported = IMPORT6.exec(trimmed);
968
+ if (imported) {
969
+ summary.imports.push(imported[1]);
970
+ return;
971
+ }
972
+ if (!isTopLevel(line)) return;
973
+ const exported = /\b(?:public|open)\b/.test(trimmed);
974
+ const type = TYPE4.exec(trimmed);
975
+ if (type) {
976
+ const [, keyword, name] = type;
977
+ if (keyword === "protocol") summary.interfaces.push(name);
978
+ else if (keyword === "enum") summary.enums.push(name);
979
+ else if (keyword === "extension") summary.types.push(name);
980
+ else summary.classes.push(name);
981
+ if (exported) summary.exports.push(name);
982
+ return;
983
+ }
984
+ const func = FUNC3.exec(trimmed);
985
+ if (func) {
986
+ const name = func[1];
987
+ summary.functions.push(name);
988
+ if (exported) summary.exports.push(name);
989
+ return;
990
+ }
991
+ const property = PROPERTY.exec(trimmed);
992
+ if (property) {
993
+ const name = property[1];
994
+ summary.variables.push(name);
995
+ if (exported && isPublicName(name)) summary.exports.push(name);
996
+ }
997
+ });
998
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
999
+ }
1000
+ };
1001
+
1002
+ // src/ast/profiles/terraform.ts
1003
+ var BLOCK = /^\s*(resource|data|module|provider|variable|output|locals|terraform)\b\s*("([^"]+)")?\s*("([^"]+)")?/;
1004
+ var TERRAFORM_VERSION = /^\s*required_version\s*=\s*"([^"]+)"/;
1005
+ var terraformProfile = {
1006
+ id: "terraform",
1007
+ extensions: [".tf", ".tfvars"],
1008
+ summarize(_path, content) {
1009
+ const summary = emptySummary();
1010
+ eachLine(content, (line) => {
1011
+ const trimmed = line.trim();
1012
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//")) return;
1013
+ const version = TERRAFORM_VERSION.exec(line);
1014
+ if (version) {
1015
+ summary.directives.push(`required_version ${version[1]}`);
1016
+ return;
1017
+ }
1018
+ if (/^[A-Za-z_][\w-]*\s*=/.test(trimmed)) {
1019
+ summary.variables.push(/^([A-Za-z_][\w-]*)/.exec(trimmed)?.[1] ?? trimmed);
1020
+ return;
1021
+ }
1022
+ const block = BLOCK.exec(line);
1023
+ if (!block) return;
1024
+ const keyword = block[1];
1025
+ const first = block[3];
1026
+ const second = block[5];
1027
+ switch (keyword) {
1028
+ case "resource":
1029
+ case "data": {
1030
+ if (!first || !second) return;
1031
+ const name = `${first}.${second}`;
1032
+ summary.classes.push(name);
1033
+ summary.exports.push(name);
1034
+ return;
1035
+ }
1036
+ case "module": {
1037
+ if (!first) return;
1038
+ summary.imports.push(first);
1039
+ return;
1040
+ }
1041
+ case "variable": {
1042
+ if (!first) return;
1043
+ summary.variables.push(first);
1044
+ return;
1045
+ }
1046
+ case "output": {
1047
+ if (!first) return;
1048
+ summary.exports.push(first);
1049
+ return;
1050
+ }
1051
+ case "provider": {
1052
+ if (!first) return;
1053
+ summary.imports.push(`provider:${first}`);
1054
+ return;
1055
+ }
1056
+ default: {
1057
+ summary.directives.push(keyword);
1058
+ }
1059
+ }
1060
+ });
1061
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
754
1062
  }
755
1063
  };
756
1064
 
@@ -764,7 +1072,13 @@ var LANGUAGE_PROFILES = [
764
1072
  phpProfile,
765
1073
  rubyProfile,
766
1074
  cfamilyProfile,
767
- sfcProfile
1075
+ sfcProfile,
1076
+ swiftProfile,
1077
+ dartProfile,
1078
+ sqlProfile,
1079
+ graphqlProfile,
1080
+ protobufProfile,
1081
+ terraformProfile
768
1082
  ];
769
1083
  var BY_EXTENSION = /* @__PURE__ */ new Map();
770
1084
  for (const profile of LANGUAGE_PROFILES) {
@@ -772,15 +1086,15 @@ for (const profile of LANGUAGE_PROFILES) {
772
1086
  BY_EXTENSION.set(extension.toLowerCase(), profile);
773
1087
  }
774
1088
  }
775
- function profileForPath(path14) {
776
- const lower = path14.toLowerCase();
1089
+ function profileForPath(path17) {
1090
+ const lower = path17.toLowerCase();
777
1091
  const dot = lower.lastIndexOf(".");
778
1092
  if (dot === -1) return null;
779
1093
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
780
1094
  }
781
- function summarizeWithProfile(path14, content) {
782
- const profile = profileForPath(path14);
783
- return profile ? profile.summarize(path14, content) : null;
1095
+ function summarizeWithProfile(path17, content) {
1096
+ const profile = profileForPath(path17);
1097
+ return profile ? profile.summarize(path17, content) : null;
784
1098
  }
785
1099
 
786
1100
  // src/ast/structured.ts
@@ -880,9 +1194,17 @@ function summarizeMarkdown(content) {
880
1194
  const frontmatterKeys = [];
881
1195
  const lines = content.replace(/^\uFEFF/, "").split("\n");
882
1196
  let inFrontmatter = false;
1197
+ let fence = null;
1198
+ let closesFrontmatter = false;
1199
+ for (let index = 1; index < lines.length; index++) {
1200
+ if (lines[index].trim() === "---") {
1201
+ closesFrontmatter = true;
1202
+ break;
1203
+ }
1204
+ }
883
1205
  for (let index = 0; index < lines.length; index++) {
884
1206
  const line = lines[index];
885
- if (index === 0 && line.trim() === "---") {
1207
+ if (index === 0 && line.trim() === "---" && closesFrontmatter) {
886
1208
  inFrontmatter = true;
887
1209
  continue;
888
1210
  }
@@ -895,7 +1217,20 @@ function summarizeMarkdown(content) {
895
1217
  if (match) frontmatterKeys.push(match[1]);
896
1218
  continue;
897
1219
  }
898
- const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
1220
+ const trimmed = line.trim();
1221
+ const fenceMatch = /^(`{3,}|~{3,})(.*)$/.exec(trimmed);
1222
+ if (fenceMatch) {
1223
+ const run = fenceMatch[1];
1224
+ const rest = fenceMatch[2].trim();
1225
+ if (fence === null) {
1226
+ fence = { marker: run[0], length: run.length };
1227
+ } else if (run[0] === fence.marker && run.length >= fence.length && rest === "") {
1228
+ fence = null;
1229
+ }
1230
+ continue;
1231
+ }
1232
+ if (fence !== null) continue;
1233
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(trimmed);
899
1234
  if (headingMatch) {
900
1235
  headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
901
1236
  }
@@ -905,6 +1240,52 @@ function summarizeMarkdown(content) {
905
1240
  frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
906
1241
  };
907
1242
  }
1243
+ function hasCollapsibleBody(node) {
1244
+ if (!("body" in node)) return false;
1245
+ const body = node.body;
1246
+ return body !== void 0 && ts__default.default.isBlock(body);
1247
+ }
1248
+ function isTsxFileName(fileName) {
1249
+ return fileName.toLowerCase().endsWith(".tsx");
1250
+ }
1251
+ function skeletonizeTypeScript(source, fileName = "file.ts") {
1252
+ const scriptKind = isTsxFileName(fileName) ? ts__default.default.ScriptKind.TSX : ts__default.default.ScriptKind.TS;
1253
+ const sourceFile = ts__default.default.createSourceFile(
1254
+ fileName,
1255
+ source,
1256
+ ts__default.default.ScriptTarget.Latest,
1257
+ true,
1258
+ scriptKind
1259
+ );
1260
+ const spans = [];
1261
+ const visit = (node) => {
1262
+ if ((ts__default.default.isFunctionDeclaration(node) || ts__default.default.isMethodDeclaration(node) || ts__default.default.isConstructorDeclaration(node) || ts__default.default.isGetAccessorDeclaration(node) || ts__default.default.isSetAccessorDeclaration(node) || ts__default.default.isFunctionExpression(node) || ts__default.default.isArrowFunction(node)) && hasCollapsibleBody(node)) {
1263
+ const body = node.body;
1264
+ const start = body.getStart(sourceFile);
1265
+ const end = body.getEnd();
1266
+ if (end > start) spans.push({ start, end });
1267
+ }
1268
+ ts__default.default.forEachChild(node, visit);
1269
+ };
1270
+ visit(sourceFile);
1271
+ if (spans.length === 0) return { text: source, collapsed: 0 };
1272
+ const outermost = [];
1273
+ for (const span of spans.sort((a, b) => a.start - b.start || b.end - a.end)) {
1274
+ const last = outermost[outermost.length - 1];
1275
+ if (last && span.start < last.end) continue;
1276
+ outermost.push(span);
1277
+ }
1278
+ let text2 = source;
1279
+ for (let index = outermost.length - 1; index >= 0; index -= 1) {
1280
+ const span = outermost[index];
1281
+ text2 = `${text2.slice(0, span.start)}{ /* ... */ }${text2.slice(span.end)}`;
1282
+ }
1283
+ return { text: text2, collapsed: outermost.length };
1284
+ }
1285
+ function isSkeletonizable(filePath) {
1286
+ const lower = filePath.toLowerCase();
1287
+ return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts");
1288
+ }
908
1289
  var MANIFEST_FILES = [
909
1290
  "package.json",
910
1291
  "pyproject.toml",
@@ -951,15 +1332,15 @@ var SKIPPED_WALK_DIRS = /* @__PURE__ */ new Set([
951
1332
  ]);
952
1333
  function readSmallFile(fullPath) {
953
1334
  try {
954
- const stats = fs12__namespace.statSync(fullPath);
1335
+ const stats = fs14__namespace.statSync(fullPath);
955
1336
  if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
956
- return fs12__namespace.readFileSync(fullPath, "utf8");
1337
+ return fs14__namespace.readFileSync(fullPath, "utf8");
957
1338
  } catch {
958
1339
  return null;
959
1340
  }
960
1341
  }
961
1342
  function readManifest(cwd, name) {
962
- return readSmallFile(path13__namespace.join(cwd, name));
1343
+ return readSmallFile(path16__namespace.join(cwd, name));
963
1344
  }
964
1345
  function dependenciesFromPackageJson(raw) {
965
1346
  const names = /* @__PURE__ */ new Set();
@@ -1019,7 +1400,7 @@ function pnpmWorkspacePatterns(raw) {
1019
1400
  function childDirectories(cwd, parent) {
1020
1401
  let entries;
1021
1402
  try {
1022
- entries = fs12__namespace.readdirSync(path13__namespace.join(cwd, parent), { withFileTypes: true });
1403
+ entries = fs14__namespace.readdirSync(path16__namespace.join(cwd, parent), { withFileTypes: true });
1023
1404
  } catch {
1024
1405
  return [];
1025
1406
  }
@@ -1054,13 +1435,13 @@ function workspacePackageDirs(cwd, patterns) {
1054
1435
  function scanWorkspacePackages(cwd, patterns) {
1055
1436
  const files = /* @__PURE__ */ new Set();
1056
1437
  const dependencies = /* @__PURE__ */ new Set();
1057
- for (const relative7 of workspacePackageDirs(cwd, patterns)) {
1058
- const full = path13__namespace.join(cwd, relative7);
1438
+ for (const relative8 of workspacePackageDirs(cwd, patterns)) {
1439
+ const full = path16__namespace.join(cwd, relative8);
1059
1440
  try {
1060
- for (const entry of fs12__namespace.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
1441
+ for (const entry of fs14__namespace.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
1061
1442
  } catch {
1062
1443
  }
1063
- const raw = readSmallFile(path13__namespace.join(full, "package.json"));
1444
+ const raw = readSmallFile(path16__namespace.join(full, "package.json"));
1064
1445
  if (raw !== null) {
1065
1446
  for (const name of dependenciesFromPackageJson(raw)) dependencies.add(name);
1066
1447
  }
@@ -1077,7 +1458,7 @@ function countMarkdownFiles(cwd) {
1077
1458
  directoriesSeen += 1;
1078
1459
  let entries;
1079
1460
  try {
1080
- entries = fs12__namespace.readdirSync(current.dir, { withFileTypes: true });
1461
+ entries = fs14__namespace.readdirSync(current.dir, { withFileTypes: true });
1081
1462
  } catch {
1082
1463
  continue;
1083
1464
  }
@@ -1088,7 +1469,7 @@ function countMarkdownFiles(cwd) {
1088
1469
  }
1089
1470
  if (!entry.isDirectory() || current.depth >= 2) continue;
1090
1471
  if (entry.name.startsWith(".") || SKIPPED_WALK_DIRS.has(entry.name)) continue;
1091
- queue.push({ dir: path13__namespace.join(current.dir, entry.name), depth: current.depth + 1 });
1472
+ queue.push({ dir: path16__namespace.join(current.dir, entry.name), depth: current.depth + 1 });
1092
1473
  }
1093
1474
  }
1094
1475
  return count;
@@ -1097,7 +1478,7 @@ function buildPresetContext(cwd) {
1097
1478
  const files = /* @__PURE__ */ new Set();
1098
1479
  const dirs = /* @__PURE__ */ new Set();
1099
1480
  try {
1100
- for (const entry of fs12__namespace.readdirSync(cwd, { withFileTypes: true })) {
1481
+ for (const entry of fs14__namespace.readdirSync(cwd, { withFileTypes: true })) {
1101
1482
  if (entry.isDirectory()) dirs.add(entry.name);
1102
1483
  else if (entry.isFile()) files.add(entry.name);
1103
1484
  }
@@ -1114,7 +1495,7 @@ function buildPresetContext(cwd) {
1114
1495
  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;
1115
1496
  let workspace = { files: /* @__PURE__ */ new Set(), dependencies: /* @__PURE__ */ new Set() };
1116
1497
  if (isWorkspace) {
1117
- const pnpmRaw = files.has("pnpm-workspace.yaml") ? readSmallFile(path13__namespace.join(cwd, "pnpm-workspace.yaml")) : null;
1498
+ const pnpmRaw = files.has("pnpm-workspace.yaml") ? readSmallFile(path16__namespace.join(cwd, "pnpm-workspace.yaml")) : null;
1118
1499
  const declared = pnpmRaw === null ? [] : pnpmWorkspacePatterns(pnpmRaw);
1119
1500
  const patterns = declared.length > 0 ? declared : declaredWorkspaces.length > 0 ? declaredWorkspaces : DEFAULT_WORKSPACE_PATTERNS;
1120
1501
  workspace = scanWorkspacePackages(cwd, patterns);
@@ -1959,10 +2340,15 @@ function detectPreset(cwd, forced) {
1959
2340
  // src/security/patterns.ts
1960
2341
  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)";
1961
2342
  var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
1962
- var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
1963
- var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/%:.~-]))([A-Za-z0-9_+=/%:~-]{1,512})(?![A-Za-z0-9_+=/%:.~-]|\\()";
1964
- var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/%:.~-]))([A-Za-z0-9_+=/%:~-]{8,512})(?![A-Za-z0-9_+=/%:.~-]|\\()";
1965
- var QUOTED_VALUE = String.raw`((?:[^"'\\\n]|\\.){1,512})`;
2343
+ 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)";
2344
+ var UNQUOTED_MIN_LENGTH = 2;
2345
+ var PROSE_CONTINUATION = String.raw`(?!\s+[a-z])`;
2346
+ 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;
2347
+ 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;
2348
+ var ASSIGN = String.raw`(?<![=!<>])[:=](?!=)`;
2349
+ var NOT_AFTER_QUOTE = String.raw`(?<!["'])`;
2350
+ var NOT_A_NAMESPACED_IDENTIFIER = String.raw`(?![a-z][a-z0-9]*(?:\.[a-z][a-z0-9-]*)+/)`;
2351
+ var QUOTED_VALUE = String.raw`((?:[^"'\\\n]|\\\\["'](?=[A-Za-z0-9_])|\\.){1,512})`;
1966
2352
  var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
1967
2353
  var DEFAULT_SECRET_PATTERNS = [
1968
2354
  {
@@ -2005,7 +2391,7 @@ var DEFAULT_SECRET_PATTERNS = [
2005
2391
  // literal words matched. The value is masked alone, so the key survives.
2006
2392
  rule: "aws-secret-access-key",
2007
2393
  pattern: new RegExp(
2008
- `${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
2394
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${STRONG_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
2009
2395
  "i"
2010
2396
  ),
2011
2397
  valueGroup: 1
@@ -2117,7 +2503,7 @@ var DEFAULT_SECRET_PATTERNS = [
2117
2503
  // the key name.
2118
2504
  rule: "credential-assignment",
2119
2505
  pattern: new RegExp(
2120
- `(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])${QUOTED_VALUE}\\1`,
2506
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${STRONG_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_NAMESPACED_IDENTIFIER}(["'\`])${QUOTED_VALUE}\\1`,
2121
2507
  "i"
2122
2508
  ),
2123
2509
  valueGroup: 2
@@ -2127,7 +2513,7 @@ var DEFAULT_SECRET_PATTERNS = [
2127
2513
  // are accepted only against a quoted value long enough to be a credential.
2128
2514
  rule: "credential-assignment-weak",
2129
2515
  pattern: new RegExp(
2130
- `(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])${QUOTED_VALUE}\\1`,
2516
+ `(?:["']${WEAK_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${WEAK_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}(["'\`])${QUOTED_VALUE}\\1`,
2131
2517
  "i"
2132
2518
  ),
2133
2519
  valueGroup: 2
@@ -2138,7 +2524,7 @@ var DEFAULT_SECRET_PATTERNS = [
2138
2524
  // `POSTGRES_PASSWORD:` in the output.
2139
2525
  rule: "credential-assignment-unquoted",
2140
2526
  pattern: new RegExp(
2141
- `${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2527
+ `${NOT_AFTER_QUOTE}${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2142
2528
  "i"
2143
2529
  ),
2144
2530
  valueGroup: 1
@@ -2149,7 +2535,7 @@ var DEFAULT_SECRET_PATTERNS = [
2149
2535
  // `pass: 3` / `credentials: none` readable.
2150
2536
  rule: "credential-assignment-unquoted-weak",
2151
2537
  pattern: new RegExp(
2152
- `${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
2538
+ `${NOT_AFTER_QUOTE}${WEAK_CREDENTIAL_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
2153
2539
  "i"
2154
2540
  ),
2155
2541
  valueGroup: 1
@@ -2170,7 +2556,7 @@ var DEFAULT_SECRET_PATTERNS = [
2170
2556
  // above because it also matches a *quoted key* followed by an unquoted
2171
2557
  // value, e.g. `"client_secret": abc123`.
2172
2558
  rule: "generic-secret-assignment",
2173
- pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
2559
+ pattern: /(?<!["'])(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*(?<![=!<>])[:=](?!=)\s*["']([^"'\s]{8,256})["']/i,
2174
2560
  valueGroup: 1
2175
2561
  },
2176
2562
  {
@@ -2186,8 +2572,17 @@ var DEFAULT_SECRET_PATTERNS = [
2186
2572
  // Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
2187
2573
  // comment or space disabled the rule, so `TOKEN=secret # rotate` and
2188
2574
  // `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
2575
+ //
2576
+ // The name is now required to be uppercase. With `/i`, a reassignment inside a
2577
+ // function — ` key = fs.realpathSync(stagingRoot)` — matched `key`…`KEY` and
2578
+ // was emitted as `key = [REDACTED](stagingRoot)`, which corrupted this
2579
+ // repository's own `src/index.ts`. Shell and `.env` names are uppercase by
2580
+ // convention and a lowercase `key = …` at an indent is a variable, so the case
2581
+ // of the name is the discriminator that separates the two. The value must end
2582
+ // at end-of-line, whitespace or a comment: that is what stops a call expression
2583
+ // (`NAME=f(x)`) from being read as an assigned literal.
2189
2584
  rule: "env-secret-assignment",
2190
- 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,
2585
+ 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#;])/,
2191
2586
  valueGroup: 1
2192
2587
  }
2193
2588
  ];
@@ -2325,6 +2720,12 @@ function scanAndRedactSecrets(content, rules, filePath) {
2325
2720
  continue;
2326
2721
  }
2327
2722
  const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
2723
+ if (group && isSelfNamingConstant(line, group[0], group[1])) continue;
2724
+ if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index)) continue;
2725
+ if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index)) {
2726
+ continue;
2727
+ }
2728
+ if (group && isTemplateInterpolation(line, group[0], group[1])) continue;
2328
2729
  spans.push(
2329
2730
  group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
2330
2731
  );
@@ -2354,6 +2755,46 @@ function expandOverToken(line, start, end) {
2354
2755
  while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
2355
2756
  return { start: left, end: right };
2356
2757
  }
2758
+ function startsInsideStringLiteral(line, matchIndex) {
2759
+ let runStart = matchIndex;
2760
+ while (runStart > 0 && /[A-Za-z0-9_-]/.test(line[runStart - 1])) runStart -= 1;
2761
+ const before = runStart > 0 ? line[runStart - 1] : "";
2762
+ return before === '"' || before === "'" || before === "`";
2763
+ }
2764
+ function isTemplateInterpolation(line, start, end) {
2765
+ return line[start] === "$" && line[start + 1] === "{" && line[end - 1] === "}";
2766
+ }
2767
+ function isSelfNamingConstant(line, start, end) {
2768
+ const value = line.slice(start, end);
2769
+ if (!/[a-z][A-Z]/.test(value)) return false;
2770
+ let index = start;
2771
+ if (index > 0) {
2772
+ const openingQuote = line[index - 1];
2773
+ if (openingQuote === '"' || openingQuote === "'" || openingQuote === "`") index -= 1;
2774
+ }
2775
+ while (index > 0 && /\s/.test(line[index - 1])) index -= 1;
2776
+ if (index === 0) return false;
2777
+ if (line[index - 1] !== ":" && line[index - 1] !== "=") return false;
2778
+ index -= 1;
2779
+ while (index > 0 && /\s/.test(line[index - 1])) index -= 1;
2780
+ if (index > 0) {
2781
+ const closingQuote = line[index - 1];
2782
+ if (closingQuote === '"' || closingQuote === "'" || closingQuote === "`") index -= 1;
2783
+ }
2784
+ return line.endsWith(value, index);
2785
+ }
2786
+ function isUnquotedValue(line, start) {
2787
+ const before = start > 0 ? line[start - 1] : "";
2788
+ return before !== '"' && before !== "'" && before !== "`";
2789
+ }
2790
+ var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|declare|export|using)\s+$/;
2791
+ var DECLARATION_WINDOW = 40;
2792
+ function isDeclaredBinding(line, matchIndex) {
2793
+ let runStart = matchIndex;
2794
+ while (runStart > 0 && /[A-Za-z0-9_$]/.test(line[runStart - 1])) runStart -= 1;
2795
+ const from = Math.max(0, runStart - DECLARATION_WINDOW);
2796
+ return DECLARATION_KEYWORDS.test(line.slice(from, runStart));
2797
+ }
2357
2798
  function applySpans(line, spans) {
2358
2799
  const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
2359
2800
  const merged = [];
@@ -2380,45 +2821,58 @@ function applySpans(line, spans) {
2380
2821
  function isDiscoveryMode(value) {
2381
2822
  return value === "auto" || value === "git" || value === "walk";
2382
2823
  }
2383
- var activeDiscovery = "auto";
2384
- function setActiveDiscovery(mode) {
2385
- activeDiscovery = mode;
2386
- }
2387
2824
  var PRESET_IGNORES = new Set(
2388
2825
  PRESETS.flatMap((preset) => preset.exclude ?? [])
2389
2826
  );
2390
2827
  function readLinkTarget(linkPath, fallback) {
2391
2828
  try {
2392
- return fs12__namespace.readlinkSync(linkPath);
2829
+ return fs14__namespace.readlinkSync(linkPath);
2393
2830
  } catch {
2394
2831
  return fallback;
2395
2832
  }
2396
2833
  }
2397
2834
  function isInside(root, candidate) {
2398
- const relative7 = path13__namespace.relative(root, candidate);
2399
- return relative7 === "" || !relative7.startsWith(`..${path13__namespace.sep}`) && relative7 !== ".." && !path13__namespace.isAbsolute(relative7);
2835
+ const relative8 = path16__namespace.relative(root, candidate);
2836
+ return relative8 === "" || !relative8.startsWith(`..${path16__namespace.sep}`) && relative8 !== ".." && !path16__namespace.isAbsolute(relative8);
2400
2837
  }
2838
+ var GIT_OUTPUT_LIMIT = 68157440;
2401
2839
  function getGitInfo(cwd, fallbackBranch = "main") {
2402
2840
  try {
2403
- const reported = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
2404
- cwd,
2405
- stdio: ["ignore", "pipe", "ignore"],
2406
- encoding: "utf8"
2407
- }).trim();
2408
- const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
2409
- const commit = childProcess__namespace.execSync("git rev-parse HEAD", {
2410
- cwd,
2411
- stdio: ["ignore", "pipe", "ignore"],
2412
- encoding: "utf8"
2413
- }).trim() || "unknown";
2414
- const status = childProcess__namespace.execSync("git status --porcelain", {
2415
- cwd,
2416
- stdio: ["ignore", "pipe", "ignore"],
2417
- encoding: "utf8"
2418
- }).trim();
2419
- return { branch, commit, isDirty: status.length > 0 };
2420
- } catch {
2421
- return { branch: fallbackBranch, commit: "unknown", isDirty: false };
2841
+ const output = childProcess__namespace.execFileSync(
2842
+ "git",
2843
+ ["status", "--porcelain=v2", "--branch", "-z"],
2844
+ {
2845
+ cwd,
2846
+ stdio: ["ignore", "pipe", "ignore"],
2847
+ encoding: "utf8",
2848
+ maxBuffer: GIT_OUTPUT_LIMIT
2849
+ }
2850
+ );
2851
+ let branch = fallbackBranch;
2852
+ let commit = "unknown";
2853
+ let isDirty = false;
2854
+ const records = output.split("\0").flatMap((record) => record.split("\n"));
2855
+ for (const record of records) {
2856
+ if (record.length === 0) continue;
2857
+ if (!record.startsWith("# ")) {
2858
+ isDirty = true;
2859
+ break;
2860
+ }
2861
+ const oid = record.match(/^# branch\.oid (.+)$/);
2862
+ if (oid) {
2863
+ commit = oid[1] === "(initial)" ? "unknown" : oid[1];
2864
+ continue;
2865
+ }
2866
+ const head = record.match(/^# branch\.head (.+)$/);
2867
+ if (head) {
2868
+ branch = head[1] === "(detached)" ? fallbackBranch : head[1];
2869
+ }
2870
+ }
2871
+ return { branch, commit, isDirty };
2872
+ } catch (err) {
2873
+ const code = err.code;
2874
+ const isDirty = code === "ENOBUFS";
2875
+ return { branch: fallbackBranch, commit: "unknown", isDirty };
2422
2876
  }
2423
2877
  }
2424
2878
  var DEFAULT_IGNORED_DIRS = [
@@ -2465,16 +2919,27 @@ function isSensitivePath(relativePath) {
2465
2919
  }
2466
2920
  return matchesSensitiveList(posixPath, base);
2467
2921
  }
2922
+ var GLOBAL_PATTERNS = /* @__PURE__ */ new WeakMap();
2923
+ function globalPattern(pattern) {
2924
+ let compiled = GLOBAL_PATTERNS.get(pattern);
2925
+ if (compiled === void 0) {
2926
+ compiled = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2927
+ GLOBAL_PATTERNS.set(pattern, compiled);
2928
+ }
2929
+ return compiled;
2930
+ }
2931
+ var DEFAULT_PATH_RULES = [...DEFAULT_SECRET_PATTERNS];
2468
2932
  function pathScanRules(extraRules) {
2469
- const fallback = extraRules ?? [];
2470
- return [...DEFAULT_SECRET_PATTERNS, ...fallback];
2933
+ if (extraRules === void 0 || extraRules.length === 0) return DEFAULT_PATH_RULES;
2934
+ return [...DEFAULT_SECRET_PATTERNS, ...extraRules];
2471
2935
  }
2472
2936
  function findSecretInPath(relativePath, extraRules) {
2473
2937
  const posixPath = relativePath.replace(/\\/g, "/");
2474
2938
  const spans = [];
2475
2939
  const rules = [];
2476
2940
  for (const { rule, pattern } of pathScanRules(extraRules)) {
2477
- const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2941
+ const scan = globalPattern(pattern);
2942
+ scan.lastIndex = 0;
2478
2943
  let match;
2479
2944
  while ((match = scan.exec(posixPath)) !== null) {
2480
2945
  if (match[0].length === 0) {
@@ -2601,10 +3066,10 @@ function trimUnescapedTrailingSpaces(line) {
2601
3066
  return line.slice(0, end);
2602
3067
  }
2603
3068
  function readGitignoreRules(dir) {
2604
- const gitignorePath = path13__namespace.join(dir, ".gitignore");
3069
+ const gitignorePath = path16__namespace.join(dir, ".gitignore");
2605
3070
  let raw;
2606
3071
  try {
2607
- raw = fs12__namespace.readFileSync(gitignorePath, "utf8");
3072
+ raw = fs14__namespace.readFileSync(gitignorePath, "utf8");
2608
3073
  } catch {
2609
3074
  return [];
2610
3075
  }
@@ -2630,7 +3095,7 @@ function readGitignoreRules(dir) {
2630
3095
  function escapeRegExpChar(character) {
2631
3096
  return /[\\^$.*+?()[\]{}|/]/.test(character) ? `\\${character}` : character;
2632
3097
  }
2633
- function matchEscapedGitignorePattern(target, pattern) {
3098
+ function matchGitignorePattern(target, pattern) {
2634
3099
  const anchored = pattern.startsWith("/");
2635
3100
  const body = anchored ? pattern.slice(1) : pattern;
2636
3101
  let source = "";
@@ -2664,15 +3129,33 @@ function matchEscapedGitignorePattern(target, pattern) {
2664
3129
  source += "[^/]";
2665
3130
  continue;
2666
3131
  }
3132
+ if (character === "[") {
3133
+ const close = body.indexOf("]", i + 1);
3134
+ if (close !== -1 && close > i + 1) {
3135
+ const inner = body.slice(i + 1, close);
3136
+ const negated = inner.startsWith("!") || inner.startsWith("^");
3137
+ const chars = negated ? inner.slice(1) : inner;
3138
+ const safe = chars.replace(/[\\^\]]/g, (value) => `\\${value}`);
3139
+ source += negated ? `[^/${safe}]` : `[${safe}]`;
3140
+ i = close;
3141
+ continue;
3142
+ }
3143
+ }
2667
3144
  source += escapeRegExpChar(character);
2668
3145
  }
2669
3146
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
2670
3147
  return new RegExp(`${prefix}${source}(?:/.*)?$`).test(target);
2671
3148
  }
2672
- function matchesGitignorePattern(target, pattern) {
2673
- return pattern.includes("\\") ? matchEscapedGitignorePattern(target, pattern) : matchesExclude(target, pattern);
3149
+ function matchesGitignoreRule(rule, scoped, isDirectory) {
3150
+ if (!rule.dirOnly) return matchGitignorePattern(scoped, rule.pattern);
3151
+ const segments = scoped.split("/");
3152
+ const limit = isDirectory ? segments.length : segments.length - 1;
3153
+ for (let depth = 1; depth <= limit; depth += 1) {
3154
+ if (matchGitignorePattern(segments.slice(0, depth).join("/"), rule.pattern)) return true;
3155
+ }
3156
+ return false;
2674
3157
  }
2675
- function gitignoreVerdict(layers, relativePath) {
3158
+ function gitignoreVerdict(layers, relativePath, isDirectory) {
2676
3159
  let verdict = null;
2677
3160
  for (const layer2 of layers) {
2678
3161
  let scoped;
@@ -2685,8 +3168,7 @@ function gitignoreVerdict(layers, relativePath) {
2685
3168
  }
2686
3169
  let layerVerdict = null;
2687
3170
  for (const rule of layer2.rules) {
2688
- const target = rule.dirOnly ? scoped.replace(/\/.*$/, "") : scoped;
2689
- if (matchesGitignorePattern(target, rule.pattern)) layerVerdict = !rule.negated;
3171
+ if (matchesGitignoreRule(rule, scoped, isDirectory)) layerVerdict = !rule.negated;
2690
3172
  }
2691
3173
  if (layerVerdict !== null) verdict = layerVerdict;
2692
3174
  }
@@ -2711,13 +3193,14 @@ function defaultIgnoreReason(relativePath) {
2711
3193
  }
2712
3194
  function listIgnoredPaths(cwd) {
2713
3195
  try {
2714
- const stdout = childProcess__namespace.execSync(
2715
- "git ls-files -z --others --ignored --exclude-standard --directory",
3196
+ const stdout = childProcess__namespace.execFileSync(
3197
+ "git",
3198
+ ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"],
2716
3199
  {
2717
3200
  cwd,
2718
3201
  stdio: ["ignore", "pipe", "ignore"],
2719
3202
  encoding: "utf8",
2720
- maxBuffer: 32 * 1024 * 1024
3203
+ maxBuffer: GIT_OUTPUT_LIMIT
2721
3204
  }
2722
3205
  );
2723
3206
  return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
@@ -2725,13 +3208,9 @@ function listIgnoredPaths(cwd) {
2725
3208
  return [];
2726
3209
  }
2727
3210
  }
2728
- var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
2729
- function isToolOwnedDirName(name) {
2730
- return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
2731
- }
2732
3211
  function hasSnapshotMarker(target) {
2733
3212
  try {
2734
- return fs12__namespace.statSync(target).isDirectory() && fs12__namespace.existsSync(path13__namespace.join(target, ".codebase-snapshot"));
3213
+ return fs14__namespace.statSync(target).isDirectory() && fs14__namespace.existsSync(path16__namespace.join(target, ".codebase-snapshot"));
2735
3214
  } catch {
2736
3215
  return false;
2737
3216
  }
@@ -2745,7 +3224,7 @@ function makeToolOwnedDecider(cwd, toolOwnedPaths) {
2745
3224
  const prefix = parts.slice(0, depth).join("/");
2746
3225
  let hasMarker = markers.get(prefix);
2747
3226
  if (hasMarker === void 0) {
2748
- hasMarker = fs12__namespace.existsSync(path13__namespace.join(cwd, prefix, ".codebase-snapshot"));
3227
+ hasMarker = fs14__namespace.existsSync(path16__namespace.join(cwd, prefix, ".codebase-snapshot"));
2749
3228
  markers.set(prefix, hasMarker);
2750
3229
  }
2751
3230
  if (hasMarker) return true;
@@ -2760,7 +3239,7 @@ function makeLinkProbe() {
2760
3239
  let known = cache.get(absolutePath);
2761
3240
  if (known === void 0) {
2762
3241
  try {
2763
- known = fs12__namespace.lstatSync(absolutePath).isSymbolicLink();
3242
+ known = fs14__namespace.lstatSync(absolutePath).isSymbolicLink();
2764
3243
  } catch {
2765
3244
  known = false;
2766
3245
  }
@@ -2773,7 +3252,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2773
3252
  const isLink = makeLinkProbe();
2774
3253
  let rootReal = cwd;
2775
3254
  try {
2776
- rootReal = fs12__namespace.realpathSync(cwd);
3255
+ rootReal = fs14__namespace.realpathSync(cwd);
2777
3256
  } catch {
2778
3257
  }
2779
3258
  const reported = /* @__PURE__ */ new Set();
@@ -2783,7 +3262,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2783
3262
  let linkedDir = null;
2784
3263
  for (let depth = 1; depth <= parts.length; depth += 1) {
2785
3264
  const prefix = parts.slice(0, depth).join("/");
2786
- if (isLink(path13__namespace.join(cwd, prefix))) {
3265
+ if (isLink(path16__namespace.join(cwd, prefix))) {
2787
3266
  linkedDir = prefix;
2788
3267
  break;
2789
3268
  }
@@ -2794,10 +3273,10 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2794
3273
  }
2795
3274
  if (reported.has(linkedDir)) continue;
2796
3275
  reported.add(linkedDir);
2797
- const absolute = path13__namespace.join(cwd, linkedDir);
3276
+ const absolute = path16__namespace.join(cwd, linkedDir);
2798
3277
  let targetReal = absolute;
2799
3278
  try {
2800
- targetReal = fs12__namespace.realpathSync(absolute);
3279
+ targetReal = fs14__namespace.realpathSync(absolute);
2801
3280
  } catch {
2802
3281
  }
2803
3282
  const target = readLinkTarget(absolute, targetReal);
@@ -2817,13 +3296,14 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2817
3296
  return kept;
2818
3297
  }
2819
3298
  function listGitCandidates(cwd, skipped, isToolOwned) {
2820
- const stdout = childProcess__namespace.execSync(
2821
- "git ls-files -z --cached --others --exclude-standard",
3299
+ const stdout = childProcess__namespace.execFileSync(
3300
+ "git",
3301
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
2822
3302
  {
2823
3303
  cwd,
2824
3304
  stdio: ["ignore", "pipe", "ignore"],
2825
3305
  encoding: "utf8",
2826
- maxBuffer: 50 * 1024 * 1024
3306
+ maxBuffer: GIT_OUTPUT_LIMIT
2827
3307
  }
2828
3308
  );
2829
3309
  let candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
@@ -2860,7 +3340,7 @@ function collectFiles(cwd, options = {}) {
2860
3340
  const isToolOwned = makeToolOwnedDecider(cwd, [...toolOwnedPaths]);
2861
3341
  let candidates;
2862
3342
  const skipped = [];
2863
- const discovery = options.discovery ?? activeDiscovery;
3343
+ const discovery = options.discovery ?? "auto";
2864
3344
  if (discovery === "walk") {
2865
3345
  const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), toolOwnedPaths);
2866
3346
  candidates = walked.files;
@@ -2874,7 +3354,7 @@ function collectFiles(cwd, options = {}) {
2874
3354
  "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."
2875
3355
  );
2876
3356
  }
2877
- if (fs12__namespace.existsSync(path13__namespace.join(cwd, ".git"))) {
3357
+ if (fs14__namespace.existsSync(path16__namespace.join(cwd, ".git"))) {
2878
3358
  throw new Error(
2879
3359
  "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."
2880
3360
  );
@@ -2933,7 +3413,7 @@ function collectFiles(cwd, options = {}) {
2933
3413
  }
2934
3414
  function rootRealFor(root) {
2935
3415
  try {
2936
- return fs12__namespace.realpathSync(root);
3416
+ return fs14__namespace.realpathSync(root);
2937
3417
  } catch {
2938
3418
  return root;
2939
3419
  }
@@ -2941,18 +3421,18 @@ function rootRealFor(root) {
2941
3421
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
2942
3422
  const results = { files: [], skipped: [] };
2943
3423
  const stack = [];
2944
- const relativeToBase = (target) => path13__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
3424
+ const relativeToBase = (target) => path16__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
2945
3425
  const enter = (current, layers) => {
2946
3426
  if (visited.has(current)) return false;
2947
3427
  visited.add(current);
2948
3428
  const localRules = readGitignoreRules(current);
2949
3429
  const gitignoreLayers = localRules.length === 0 ? layers : [
2950
3430
  ...layers,
2951
- { prefix: path13__namespace.relative(baseDir, current).replace(/\\/g, "/"), rules: localRules }
3431
+ { prefix: path16__namespace.relative(baseDir, current).replace(/\\/g, "/"), rules: localRules }
2952
3432
  ];
2953
3433
  let entries;
2954
3434
  try {
2955
- entries = fs12__namespace.readdirSync(current, { withFileTypes: true });
3435
+ entries = fs14__namespace.readdirSync(current, { withFileTypes: true });
2956
3436
  } catch {
2957
3437
  results.skipped.push({
2958
3438
  path: relativeToBase(current),
@@ -2974,10 +3454,10 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2974
3454
  }
2975
3455
  const entry = frame.entries[frame.index];
2976
3456
  frame.index += 1;
2977
- const fullPath = path13__namespace.join(frame.dir, entry.name);
2978
- const relPath = path13__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
2979
- if (isToolOwnedDirName(entry.name) && reservedPaths.has(relPath)) continue;
2980
- if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
3457
+ const fullPath = path16__namespace.join(frame.dir, entry.name);
3458
+ const relPath = path16__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
3459
+ if (reservedPaths.has(relPath)) continue;
3460
+ if (entry.isDirectory() && DEFAULT_IGNORED_DIRS.includes(entry.name)) {
2981
3461
  results.skipped.push({
2982
3462
  path: relPath,
2983
3463
  reason: "default-ignore",
@@ -2993,7 +3473,7 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2993
3473
  });
2994
3474
  continue;
2995
3475
  }
2996
- if (gitignoreVerdict(frame.gitignoreLayers, relPath) === true) {
3476
+ if (gitignoreVerdict(frame.gitignoreLayers, relPath, entry.isDirectory()) === true) {
2997
3477
  if (!hasSnapshotMarker(fullPath)) {
2998
3478
  results.skipped.push({
2999
3479
  path: relPath,
@@ -3008,14 +3488,14 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3008
3488
  if (entry.isSymbolicLink()) {
3009
3489
  let targetReal;
3010
3490
  try {
3011
- targetReal = fs12__namespace.realpathSync(fullPath);
3491
+ targetReal = fs14__namespace.realpathSync(fullPath);
3012
3492
  } catch {
3013
3493
  results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
3014
3494
  continue;
3015
3495
  }
3016
3496
  let stats;
3017
3497
  try {
3018
- stats = fs12__namespace.statSync(fullPath);
3498
+ stats = fs14__namespace.statSync(fullPath);
3019
3499
  } catch {
3020
3500
  results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
3021
3501
  continue;
@@ -3057,6 +3537,28 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3057
3537
  return results;
3058
3538
  }
3059
3539
 
3540
+ // src/tokenize.ts
3541
+ var WORD_RUN = /[A-Za-z0-9_]+/g;
3542
+ var SINGLE_TOKEN = /[^\sA-Za-z0-9_]/g;
3543
+ var LINE_BREAK_RUN = /\n+/g;
3544
+ var CHARS_PER_WORD_TOKEN = 6;
3545
+ function estimateTokens(text2) {
3546
+ if (text2.length === 0) return 0;
3547
+ let total = 0;
3548
+ for (const match of text2.match(WORD_RUN) ?? []) {
3549
+ total += Math.max(1, Math.round(match.length / CHARS_PER_WORD_TOKEN));
3550
+ }
3551
+ total += text2.match(SINGLE_TOKEN)?.length ?? 0;
3552
+ total += text2.match(LINE_BREAK_RUN)?.length ?? 0;
3553
+ return total;
3554
+ }
3555
+ function bytesForTokens(tokens) {
3556
+ return Math.max(0, Math.floor(tokens * 4));
3557
+ }
3558
+ function formatTokens(count) {
3559
+ return count.toLocaleString("en-US");
3560
+ }
3561
+
3060
3562
  // src/config.ts
3061
3563
  var CONFIG_DIR_SYMBOL = /* @__PURE__ */ Symbol.for("codebase-snapshot.configDir");
3062
3564
  var CONFIG_FILENAMES = [
@@ -3096,9 +3598,23 @@ function isResolvedConfig(value) {
3096
3598
  );
3097
3599
  return hasResolvedNumbers && hasResolvedStrings && hasResolvedFlags && hasResolvedRules && hasCompiledRules;
3098
3600
  }
3601
+ function normalizeBudgets(budgets, maxPartBytes) {
3602
+ const clearer = typeof maxPartBytes === "number" && Number.isFinite(maxPartBytes) && maxPartBytes > 0 ? maxPartBytes : void 0;
3603
+ if (budgets === void 0 || typeof budgets !== "object" || budgets === null) {
3604
+ return { default: clearer ?? 0, byLayer: {} };
3605
+ }
3606
+ const fallbackDefault = clearer ?? budgets.default;
3607
+ const byLayer = {};
3608
+ const layers = budgets.byLayer;
3609
+ if (layers !== void 0 && typeof layers === "object" && layers !== null) {
3610
+ for (const [layer2, value] of Object.entries(layers)) {
3611
+ byLayer[layer2] = Math.min(value, fallbackDefault);
3612
+ }
3613
+ }
3614
+ return { default: fallbackDefault, byLayer };
3615
+ }
3099
3616
  function normalizeResolvedConfig(config) {
3100
3617
  const discovery = isDiscoveryMode(config.discovery) ? config.discovery : "auto";
3101
- setActiveDiscovery(discovery);
3102
3618
  return markResolvedConfig({
3103
3619
  ...config,
3104
3620
  discovery,
@@ -3117,7 +3633,7 @@ function normalizeResolvedConfig(config) {
3117
3633
  ])
3118
3634
  ],
3119
3635
  layersMode: config.layersMode === "replace" ? "replace" : "merge",
3120
- budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
3636
+ budgets: normalizeBudgets(config.budgets, config.maxPartBytes),
3121
3637
  hooks: resolveHooks(config.hooks),
3122
3638
  excludePatterns: Array.isArray(config.excludePatterns) ? config.excludePatterns : [],
3123
3639
  layers: Array.isArray(config.layers) ? config.layers : [],
@@ -3239,6 +3755,7 @@ var CONFIG_KEYS = /* @__PURE__ */ new Set([
3239
3755
  "artifacts",
3240
3756
  "branch",
3241
3757
  "budgets",
3758
+ "changedSince",
3242
3759
  "cwd",
3243
3760
  "discovery",
3244
3761
  "exclude",
@@ -3253,12 +3770,18 @@ var CONFIG_KEYS = /* @__PURE__ */ new Set([
3253
3770
  "layersMode",
3254
3771
  "maxMonolithBytes",
3255
3772
  "maxPartBytes",
3773
+ "maxPartTokens",
3256
3774
  "outDir",
3257
3775
  "preset",
3258
3776
  "previewHeadLines",
3777
+ "staged",
3259
3778
  "previewTailLines",
3779
+ "secretRules",
3260
3780
  "secrets",
3261
- "tmpDir"
3781
+ "sarif",
3782
+ "skeleton",
3783
+ "tmpDir",
3784
+ "withDependents"
3262
3785
  ]);
3263
3786
  function assertKnownConfigKeys(value, file) {
3264
3787
  const unknownKeys = Object.keys(value).filter((key) => !CONFIG_KEYS.has(key));
@@ -3313,11 +3836,11 @@ async function loadConfigFileDetailed(cwd) {
3313
3836
  const warnings = [];
3314
3837
  const errors = [];
3315
3838
  for (const filename of CONFIG_FILENAMES) {
3316
- const fullPath = path13__namespace.join(cwd, filename);
3317
- if (!fs12__namespace.existsSync(fullPath)) continue;
3839
+ const fullPath = path16__namespace.join(cwd, filename);
3840
+ if (!fs14__namespace.existsSync(fullPath)) continue;
3318
3841
  if (filename.endsWith(".json") || filename === ".snapshotrc") {
3319
3842
  try {
3320
- const config = parseJsonish(fs12__namespace.readFileSync(fullPath, "utf8"), fullPath);
3843
+ const config = parseJsonish(fs14__namespace.readFileSync(fullPath, "utf8"), fullPath);
3321
3844
  if (config === null || typeof config !== "object" || Array.isArray(config)) {
3322
3845
  throw new Error(
3323
3846
  `${fullPath} does not hold a configuration object (received ${describeValue2(config)}).`
@@ -3351,12 +3874,16 @@ async function loadConfigFileDetailed(cwd) {
3351
3874
  continue;
3352
3875
  }
3353
3876
  }
3354
- const pkgJsonPath = path13__namespace.join(cwd, "package.json");
3355
- if (fs12__namespace.existsSync(pkgJsonPath)) {
3877
+ const pkgJsonPath = path16__namespace.join(cwd, "package.json");
3878
+ if (fs14__namespace.existsSync(pkgJsonPath)) {
3356
3879
  try {
3357
- const parsed = parseJsonish(fs12__namespace.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
3880
+ const parsed = parseJsonish(fs14__namespace.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
3358
3881
  const snapshotKey = parsed.snapshot;
3359
3882
  if (snapshotKey && typeof snapshotKey === "object" && !Array.isArray(snapshotKey)) {
3883
+ assertKnownConfigKeys(
3884
+ snapshotKey,
3885
+ `${pkgJsonPath} (the "snapshot" key)`
3886
+ );
3360
3887
  return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
3361
3888
  }
3362
3889
  if (snapshotKey !== void 0) {
@@ -3377,7 +3904,7 @@ async function loadConfigFile(cwd) {
3377
3904
  const loaded = await loadConfigFileDetailed(cwd);
3378
3905
  if (loaded.config && loaded.file && typeof loaded.config === "object") {
3379
3906
  Object.defineProperty(loaded.config, CONFIG_DIR_SYMBOL, {
3380
- value: path13__namespace.dirname(loaded.file),
3907
+ value: path16__namespace.dirname(loaded.file),
3381
3908
  enumerable: false,
3382
3909
  configurable: true
3383
3910
  });
@@ -3388,6 +3915,7 @@ async function loadConfigFile(cwd) {
3388
3915
  return loaded.config;
3389
3916
  }
3390
3917
  var MIN_MAX_PART_BYTES = 4096;
3918
+ var MIN_MAX_PART_TOKENS = MIN_MAX_PART_BYTES / 4;
3391
3919
  var ARTIFACT_NAMES = [
3392
3920
  "readme",
3393
3921
  "manifest",
@@ -3536,12 +4064,12 @@ function resolveKindRules(value) {
3536
4064
  function outputExcludePatterns(cwd, outDir, tmpDir) {
3537
4065
  const patterns = [];
3538
4066
  for (const dir of [outDir, tmpDir]) {
3539
- const absolute = path13__namespace.resolve(cwd, dir);
3540
- const relative7 = path13__namespace.relative(cwd, absolute);
3541
- if (!relative7 || relative7 === ".." || relative7.startsWith(`..${path13__namespace.sep}`) || path13__namespace.isAbsolute(relative7)) {
4067
+ const absolute = path16__namespace.resolve(cwd, dir);
4068
+ const relative8 = path16__namespace.relative(cwd, absolute);
4069
+ if (!relative8 || relative8 === ".." || relative8.startsWith(`..${path16__namespace.sep}`) || path16__namespace.isAbsolute(relative8)) {
3542
4070
  continue;
3543
4071
  }
3544
- patterns.push(`${relative7.replace(/\\/g, "/")}/**`);
4072
+ patterns.push(`${relative8.replace(/\\/g, "/")}/**`);
3545
4073
  }
3546
4074
  return patterns;
3547
4075
  }
@@ -3588,6 +4116,20 @@ function boundedBytes(value, fallback, minimum, label) {
3588
4116
  }
3589
4117
  return parsed;
3590
4118
  }
4119
+ function boundedTokens(value, label) {
4120
+ const parsed = parseByteSize(value);
4121
+ if (parsed === void 0 || !Number.isInteger(parsed)) {
4122
+ throw new Error(
4123
+ `${label} must be a whole token count such as 30000 or "30k" (received ${describeBytes(value)}).`
4124
+ );
4125
+ }
4126
+ if (parsed < MIN_MAX_PART_TOKENS) {
4127
+ throw new Error(
4128
+ `${label} must be at least ${MIN_MAX_PART_TOKENS} tokens (${MIN_MAX_PART_BYTES} bytes is the smallest part budget); received ${describeBytes(value)}.`
4129
+ );
4130
+ }
4131
+ return parsed;
4132
+ }
3591
4133
  function optionalBytes(value, label) {
3592
4134
  if (value === void 0 || value === null) return null;
3593
4135
  const parsed = parseByteSize(value);
@@ -3657,11 +4199,11 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
3657
4199
  );
3658
4200
  }
3659
4201
  const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
3660
- const resolved = path13__namespace.resolve(candidate);
3661
- if (!fs12__namespace.existsSync(resolved)) {
4202
+ const resolved = path16__namespace.resolve(candidate);
4203
+ if (!fs14__namespace.existsSync(resolved)) {
3662
4204
  throw new Error(`Working directory does not exist: ${resolved}`);
3663
4205
  }
3664
- if (!fs12__namespace.statSync(resolved).isDirectory()) {
4206
+ if (!fs14__namespace.statSync(resolved).isDirectory()) {
3665
4207
  throw new Error(`Working directory is not a directory: ${resolved}`);
3666
4208
  }
3667
4209
  return resolved;
@@ -3676,7 +4218,6 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3676
4218
  );
3677
4219
  }
3678
4220
  const discovery = requestedDiscovery ?? "auto";
3679
- setActiveDiscovery(discovery);
3680
4221
  const forcedPreset = resolvePresetName(userConfig?.preset);
3681
4222
  if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
3682
4223
  throw new Error(unknownPresetMessage(forcedPreset));
@@ -3730,13 +4271,53 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3730
4271
  MIN_MAX_PART_BYTES,
3731
4272
  "maxPartBytes"
3732
4273
  );
4274
+ const rawMaxPartTokens = cliOptions?.maxPartTokens ?? userConfig?.maxPartTokens;
4275
+ const requestedMaxPartTokens = rawMaxPartTokens === null ? void 0 : rawMaxPartTokens;
4276
+ const maxPartTokens = requestedMaxPartTokens === void 0 ? void 0 : boundedTokens(requestedMaxPartTokens, "maxPartTokens");
4277
+ const effectiveMaxPartBytes = maxPartTokens === void 0 ? maxPartBytes : Math.min(maxPartBytes, bytesForTokens(maxPartTokens));
3733
4278
  const budgets = resolveBudgets(
3734
4279
  userConfig?.budgets,
3735
- maxPartBytes,
3736
- explicitMaxPartBytes !== void 0
4280
+ effectiveMaxPartBytes,
4281
+ explicitMaxPartBytes !== void 0 || maxPartTokens !== void 0
3737
4282
  );
4283
+ const resolvedMaxPartBytes = explicitMaxPartBytes !== void 0 || maxPartTokens !== void 0 ? effectiveMaxPartBytes : budgets.default;
3738
4284
  const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
3739
4285
  const outDir = cliOptions?.outDir || configuredOutDir;
4286
+ const rawChangedSinceSetting = cliOptions?.changedSince ?? userConfig?.changedSince;
4287
+ const rawChangedSince = rawChangedSinceSetting === null ? void 0 : rawChangedSinceSetting;
4288
+ if (rawChangedSince !== void 0 && (typeof rawChangedSince !== "string" || rawChangedSince.trim().length === 0)) {
4289
+ throw new Error(
4290
+ `\`changedSince\` must be a git ref such as "main" or "HEAD~3" (received ${describeValue2(rawChangedSince)}).`
4291
+ );
4292
+ }
4293
+ const changedSince = rawChangedSince === void 0 ? void 0 : String(rawChangedSince).trim();
4294
+ const rawStaged = cliOptions?.staged ?? userConfig?.staged;
4295
+ if (rawStaged !== void 0 && typeof rawStaged !== "boolean") {
4296
+ throw new Error(`\`staged\` must be a boolean (received ${describeValue2(rawStaged)}).`);
4297
+ }
4298
+ const staged = rawStaged === true;
4299
+ const rawWithDependents = cliOptions?.withDependents ?? userConfig?.withDependents;
4300
+ if (rawWithDependents !== void 0 && typeof rawWithDependents !== "boolean") {
4301
+ throw new Error(
4302
+ `\`withDependents\` must be a boolean (received ${describeValue2(rawWithDependents)}).`
4303
+ );
4304
+ }
4305
+ const withDependents = rawWithDependents === true;
4306
+ const rawSkeleton = cliOptions?.skeleton ?? userConfig?.skeleton;
4307
+ if (rawSkeleton !== void 0 && typeof rawSkeleton !== "boolean") {
4308
+ throw new Error(`\`skeleton\` must be a boolean (received ${describeValue2(rawSkeleton)}).`);
4309
+ }
4310
+ const skeleton = rawSkeleton === true;
4311
+ const rawSarif = cliOptions?.sarif ?? userConfig?.sarif;
4312
+ if (rawSarif !== void 0 && typeof rawSarif !== "boolean") {
4313
+ throw new Error(`\`sarif\` must be a boolean (received ${describeValue2(rawSarif)}).`);
4314
+ }
4315
+ const sarif = rawSarif === true;
4316
+ if (withDependents && changedSince === void 0 && !staged) {
4317
+ throw new Error(
4318
+ "`withDependents` widens a changed-files scope, so it needs `changedSince` or `staged` (with neither, every file is already included)."
4319
+ );
4320
+ }
3740
4321
  const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
3741
4322
  const branch = strictString(userConfig?.branch, "main", "`branch`");
3742
4323
  const configuredSensitiveFiles = strictBoolean(
@@ -3757,7 +4338,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3757
4338
  branch,
3758
4339
  outDir,
3759
4340
  tmpDir,
3760
- maxPartBytes,
4341
+ maxPartBytes: resolvedMaxPartBytes,
4342
+ ...maxPartTokens === void 0 ? {} : { maxPartTokens },
3761
4343
  largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
3762
4344
  previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
3763
4345
  previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
@@ -3767,6 +4349,11 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3767
4349
  "maxMonolithBytes"
3768
4350
  ),
3769
4351
  includeSensitiveFiles,
4352
+ ...changedSince === void 0 ? {} : { changedSince },
4353
+ ...staged ? { staged } : {},
4354
+ ...withDependents ? { withDependents } : {},
4355
+ ...skeleton ? { skeleton } : {},
4356
+ ...sarif ? { sarif } : {},
3770
4357
  include: resolveInclude(userConfig?.include),
3771
4358
  preset: preset.presetName,
3772
4359
  presetExclude,
@@ -3800,14 +4387,11 @@ function matchRule(filePath, matcher) {
3800
4387
  if (pattern.endsWith("/**")) {
3801
4388
  const prefix = pattern.slice(0, -3).replace(/\/+$/, "");
3802
4389
  if (!prefix) return true;
3803
- if (!/[*?]/.test(prefix)) {
4390
+ if (!/[*?{[]/.test(prefix)) {
3804
4391
  return filePath === prefix || filePath.startsWith(`${prefix}/`);
3805
4392
  }
3806
4393
  }
3807
- if (pattern.includes("*") || pattern.includes("?")) {
3808
- return globToRegExp(pattern).test(filePath);
3809
- }
3810
- return filePath.includes(pattern);
4394
+ return matchesExclude(filePath, pattern);
3811
4395
  }
3812
4396
  return false;
3813
4397
  }
@@ -3826,6 +4410,21 @@ function classifyKind(filePath, kinds) {
3826
4410
  function configDigest(config) {
3827
4411
  const material = {
3828
4412
  maxPartBytes: config.maxPartBytes,
4413
+ // The user's own unit. `maxPartBytes` above already reflects the converted ceiling, but
4414
+ // recording the token number means changing 32k to 64k moves the digest even in the
4415
+ // corner where both round to the same byte budget.
4416
+ maxPartTokens: config.maxPartTokens ?? null,
4417
+ // A changed-files scope decides which files are in the snapshot at all, so a snapshot
4418
+ // taken over a different scope is a different snapshot.
4419
+ changedSince: config.changedSince ?? null,
4420
+ staged: config.staged === true,
4421
+ withDependents: config.withDependents === true,
4422
+ // Signature compression changes the emitted bytes of every TypeScript file it touches.
4423
+ skeleton: config.skeleton === true,
4424
+ // `sarif` is not one of `artifacts`: it writes `reports/audit.sarif` beside the audit, and
4425
+ // leaving it out of the digest meant a snapshot taken without it and one taken with it
4426
+ // compared equal even though their directories differ.
4427
+ sarif: config.sarif === true,
3829
4428
  largeFileBytes: config.largeFileBytes,
3830
4429
  previewHeadLines: config.previewHeadLines,
3831
4430
  previewTailLines: config.previewTailLines,
@@ -3838,11 +4437,21 @@ function configDigest(config) {
3838
4437
  preset: config.preset,
3839
4438
  layersMode: config.layersMode,
3840
4439
  budgets: config.budgets,
3841
- layers: config.layers.map((layer2) => layer2.name),
3842
- kinds: config.kinds.map((kind) => kind.kind),
3843
- secretRules: config.secretRules.map((rule) => `${rule.rule}:${rule.pattern.source}`).sort()
4440
+ // The matcher is what makes a layer a layer. Hashing only the names meant that
4441
+ // moving `{ name: "api", match: "src/api/**" }` to `"src/v2/**"` produced the same
4442
+ // digest, so `validate` reported "valid and up to date" for a snapshot whose files
4443
+ // had all been reclassified — and per-layer budgets applied to different files.
4444
+ layers: config.layers.map((layer2) => ({ name: layer2.name, match: String(layer2.match) })),
4445
+ kinds: config.kinds.map((kind) => ({ kind: kind.kind, match: String(kind.match) })),
4446
+ // How files are enumerated decides what is in the snapshot at all.
4447
+ discovery: config.discovery ?? "auto",
4448
+ // `flags` and `valueGroup` change *what is masked*, and a rule's source alone does
4449
+ // not imply either: the same source with and without `i` redacts different lines.
4450
+ secretRules: config.secretRules.map(
4451
+ (rule) => `${rule.rule}:${rule.pattern.source}/${rule.pattern.flags}:${rule.valueGroup ?? ""}`
4452
+ ).sort()
3844
4453
  };
3845
- return crypto.createHash("sha256").update(JSON.stringify(material)).digest("hex");
4454
+ return crypto2.createHash("sha256").update(JSON.stringify(material)).digest("hex");
3846
4455
  }
3847
4456
 
3848
4457
  // src/define-config.ts
@@ -4020,6 +4629,95 @@ function generateAuditReport(manifest, secretFindings, secretRules) {
4020
4629
  }
4021
4630
  return lines.join("\n");
4022
4631
  }
4632
+ var SARIF_VERSION = "2.1.0";
4633
+ var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
4634
+ var BUILTIN_RULE_IDS = new Map(
4635
+ [
4636
+ ...DEFAULT_SECRET_PATTERNS.map((rule) => rule.rule),
4637
+ // Produced by the whole-block pass in `redact.ts`, not by a pattern in the catalogue.
4638
+ "private-key-block"
4639
+ ].map((name, index) => [name, `CS${String(index + 1).padStart(3, "0")}`])
4640
+ );
4641
+ function ruleIdFor(name) {
4642
+ const builtin = BUILTIN_RULE_IDS.get(name);
4643
+ if (builtin !== void 0) return builtin;
4644
+ const digest = crypto2__namespace.createHash("sha256").update(name).digest("hex").slice(0, 8);
4645
+ return `CS${digest.toUpperCase()}`;
4646
+ }
4647
+ function generateSarifReport(manifest, findings, rules, toolVersion) {
4648
+ const ruleNames = Array.from(
4649
+ /* @__PURE__ */ new Set([...rules.map((rule) => rule.rule), ...findings.flatMap((finding) => finding.rules)])
4650
+ ).sort();
4651
+ const ruleIndex = /* @__PURE__ */ new Map();
4652
+ const sarifRules = ruleNames.map((name, index) => {
4653
+ ruleIndex.set(name, index);
4654
+ const id = ruleIdFor(name);
4655
+ return {
4656
+ id,
4657
+ name,
4658
+ shortDescription: { text: `Credential-shaped value masked by the "${name}" rule` },
4659
+ defaultConfiguration: { level: "note" },
4660
+ help: {
4661
+ 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."
4662
+ }
4663
+ };
4664
+ });
4665
+ const results = findings.filter((finding) => typeof finding.path === "string" && finding.path.length > 0).map((finding) => {
4666
+ const primary = finding.rules[0] ?? "unknown";
4667
+ const line = Math.max(1, finding.line);
4668
+ const findingPath = String(finding.path).replace(/\\/g, "/");
4669
+ return {
4670
+ ruleId: ruleIdFor(primary),
4671
+ ruleIndex: ruleIndex.get(primary) ?? 0,
4672
+ level: "note",
4673
+ message: {
4674
+ text: `Credential-shaped value masked (rule${finding.rules.length === 1 ? "" : "s"}: ${finding.rules.join(", ")})`
4675
+ },
4676
+ locations: [
4677
+ {
4678
+ physicalLocation: {
4679
+ artifactLocation: { uri: findingPath, uriBaseId: "%SRCROOT%" },
4680
+ region: { startLine: line }
4681
+ }
4682
+ }
4683
+ ],
4684
+ partialFingerprints: {
4685
+ // Stable across commits for the same finding, which is what keeps an alert open
4686
+ // instead of closing and reopening it on every run.
4687
+ codebaseSnapshotFinding: `${findingPath}#L${line}#${finding.rules.join("+")}`
4688
+ }
4689
+ };
4690
+ }).sort(
4691
+ (a, b) => a.locations[0].physicalLocation.artifactLocation.uri.localeCompare(
4692
+ b.locations[0].physicalLocation.artifactLocation.uri
4693
+ ) || a.locations[0].physicalLocation.region.startLine - b.locations[0].physicalLocation.region.startLine
4694
+ );
4695
+ const log = {
4696
+ $schema: SARIF_SCHEMA,
4697
+ version: SARIF_VERSION,
4698
+ runs: [
4699
+ {
4700
+ tool: {
4701
+ driver: {
4702
+ name: "codebase-snapshot",
4703
+ version: toolVersion,
4704
+ informationUri: "https://github.com/ankurhalder/codebase-snapshot",
4705
+ rules: sarifRules
4706
+ }
4707
+ },
4708
+ automationDetails: {
4709
+ // The snapshot's own fingerprint: two uploads of the same snapshot are the same run,
4710
+ // so Code Scanning can tell an unchanged upload from a new one.
4711
+ id: `codebase-snapshot/${manifest.fingerprint}`
4712
+ },
4713
+ originalUriBaseIds: { "%SRCROOT%": { uri: "file:///" } },
4714
+ results
4715
+ }
4716
+ ]
4717
+ };
4718
+ return `${JSON.stringify(log, null, 2)}
4719
+ `;
4720
+ }
4023
4721
 
4024
4722
  // src/partitioner/preview.ts
4025
4723
  function countLines(content) {
@@ -4069,14 +4767,29 @@ var HEADER_RESERVE_BYTES = 2048 + Buffer.byteLength(UNTRUSTED_CONTENT_NOTICE, "u
4069
4767
  var MIN_SECTION_BUDGET_BYTES = 512;
4070
4768
  var MAX_SPLIT_PASSES = 64;
4071
4769
  var BUDGET_MARKER_RESERVE = 96;
4770
+ function insertTokenLine(markdown, tokens) {
4771
+ return markdown.replace(
4772
+ /^(- Files: .*)$/m,
4773
+ `$1
4774
+ - Estimated Tokens: ~${formatTokens(tokens)}`
4775
+ );
4776
+ }
4777
+ function replaceTokenLine(markdown, tokens) {
4778
+ const line = `- Estimated Tokens: ~${formatTokens(tokens)}`;
4779
+ if (/^- Estimated Tokens: .*$/m.test(markdown)) {
4780
+ return markdown.replace(/^- Estimated Tokens: .*$/m, line);
4781
+ }
4782
+ return insertTokenLine(markdown, tokens);
4783
+ }
4072
4784
  var BUDGET_MARKER_SUFFIX = /\n\.\.\. \[\d+ lines omitted: part budget \d+ B\] \.\.\.$/;
4073
- function truncateWithBudgetMarker(content, maxBytes, budget) {
4785
+ var TOKEN_LINE = /^- Estimated Tokens: .*$/m;
4786
+ var TOKEN_LINE_SLACK = 32;
4787
+ function truncateWithBudgetMarker(content, maxBytes, budget, originalLines) {
4074
4788
  const base = content.replace(BUDGET_MARKER_SUFFIX, "");
4075
- const baseLines = countLines(base);
4076
4789
  const truncated = truncateToBytes(base, Math.max(0, maxBytes));
4077
4790
  if (!truncated.isTruncated) return { content: base, isTruncated: false };
4078
4791
  const keptLines = countLines(truncated.content);
4079
- const omitted = Math.max(1, baseLines - keptLines);
4792
+ const omitted = Math.max(1, Math.max(countLines(base), originalLines) - keptLines);
4080
4793
  return {
4081
4794
  content: `${truncated.content}
4082
4795
  ... [${omitted} lines omitted: part budget ${budget} B] ...`,
@@ -4090,7 +4803,7 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
4090
4803
  const prepared = files.map((file) => prepareSection(file, sectionBudget));
4091
4804
  let groups = greedyGroups(prepared, budget);
4092
4805
  for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
4093
- const rendered = renderGroups(groups, contentFingerprint);
4806
+ const rendered = renderGroups(groups, budget, contentFingerprint);
4094
4807
  const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
4095
4808
  if (oversized === -1) return rendered;
4096
4809
  const group = groups[oversized];
@@ -4106,28 +4819,49 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
4106
4819
  0,
4107
4820
  Buffer.byteLength(currentContent, "utf8") - overflow - BUDGET_MARKER_RESERVE
4108
4821
  );
4109
- const shrunk = truncateWithBudgetMarker(currentContent, shrinkTo, budget);
4822
+ const shrunk = truncateWithBudgetMarker(
4823
+ currentContent,
4824
+ shrinkTo,
4825
+ budget,
4826
+ section.file.originalLines
4827
+ );
4110
4828
  if (shrunk.isTruncated) {
4111
4829
  applyTruncatedContent(section.file, shrunk.content);
4112
4830
  section.text = renderFileSection(section.file);
4113
4831
  section.byteSize = Buffer.byteLength(section.text, "utf8");
4114
4832
  continue;
4115
4833
  }
4116
- const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
4117
- const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
4834
+ const marker2 = TRUNCATION_MARKER;
4835
+ assertBudgetHoldsMarker(budget);
4836
+ const target = Math.max(
4837
+ 0,
4838
+ budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
4839
+ );
4118
4840
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
4119
4841
  rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
4120
4842
  return rendered;
4121
4843
  }
4122
- return enforceBudget(renderGroups(groups, contentFingerprint), budget);
4844
+ return enforceBudget(renderGroups(groups, budget, contentFingerprint), budget);
4123
4845
  }
4124
- function enforceBudget(parts, budget) {
4125
- const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
4126
- const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
4127
- return parts.map((part) => {
4128
- if (part.metadata.byteSize <= budget) return part;
4846
+ var TRUNCATION_MARKER = "\n... [part truncated: cannot fit budget] ...\n";
4847
+ function assertBudgetHoldsMarker(budget) {
4848
+ const minimum = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
4849
+ if (budget < minimum) {
4850
+ throw new Error(
4851
+ `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.`
4852
+ );
4853
+ }
4854
+ }
4855
+ function enforceBudget(parts, budget) {
4856
+ const target = Math.max(
4857
+ 0,
4858
+ budget - Buffer.byteLength(TRUNCATION_MARKER, "utf8") - TOKEN_LINE_SLACK
4859
+ );
4860
+ return parts.map((part) => {
4861
+ if (part.metadata.byteSize <= budget) return part;
4862
+ assertBudgetHoldsMarker(budget);
4129
4863
  const clipped = truncateToBytes(part.markdown, target);
4130
- return rehashPart(part, clipped.content + marker2);
4864
+ return rehashPart(part, clipped.content + TRUNCATION_MARKER);
4131
4865
  });
4132
4866
  }
4133
4867
  function renumberParts(parts, contentFingerprint) {
@@ -4136,7 +4870,7 @@ function renumberParts(parts, contentFingerprint) {
4136
4870
  const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
4137
4871
  const nameFor = (position) => `part-${zeroPad(position)}.md`;
4138
4872
  const marker2 = "## Contents\n";
4139
- return parts.map((part, index) => {
4873
+ const renumbered = parts.map((part, index) => {
4140
4874
  const markerIndex = part.markdown.indexOf(marker2);
4141
4875
  const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker2.length);
4142
4876
  const included = part.metadata.includedFiles;
@@ -4172,19 +4906,25 @@ function renumberParts(parts, contentFingerprint) {
4172
4906
  "## Contents",
4173
4907
  ""
4174
4908
  ].join("\n");
4175
- const markdown = (header + body).replace(/\r\n/g, "\n");
4909
+ const assembled = (header + body).replace(/\r\n/g, "\n");
4910
+ const estimatedTokens = estimateTokens(assembled);
4911
+ const markdown = insertTokenLine(assembled, estimatedTokens);
4176
4912
  const metadata = {
4177
4913
  ...part.metadata,
4178
4914
  index: index + 1,
4179
4915
  filename: nameFor(index + 1),
4180
4916
  byteSize: Buffer.byteLength(markdown, "utf8"),
4181
- sha256: crypto__namespace.createHash("sha256").update(markdown).digest("hex"),
4917
+ estimatedTokens,
4918
+ sha256: crypto2__namespace.createHash("sha256").update(markdown).digest("hex"),
4182
4919
  lines: countLines(markdown),
4183
4920
  fileCount: included.length,
4184
4921
  layersCovered: Object.keys(layerCounts).sort()
4185
4922
  };
4186
4923
  return { filename: metadata.filename, markdown, metadata };
4187
4924
  });
4925
+ return renumbered.map(
4926
+ (part) => enforceBudget([part], part.metadata.budgetBytes)[0] ?? part
4927
+ );
4188
4928
  }
4189
4929
  var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
4190
4930
  var METADATA_VALUE_MAX = 200;
@@ -4208,7 +4948,12 @@ function prepareSection(file, sectionBudget) {
4208
4948
  const overhead = byteSize - Buffer.byteLength(content, "utf8");
4209
4949
  const allowed = sectionBudget - overhead - BUDGET_MARKER_RESERVE;
4210
4950
  if (allowed <= 0) return;
4211
- const truncated = truncateWithBudgetMarker(content, allowed, sectionBudget);
4951
+ const truncated = truncateWithBudgetMarker(
4952
+ content,
4953
+ allowed,
4954
+ sectionBudget,
4955
+ file.originalLines
4956
+ );
4212
4957
  if (!truncated.isTruncated) return;
4213
4958
  applyTruncatedContent(file, truncated.content);
4214
4959
  text2 = renderFileSection(file);
@@ -4238,14 +4983,17 @@ function applyTruncatedContent(file, content) {
4238
4983
  file.includedLines = countLines(content);
4239
4984
  }
4240
4985
  function rehashPart(part, markdown) {
4986
+ const estimatedTokens = estimateTokens(markdown.replace(TOKEN_LINE, ""));
4987
+ const redistributed = replaceTokenLine(markdown, estimatedTokens);
4241
4988
  return {
4242
4989
  filename: part.filename,
4243
- markdown,
4990
+ markdown: redistributed,
4244
4991
  metadata: {
4245
4992
  ...part.metadata,
4246
- byteSize: Buffer.byteLength(markdown, "utf8"),
4247
- sha256: crypto__namespace.createHash("sha256").update(markdown).digest("hex"),
4248
- lines: countLines(markdown)
4993
+ byteSize: Buffer.byteLength(redistributed, "utf8"),
4994
+ sha256: crypto2__namespace.createHash("sha256").update(redistributed).digest("hex"),
4995
+ lines: countLines(redistributed),
4996
+ estimatedTokens
4249
4997
  }
4250
4998
  };
4251
4999
  }
@@ -4274,7 +5022,7 @@ function greedyGroups(sections, budget) {
4274
5022
  if (current.length > 0) groups.push(current);
4275
5023
  return groups;
4276
5024
  }
4277
- function renderGroups(groups, contentFingerprint) {
5025
+ function renderGroups(groups, budget, contentFingerprint) {
4278
5026
  const total = groups.length;
4279
5027
  const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
4280
5028
  return groups.map((group, index) => {
@@ -4314,9 +5062,11 @@ function renderGroups(groups, contentFingerprint) {
4314
5062
  "## Contents",
4315
5063
  ""
4316
5064
  ].join("\n");
4317
- const content = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
5065
+ const assembled = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
5066
+ const estimatedTokens = estimateTokens(assembled);
5067
+ const content = insertTokenLine(assembled, estimatedTokens);
4318
5068
  const byteSize = Buffer.byteLength(content, "utf8");
4319
- const sha256 = crypto__namespace.createHash("sha256").update(content).digest("hex");
5069
+ const sha256 = crypto2__namespace.createHash("sha256").update(content).digest("hex");
4320
5070
  const includedFiles = group.map(({ file }) => ({
4321
5071
  relativePath: file.relativePath,
4322
5072
  sourceBytes: file.sizeBytes,
@@ -4329,6 +5079,8 @@ function renderGroups(groups, contentFingerprint) {
4329
5079
  index: index + 1,
4330
5080
  filename,
4331
5081
  byteSize,
5082
+ estimatedTokens,
5083
+ budgetBytes: budget,
4332
5084
  sha256,
4333
5085
  lines: countLines(content),
4334
5086
  fileCount: group.length,
@@ -4342,6 +5094,16 @@ function emittedBody(content) {
4342
5094
  const normalised = content.replace(/\r\n/g, "\n");
4343
5095
  return normalised.endsWith("\n") ? normalised.slice(0, -1) : normalised;
4344
5096
  }
5097
+ function compactMetadata(file) {
5098
+ const count = (value) => `${value} line${value === 1 ? "" : "s"}`;
5099
+ const lines = file.includedLines === file.originalLines ? count(file.originalLines) : `${count(file.includedLines)} of ${count(file.originalLines)}`;
5100
+ return [
5101
+ `- Meta: ${capMetadata(file.layer)}/${capMetadata(file.kind)}`,
5102
+ capMetadata(file.language),
5103
+ file.inclusionMode,
5104
+ `${lines} (${file.sizeBytes} B)`
5105
+ ].join(" | ");
5106
+ }
4345
5107
  function renderFileSection(file) {
4346
5108
  const content = file.content ?? "";
4347
5109
  const body = emittedBody(content);
@@ -4353,16 +5115,9 @@ function renderFileSection(file) {
4353
5115
  `<a id="${anchor}"></a>`,
4354
5116
  `### FILE: ${displayPath(file.relativePath)}`,
4355
5117
  "",
4356
- `- Layer: ${capMetadata(file.layer)}`,
4357
- `- Kind: ${capMetadata(file.kind)}`,
4358
- `- Language: ${capMetadata(file.language)}`,
4359
- `- Inclusion: ${file.inclusionMode}`,
4360
- `- Source Bytes: ${file.sizeBytes}`,
4361
- `- Source Lines: ${file.originalLines}`,
4362
- `- Included Lines: ${file.includedLines}`,
4363
- `- SHA-256: ${file.hashSha256}`,
5118
+ compactMetadata(file),
4364
5119
  ...file.summaryQuality ? [`- Summary Quality: ${file.summaryQuality}`] : [],
4365
- ...renderSummary(file.summary),
5120
+ ...renderSummary(file.summary, file.inclusionMode),
4366
5121
  ...file.summaryOmitted ? ["- Summary: (omitted: exceeds the part budget)"] : [],
4367
5122
  "",
4368
5123
  file.inclusionMode === "preview" ? "#### Bounded Preview" : "#### Content",
@@ -4382,14 +5137,15 @@ function fenceForText(content, minimum) {
4382
5137
  const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
4383
5138
  return "`".repeat(Math.max(minimum, maxRun + 1));
4384
5139
  }
4385
- function renderSummary(summary) {
5140
+ function renderSummary(summary, inclusionMode) {
4386
5141
  if (!summary) return [];
4387
5142
  if (typeof summary === "string") return [`- Summary: ${summary}`];
5143
+ if (inclusionMode === "full") return [];
4388
5144
  return ["- Summary:", "```json", JSON.stringify(summary, null, 2), "```"];
4389
5145
  }
4390
5146
  function anchorForPath(filePath) {
4391
5147
  const slug = filePath.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4392
- const digest = crypto__namespace.createHash("sha256").update(filePath).digest("hex").slice(0, 8);
5148
+ const digest = crypto2__namespace.createHash("sha256").update(filePath).digest("hex").slice(0, 8);
4393
5149
  return `${slug || "file"}-${digest}`;
4394
5150
  }
4395
5151
 
@@ -4685,7 +5441,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
4685
5441
  ".so"
4686
5442
  ]);
4687
5443
  function isBinaryExtension(filePath) {
4688
- const ext = path13__namespace.extname(filePath).toLowerCase();
5444
+ const ext = path16__namespace.extname(filePath).toLowerCase();
4689
5445
  return BINARY_EXTENSIONS.has(ext);
4690
5446
  }
4691
5447
  function isBinaryBuffer(buffer) {
@@ -4855,13 +5611,13 @@ function isBinaryFile(absolutePath) {
4855
5611
  if (isBinaryExtension(absolutePath)) return true;
4856
5612
  let fd;
4857
5613
  try {
4858
- fd = fs12__namespace.openSync(absolutePath, "r");
5614
+ fd = fs14__namespace.openSync(absolutePath, "r");
4859
5615
  const buffer = Buffer.alloc(CHUNK);
4860
5616
  let sample;
4861
5617
  let position = 0;
4862
5618
  while (position < BINARY_SCAN_BYTES) {
4863
5619
  const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
4864
- const bytesRead = fs12__namespace.readSync(fd, buffer, 0, wanted, position);
5620
+ const bytesRead = fs14__namespace.readSync(fd, buffer, 0, wanted, position);
4865
5621
  if (bytesRead === 0) return false;
4866
5622
  const chunk = buffer.subarray(0, bytesRead);
4867
5623
  if (sample === void 0) {
@@ -4879,12 +5635,145 @@ function isBinaryFile(absolutePath) {
4879
5635
  } finally {
4880
5636
  if (fd !== void 0) {
4881
5637
  try {
4882
- fs12__namespace.closeSync(fd);
5638
+ fs14__namespace.closeSync(fd);
4883
5639
  } catch {
4884
5640
  }
4885
5641
  }
4886
5642
  }
4887
5643
  }
5644
+ function gitPaths(cwd, args) {
5645
+ let output;
5646
+ try {
5647
+ output = childProcess.execFileSync("git", args, {
5648
+ cwd,
5649
+ stdio: ["ignore", "pipe", "pipe"],
5650
+ encoding: "utf8",
5651
+ maxBuffer: 64 * 1024 * 1024
5652
+ });
5653
+ } catch (err) {
5654
+ const stderr = err.stderr ?? "";
5655
+ const reason = stderr.trim() || err.message;
5656
+ throw new Error(
5657
+ `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.`
5658
+ );
5659
+ }
5660
+ return output.split("\0").map((entry) => entry.trim().replace(/\\/g, "/")).filter((entry) => entry.length > 0);
5661
+ }
5662
+ function assertUsableRef(ref) {
5663
+ if (ref.startsWith("-")) {
5664
+ throw new Error(
5665
+ `--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\`.`
5666
+ );
5667
+ }
5668
+ }
5669
+ function resolveChangedScope(cwd, request) {
5670
+ const paths = /* @__PURE__ */ new Set();
5671
+ const descriptions = [];
5672
+ if (request.changedSince !== void 0) {
5673
+ assertUsableRef(request.changedSince);
5674
+ for (const path17 of gitPaths(cwd, [
5675
+ "diff",
5676
+ "--name-only",
5677
+ "-z",
5678
+ "--relative",
5679
+ request.changedSince,
5680
+ "--"
5681
+ ])) {
5682
+ paths.add(path17);
5683
+ }
5684
+ for (const path17 of gitPaths(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--"])) {
5685
+ paths.add(path17);
5686
+ }
5687
+ descriptions.push(`changed since ${request.changedSince}`);
5688
+ }
5689
+ if (request.staged === true) {
5690
+ for (const path17 of gitPaths(cwd, [
5691
+ "diff",
5692
+ "--cached",
5693
+ "--name-only",
5694
+ "-z",
5695
+ "--relative",
5696
+ "--"
5697
+ ])) {
5698
+ paths.add(path17);
5699
+ }
5700
+ descriptions.push("staged");
5701
+ }
5702
+ return { paths, detail: descriptions.join(" and ") };
5703
+ }
5704
+ var RELATIVE_SPECIFIER = /^\.{1,2}\//;
5705
+ var IMPORT_CANDIDATE_SUFFIXES = [
5706
+ ".ts",
5707
+ ".tsx",
5708
+ ".mts",
5709
+ ".cts",
5710
+ ".js",
5711
+ ".jsx",
5712
+ ".mjs",
5713
+ ".cjs",
5714
+ "/index.ts",
5715
+ "/index.tsx",
5716
+ "/index.js",
5717
+ "/index.mjs"
5718
+ ];
5719
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5720
+ ".ts",
5721
+ ".tsx",
5722
+ ".mts",
5723
+ ".cts",
5724
+ ".js",
5725
+ ".jsx",
5726
+ ".mjs",
5727
+ ".cjs"
5728
+ ]);
5729
+ function buildImportResolver(files) {
5730
+ const byPath = /* @__PURE__ */ new Map();
5731
+ const byPathFolded = /* @__PURE__ */ new Map();
5732
+ const byStem = /* @__PURE__ */ new Map();
5733
+ const byStemFolded = /* @__PURE__ */ new Map();
5734
+ const remember = (map, key, value) => {
5735
+ if (!map.has(key)) map.set(key, value);
5736
+ };
5737
+ for (const file of files) {
5738
+ const relativePath = file.relativePath;
5739
+ remember(byPath, relativePath, relativePath);
5740
+ remember(byPathFolded, relativePath.toLowerCase(), relativePath);
5741
+ const extension = path16__namespace.posix.extname(relativePath);
5742
+ if (!SOURCE_EXTENSIONS.has(extension)) continue;
5743
+ const stem = relativePath.slice(0, -extension.length);
5744
+ remember(byStem, stem, relativePath);
5745
+ remember(byStemFolded, stem.toLowerCase(), relativePath);
5746
+ if (path16__namespace.posix.basename(stem) === "index") {
5747
+ const directory = path16__namespace.posix.dirname(stem);
5748
+ remember(byStem, directory, relativePath);
5749
+ remember(byStemFolded, directory.toLowerCase(), relativePath);
5750
+ }
5751
+ }
5752
+ return { byPath, byPathFolded, byStem, byStemFolded };
5753
+ }
5754
+ function resolveSpecifier(fromFile, specifier, resolver) {
5755
+ if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5756
+ const joined = path16__namespace.posix.normalize(
5757
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
5758
+ );
5759
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5760
+ const direct = exact(joined);
5761
+ if (direct) return direct;
5762
+ for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
5763
+ const hit = exact(`${joined}${suffix}`);
5764
+ if (hit) return hit;
5765
+ }
5766
+ const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5767
+ const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
5768
+ return byStem(joined) ?? byStem(stem);
5769
+ }
5770
+ function importsChangedFile(importerPath, imports, changedPaths, resolver) {
5771
+ for (const specifier of imports) {
5772
+ const resolved = resolveSpecifier(importerPath, specifier, resolver);
5773
+ if (resolved !== null && changedPaths.has(resolved)) return true;
5774
+ }
5775
+ return false;
5776
+ }
4888
5777
  var LANGUAGE_BY_EXTENSION = {
4889
5778
  ".ts": "typescript",
4890
5779
  ".mts": "typescript",
@@ -4918,6 +5807,8 @@ var LANGUAGE_BY_EXTENSION = {
4918
5807
  ".java": "java",
4919
5808
  ".rb": "ruby",
4920
5809
  ".php": "php",
5810
+ ".swift": "swift",
5811
+ ".dart": "dart",
4921
5812
  ".toml": "toml",
4922
5813
  ".ini": "ini",
4923
5814
  ".env": "dotenv",
@@ -4926,7 +5817,8 @@ var LANGUAGE_BY_EXTENSION = {
4926
5817
  ".graphql": "graphql",
4927
5818
  ".gql": "graphql",
4928
5819
  ".proto": "protobuf",
4929
- ".tf": "hcl"
5820
+ ".tf": "hcl",
5821
+ ".tfvars": "hcl"
4930
5822
  };
4931
5823
  var LANGUAGE_BY_BASENAME = {
4932
5824
  Dockerfile: "dockerfile",
@@ -4939,44 +5831,66 @@ var LANGUAGE_BY_BASENAME = {
4939
5831
  };
4940
5832
  function detectLanguage(filePath) {
4941
5833
  const normalized = filePath.replace(/\\/g, "/");
4942
- const base = path13__namespace.posix.basename(normalized);
5834
+ const base = path16__namespace.posix.basename(normalized);
4943
5835
  if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
4944
5836
  if (base === ".env" || base.startsWith(".env.")) return "dotenv";
4945
- const ext = path13__namespace.posix.extname(base).toLowerCase();
5837
+ const ext = path16__namespace.posix.extname(base).toLowerCase();
4946
5838
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
4947
5839
  }
5840
+ var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5841
+ var isTsSource = (relativePath) => {
5842
+ const lower = relativePath.toLowerCase();
5843
+ return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
5844
+ };
5845
+ function importsOfText(relativePath, text2) {
5846
+ if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
5847
+ return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
5848
+ }
5849
+ function importsOfFile(relativePath, absolutePath) {
5850
+ let buffer;
5851
+ try {
5852
+ buffer = fs14__namespace.readFileSync(absolutePath);
5853
+ } catch {
5854
+ return [];
5855
+ }
5856
+ const decoded = decodeText(buffer);
5857
+ if (decoded.binary) return [];
5858
+ return importsOfText(relativePath, decoded.text);
5859
+ }
5860
+
5861
+ // src/validator/validate.ts
4948
5862
  function maskedPath(value, secretRules) {
4949
5863
  return findSecretInPath(value, secretRules)?.masked ?? value;
4950
5864
  }
4951
5865
  function confineToDirectory(directory, filename) {
4952
5866
  if (filename.includes("\0")) return null;
4953
- if (path13__namespace.isAbsolute(filename)) return null;
4954
- const base = path13__namespace.resolve(directory);
4955
- const resolved = path13__namespace.resolve(base, filename);
5867
+ if (path16__namespace.isAbsolute(filename)) return null;
5868
+ const base = path16__namespace.resolve(directory);
5869
+ const resolved = path16__namespace.resolve(base, filename);
4956
5870
  const caseInsensitive = process.platform === "win32" || process.platform === "darwin";
4957
- const prefix = caseInsensitive ? `${base}${path13__namespace.sep}`.toLowerCase() : `${base}${path13__namespace.sep}`;
5871
+ const prefix = caseInsensitive ? `${base}${path16__namespace.sep}`.toLowerCase() : `${base}${path16__namespace.sep}`;
4958
5872
  const candidate = caseInsensitive ? resolved.toLowerCase() : resolved;
4959
5873
  return candidate.startsWith(prefix) ? resolved : null;
4960
5874
  }
4961
5875
  function findStagingDirectory(config) {
4962
- const tmpDir = path13__namespace.resolve(config.cwd, config.tmpDir);
5876
+ const tmpDir = path16__namespace.resolve(config.cwd, config.tmpDir);
4963
5877
  let entries;
4964
5878
  try {
4965
- entries = fs12__namespace.readdirSync(tmpDir);
5879
+ entries = fs14__namespace.readdirSync(tmpDir);
4966
5880
  } catch {
4967
5881
  return null;
4968
5882
  }
4969
5883
  const runs = entries.filter((name) => name.startsWith("run-")).sort();
4970
5884
  const newest = runs[runs.length - 1];
4971
- return newest ? path13__namespace.join(tmpDir, newest) : null;
5885
+ return newest ? path16__namespace.join(tmpDir, newest) : null;
4972
5886
  }
4973
5887
  function describeMissingManifest(config, manifestPath) {
4974
- const outDir = path13__namespace.resolve(config.cwd, config.outDir);
4975
- const markerPath = path13__namespace.join(outDir, ".codebase-snapshot");
5888
+ const outDir = path16__namespace.resolve(config.cwd, config.outDir);
5889
+ const markerPath = path16__namespace.join(outDir, ".codebase-snapshot");
4976
5890
  let marker2 = null;
4977
5891
  try {
4978
- if (fs12__namespace.existsSync(markerPath)) {
4979
- const parsed = JSON.parse(fs12__namespace.readFileSync(markerPath, "utf8"));
5892
+ if (fs14__namespace.existsSync(markerPath)) {
5893
+ const parsed = JSON.parse(fs14__namespace.readFileSync(markerPath, "utf8"));
4980
5894
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4981
5895
  marker2 = parsed;
4982
5896
  }
@@ -5008,18 +5922,18 @@ function emptyDrift() {
5008
5922
  };
5009
5923
  }
5010
5924
  function sha256Hex(buffer) {
5011
- return crypto__namespace.createHash("sha256").update(buffer).digest("hex");
5925
+ return crypto2__namespace.createHash("sha256").update(buffer).digest("hex");
5012
5926
  }
5013
5927
  var VERIFY_SNIFF_MAX_BYTES = 8 * 1024 * 1024;
5014
- function wouldNowBeIncluded(cwd, relativePath, reason) {
5015
- const absolutePath = path13__namespace.join(cwd, relativePath);
5928
+ function wouldNowBeIncluded(cwd, relativePath) {
5929
+ const absolutePath = path16__namespace.join(cwd, relativePath);
5016
5930
  try {
5017
- const stats = fs12__namespace.statSync(absolutePath);
5931
+ const stats = fs14__namespace.statSync(absolutePath);
5018
5932
  if (!stats.isFile()) return false;
5019
- if (isBinaryFile(absolutePath)) return false;
5020
- if (reason !== "binary") return true;
5021
- if (stats.size > VERIFY_SNIFF_MAX_BYTES) return false;
5022
- return !decodeText(fs12__namespace.readFileSync(absolutePath)).binary;
5933
+ if (stats.size <= VERIFY_SNIFF_MAX_BYTES) {
5934
+ return !decodeText(fs14__namespace.readFileSync(absolutePath)).binary;
5935
+ }
5936
+ return !isBinaryFile(absolutePath);
5023
5937
  } catch {
5024
5938
  return false;
5025
5939
  }
@@ -5077,8 +5991,8 @@ function describeManifestShapeProblem(value) {
5077
5991
  function validateSnapshot(configInput) {
5078
5992
  assertUsableConfigInput(configInput);
5079
5993
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
5080
- const manifestPath = path13__namespace.resolve(config.cwd, config.outDir, "manifest.json");
5081
- if (!fs12__namespace.existsSync(manifestPath)) {
5994
+ const manifestPath = path16__namespace.resolve(config.cwd, config.outDir, "manifest.json");
5995
+ if (!fs14__namespace.existsSync(manifestPath)) {
5082
5996
  return {
5083
5997
  valid: false,
5084
5998
  manifestPresent: false,
@@ -5089,7 +6003,7 @@ function validateSnapshot(configInput) {
5089
6003
  }
5090
6004
  let manifest;
5091
6005
  try {
5092
- manifest = JSON.parse(fs12__namespace.readFileSync(manifestPath, "utf8"));
6006
+ manifest = JSON.parse(fs14__namespace.readFileSync(manifestPath, "utf8"));
5093
6007
  } catch (err) {
5094
6008
  return {
5095
6009
  valid: false,
@@ -5111,7 +6025,7 @@ function validateSnapshot(configInput) {
5111
6025
  }
5112
6026
  const drift = emptyDrift();
5113
6027
  const warnings = [];
5114
- const snapshotDir = path13__namespace.resolve(config.cwd, config.outDir);
6028
+ const snapshotDir = path16__namespace.resolve(config.cwd, config.outDir);
5115
6029
  const manifestFileMap = /* @__PURE__ */ new Map();
5116
6030
  for (const file of manifest.files) {
5117
6031
  if (file && typeof file.relativePath === "string") {
@@ -5129,23 +6043,72 @@ function validateSnapshot(configInput) {
5129
6043
  if (direct !== void 0) return direct;
5130
6044
  return skippedReasons.get(maskedPath(diskFile, config.secretRules)) ?? null;
5131
6045
  };
6046
+ let unchangedScope = null;
6047
+ const recordedChangedSince = manifest.config?.changedSince;
6048
+ const recordedStaged = manifest.config?.staged === true;
6049
+ const recordedWithDependents = manifest.config?.withDependents === true;
6050
+ if (recordedChangedSince !== void 0 || recordedStaged) {
6051
+ try {
6052
+ unchangedScope = new Set(
6053
+ resolveChangedScope(config.cwd, {
6054
+ ...recordedChangedSince === void 0 ? {} : { changedSince: recordedChangedSince },
6055
+ ...recordedStaged ? { staged: true } : {}
6056
+ }).paths
6057
+ );
6058
+ } catch {
6059
+ warnings.push(
6060
+ "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."
6061
+ );
6062
+ }
6063
+ }
6064
+ let dependentResolver = null;
6065
+ function becameDependent(diskFile) {
6066
+ if (!recordedWithDependents || unchangedScope === null) return false;
6067
+ dependentResolver ??= buildImportResolver(
6068
+ collection.files.map((file) => ({ relativePath: file }))
6069
+ );
6070
+ return importsChangedFile(
6071
+ diskFile,
6072
+ importsOfFile(diskFile, path16__namespace.join(config.cwd, diskFile)),
6073
+ unchangedScope,
6074
+ dependentResolver
6075
+ );
6076
+ }
5132
6077
  const collection = collectFiles(config.cwd, {
5133
6078
  includePatterns: config.include,
5134
6079
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5135
6080
  includeSensitiveFiles: config.includeSensitiveFiles,
5136
- secretRules: config.secretRules
6081
+ secretRules: config.secretRules,
6082
+ // Passed explicitly, like `generate` does. Relying on a process-wide "current
6083
+ // discovery mode" meant `validate` could enumerate files one way while the snapshot
6084
+ // had been generated another way, and then blame the difference on drift.
6085
+ discovery: config.discovery,
6086
+ // The same exclusion `generate` applies: without it, `validate` recounted the tool's
6087
+ // own output directory as a set of files missing from the snapshot, so a correct
6088
+ // snapshot was reported as drifting.
6089
+ toolOwnedPaths: [config.outDir, config.tmpDir].map(
6090
+ (directory) => path16__namespace.relative(config.cwd, path16__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
6091
+ ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
5137
6092
  });
5138
6093
  for (const diskFile of collection.files) {
5139
6094
  const snapshotHash = manifestFileMap.get(diskFile);
5140
6095
  if (!snapshotHash) {
5141
6096
  const skipReason = skippedReasonOnDisk(diskFile);
5142
- if (skipReason !== null && !wouldNowBeIncluded(config.cwd, diskFile, skipReason)) continue;
6097
+ if (skipReason === "unchanged") {
6098
+ if (unchangedScope !== null) {
6099
+ if (unchangedScope.has(diskFile) || becameDependent(diskFile)) {
6100
+ drift.missingInSnapshot.push(diskFile);
6101
+ }
6102
+ }
6103
+ continue;
6104
+ }
6105
+ if (skipReason !== null && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
5143
6106
  drift.missingInSnapshot.push(diskFile);
5144
6107
  continue;
5145
6108
  }
5146
6109
  let diskHash;
5147
6110
  try {
5148
- diskHash = sha256Hex(fs12__namespace.readFileSync(path13__namespace.join(config.cwd, diskFile)));
6111
+ diskHash = sha256Hex(fs14__namespace.readFileSync(path16__namespace.join(config.cwd, diskFile)));
5149
6112
  } catch (err) {
5150
6113
  if (err.code === "ENOENT") {
5151
6114
  drift.missingOnDisk.push(diskFile);
@@ -5169,7 +6132,7 @@ function validateSnapshot(configInput) {
5169
6132
  }
5170
6133
  let statFailure = null;
5171
6134
  try {
5172
- fs12__namespace.statSync(confined);
6135
+ fs14__namespace.statSync(confined);
5173
6136
  } catch (err) {
5174
6137
  statFailure = err;
5175
6138
  }
@@ -5189,7 +6152,7 @@ function validateSnapshot(configInput) {
5189
6152
  ...Object.values(config.budgets.byLayer)
5190
6153
  );
5191
6154
  const partNames = /* @__PURE__ */ new Set();
5192
- const partsDir = path13__namespace.join(snapshotDir, "parts");
6155
+ const partsDir = path16__namespace.join(snapshotDir, "parts");
5193
6156
  for (const part of manifest.parts) {
5194
6157
  if (!part || typeof part.filename !== "string") continue;
5195
6158
  partNames.add(part.filename);
@@ -5204,7 +6167,7 @@ function validateSnapshot(configInput) {
5204
6167
  }
5205
6168
  let buffer;
5206
6169
  try {
5207
- buffer = fs12__namespace.readFileSync(partPath);
6170
+ buffer = fs14__namespace.readFileSync(partPath);
5208
6171
  } catch {
5209
6172
  drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
5210
6173
  continue;
@@ -5218,10 +6181,11 @@ function validateSnapshot(configInput) {
5218
6181
  if (sha256Hex(buffer) !== part.sha256) {
5219
6182
  drift.partIssues.push({ part: part.filename, issue: "sha256 does not match the manifest" });
5220
6183
  }
5221
- if (buffer.length > maxEffectiveBudget) {
6184
+ const partBudget = typeof part.budgetBytes === "number" ? part.budgetBytes : maxEffectiveBudget;
6185
+ if (buffer.length > partBudget) {
5222
6186
  drift.partIssues.push({
5223
6187
  part: part.filename,
5224
- issue: `${buffer.length} B exceeds the effective part budget ${maxEffectiveBudget} B`
6188
+ issue: typeof part.budgetBytes === "number" ? `${buffer.length} B exceeds its part budget ${partBudget} B` : `${buffer.length} B exceeds the effective part budget ${partBudget} B`
5225
6189
  });
5226
6190
  }
5227
6191
  }
@@ -5418,9 +6382,9 @@ function validateSnapshot(configInput) {
5418
6382
  ["full-codebase.md", manifest.artifacts?.fullMarkdown]
5419
6383
  ]) {
5420
6384
  if (!artifact) continue;
5421
- const artifactPath = path13__namespace.join(snapshotDir, key);
6385
+ const artifactPath = path16__namespace.join(snapshotDir, key);
5422
6386
  try {
5423
- const buffer = fs12__namespace.readFileSync(artifactPath);
6387
+ const buffer = fs14__namespace.readFileSync(artifactPath);
5424
6388
  if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
5425
6389
  drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
5426
6390
  }
@@ -5428,6 +6392,18 @@ function validateSnapshot(configInput) {
5428
6392
  drift.partIssues.push({ part: key, issue: "declared artifact is missing" });
5429
6393
  }
5430
6394
  }
6395
+ const sarifArtifact = manifest.artifacts?.sarif;
6396
+ if (sarifArtifact) {
6397
+ const sarifKey = "reports/audit.sarif";
6398
+ try {
6399
+ const buffer = fs14__namespace.readFileSync(path16__namespace.join(snapshotDir, "reports", "audit.sarif"));
6400
+ if (sha256Hex(buffer) !== sarifArtifact.sha256 || buffer.length !== sarifArtifact.byteSize) {
6401
+ drift.partIssues.push({ part: sarifKey, issue: "artifact does not match its recorded digest" });
6402
+ }
6403
+ } catch {
6404
+ drift.partIssues.push({ part: sarifKey, issue: "declared artifact is missing" });
6405
+ }
6406
+ }
5431
6407
  if (manifest.git && manifest.git.branch && manifest.git.branch !== config.branch) {
5432
6408
  warnings.push(
5433
6409
  `Snapshot was generated on branch "${manifest.git.branch}" but the configured branch is "${config.branch}".`
@@ -5490,7 +6466,7 @@ function validateSnapshot(configInput) {
5490
6466
 
5491
6467
  // src/version.ts
5492
6468
  var GENERATOR_NAME = "codebase-snapshot";
5493
- var GENERATOR_VERSION = "0.9.9";
6469
+ var GENERATOR_VERSION = "0.9.12";
5494
6470
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
5495
6471
  function isOurGeneratorName(name) {
5496
6472
  if (typeof name !== "string") return false;
@@ -5537,17 +6513,17 @@ function isNewer(candidate, current) {
5537
6513
  }
5538
6514
  function cacheDirectory(env = process.env, platform = process.platform) {
5539
6515
  if (platform === "win32") {
5540
- const base2 = env.LOCALAPPDATA ?? path13__namespace.join(os__namespace.homedir(), "AppData", "Local");
5541
- return path13__namespace.join(base2, "codebase-snapshot");
6516
+ const base2 = env.LOCALAPPDATA ?? path16__namespace.join(os__namespace.homedir(), "AppData", "Local");
6517
+ return path16__namespace.join(base2, "codebase-snapshot");
5542
6518
  }
5543
6519
  if (platform === "darwin") {
5544
- return path13__namespace.join(os__namespace.homedir(), "Library", "Caches", "codebase-snapshot");
6520
+ return path16__namespace.join(os__namespace.homedir(), "Library", "Caches", "codebase-snapshot");
5545
6521
  }
5546
- const base = env.XDG_CACHE_HOME ?? path13__namespace.join(os__namespace.homedir(), ".cache");
5547
- return path13__namespace.join(base, "codebase-snapshot");
6522
+ const base = env.XDG_CACHE_HOME ?? path16__namespace.join(os__namespace.homedir(), ".cache");
6523
+ return path16__namespace.join(base, "codebase-snapshot");
5548
6524
  }
5549
6525
  function cacheFilePath(env = process.env) {
5550
- return path13__namespace.join(cacheDirectory(env), "update-check.json");
6526
+ return path16__namespace.join(cacheDirectory(env), "update-check.json");
5551
6527
  }
5552
6528
  function shouldCheck(argv, env) {
5553
6529
  const disabled = ["1", "true", "yes"].includes(
@@ -5560,13 +6536,23 @@ function shouldCheck(argv, env) {
5560
6536
  for (const skip of ["mcp", "describe", "skill", "--version", "-v", "--help", "-h"]) {
5561
6537
  if (args.includes(skip)) return false;
5562
6538
  }
5563
- if (args.some((argument) => argument.includes("json"))) return false;
6539
+ for (let index = 0; index < args.length; index += 1) {
6540
+ const argument = args[index];
6541
+ if (argument === "--format") {
6542
+ if (args[index + 1] === "json") return false;
6543
+ break;
6544
+ }
6545
+ if (argument.startsWith("--format=")) {
6546
+ if (argument.slice("--format=".length) === "json") return false;
6547
+ break;
6548
+ }
6549
+ }
5564
6550
  return true;
5565
6551
  }
5566
6552
  function readCache(file) {
5567
6553
  let raw;
5568
6554
  try {
5569
- raw = fs12__namespace.readFileSync(file, "utf8");
6555
+ raw = fs14__namespace.readFileSync(file, "utf8");
5570
6556
  } catch {
5571
6557
  return null;
5572
6558
  }
@@ -5583,8 +6569,8 @@ function readCache(file) {
5583
6569
  }
5584
6570
  function writeCache(file, entry) {
5585
6571
  try {
5586
- fs12__namespace.mkdirSync(path13__namespace.dirname(file), { recursive: true });
5587
- fs12__namespace.writeFileSync(file, `${JSON.stringify(entry)}
6572
+ fs14__namespace.mkdirSync(path16__namespace.dirname(file), { recursive: true });
6573
+ fs14__namespace.writeFileSync(file, `${JSON.stringify(entry)}
5588
6574
  `, "utf8");
5589
6575
  } catch {
5590
6576
  }
@@ -5678,42 +6664,42 @@ function describeMatcher(matcher) {
5678
6664
  return String(matcher);
5679
6665
  }
5680
6666
  function normalizeTarget(cwd, target) {
5681
- const absolute = path13__namespace.resolve(cwd, target);
5682
- const relative7 = path13__namespace.relative(cwd, absolute);
5683
- if (relative7.startsWith("..") || path13__namespace.isAbsolute(relative7)) return target;
5684
- return relative7.split(path13__namespace.sep).join("/");
6667
+ const absolute = path16__namespace.resolve(cwd, target);
6668
+ const relative8 = path16__namespace.relative(cwd, absolute);
6669
+ if (relative8.startsWith("..") || path16__namespace.isAbsolute(relative8)) return target;
6670
+ return relative8.split(path16__namespace.sep).join("/");
5685
6671
  }
5686
6672
  function findRuleMatch(rules, relativePath) {
5687
6673
  return rules.find(
5688
6674
  (rule) => matchRule(relativePath, rule.match)
5689
6675
  );
5690
6676
  }
5691
- function findRecordedAncestor(skipped, relative7, maskedPath2) {
6677
+ function findRecordedAncestor(skipped, relative8, maskedPath2) {
5692
6678
  let nearest;
5693
6679
  for (const record of skipped) {
5694
6680
  const ancestorPath = record.path;
5695
6681
  if (ancestorPath.length === 0) continue;
5696
- const isAncestor = relative7.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
6682
+ const isAncestor = relative8.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
5697
6683
  if (!isAncestor) continue;
5698
6684
  if (!nearest || ancestorPath.length > nearest.path.length) nearest = record;
5699
6685
  }
5700
6686
  return nearest;
5701
6687
  }
5702
6688
  function readManifest2(config) {
5703
- const manifestPath = path13__namespace.resolve(config.cwd, config.outDir, "manifest.json");
6689
+ const manifestPath = path16__namespace.resolve(config.cwd, config.outDir, "manifest.json");
5704
6690
  try {
5705
- return JSON.parse(fs12__namespace.readFileSync(manifestPath, "utf8"));
6691
+ return JSON.parse(fs14__namespace.readFileSync(manifestPath, "utf8"));
5706
6692
  } catch {
5707
6693
  return null;
5708
6694
  }
5709
6695
  }
5710
6696
  function explainPath(config, target) {
5711
- const relative7 = normalizeTarget(config.cwd, target);
5712
- const absolute = path13__namespace.join(config.cwd, relative7);
5713
- const maskedPath2 = findSecretInPath(relative7, config.secretRules)?.masked ?? relative7;
6697
+ const relative8 = normalizeTarget(config.cwd, target);
6698
+ const absolute = path16__namespace.join(config.cwd, relative8);
6699
+ const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
5714
6700
  const result = {
5715
6701
  path: maskedPath2,
5716
- exists: fs12__namespace.existsSync(absolute),
6702
+ exists: fs14__namespace.existsSync(absolute),
5717
6703
  included: false
5718
6704
  };
5719
6705
  const collection = collectFiles(config.cwd, {
@@ -5721,15 +6707,18 @@ function explainPath(config, target) {
5721
6707
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5722
6708
  includeSensitiveFiles: config.includeSensitiveFiles,
5723
6709
  secretRules: config.secretRules,
6710
+ // `explain` must enumerate files exactly as `generate` would, or it explains a
6711
+ // decision the generator never made.
6712
+ discovery: config.discovery,
5724
6713
  // Same exclusion `generate` applies, so `explain` never says "not-discovered" about a file
5725
6714
  // that is only missing because the walker dropped the tool's own output directory.
5726
6715
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
5727
- (directory) => path13__namespace.relative(config.cwd, path13__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
5728
- ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
6716
+ (directory) => path16__namespace.relative(config.cwd, path16__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
6717
+ ).filter((relative9) => relative9.length > 0 && !relative9.startsWith(".."))
5729
6718
  });
5730
- if (!collection.files.includes(relative7)) {
6719
+ if (!collection.files.includes(relative8)) {
5731
6720
  const record = collection.skipped.find(
5732
- (entry2) => entry2.path === relative7 || entry2.path === maskedPath2
6721
+ (entry2) => entry2.path === relative8 || entry2.path === maskedPath2
5733
6722
  );
5734
6723
  if (record) {
5735
6724
  result.reason = record.reason;
@@ -5738,7 +6727,7 @@ function explainPath(config, target) {
5738
6727
  result.reason = "missing";
5739
6728
  result.detail = "no such file or directory, relative to the configured cwd";
5740
6729
  } else {
5741
- const ancestor = findRecordedAncestor(collection.skipped, relative7, maskedPath2);
6730
+ const ancestor = findRecordedAncestor(collection.skipped, relative8, maskedPath2);
5742
6731
  if (ancestor) {
5743
6732
  result.reason = ancestor.reason;
5744
6733
  result.detail = ancestor.detail ? `${ancestor.detail} \u2014 inherited from the skipped directory \`${ancestor.path}\`` : `inside the skipped directory \`${ancestor.path}\``;
@@ -5750,21 +6739,58 @@ function explainPath(config, target) {
5750
6739
  return result;
5751
6740
  }
5752
6741
  result.included = true;
5753
- if (isBinaryFile(absolute)) {
6742
+ if (config.changedSince !== void 0 || config.staged === true) {
6743
+ let inScope;
6744
+ let scopeDetail = "";
6745
+ try {
6746
+ const scope = resolveChangedScope(config.cwd, {
6747
+ ...config.changedSince === void 0 ? {} : { changedSince: config.changedSince },
6748
+ ...config.staged === true ? { staged: true } : {}
6749
+ });
6750
+ inScope = scope.paths.has(relative8);
6751
+ if (!inScope && config.withDependents === true) {
6752
+ inScope = importsChangedFile(
6753
+ relative8,
6754
+ importsOfFile(relative8, absolute),
6755
+ scope.paths,
6756
+ buildImportResolver(collection.files.map((file) => ({ relativePath: file })))
6757
+ );
6758
+ }
6759
+ scopeDetail = scope.detail;
6760
+ } catch (err) {
6761
+ result.included = false;
6762
+ result.reason = "unknown";
6763
+ result.detail = `the changed-files scope could not be resolved: ${err.message}`;
6764
+ return result;
6765
+ }
6766
+ if (!inScope) {
6767
+ result.included = false;
6768
+ result.reason = "unchanged";
6769
+ 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)`;
6770
+ return result;
6771
+ }
6772
+ }
6773
+ let sizeOnDisk = null;
6774
+ try {
6775
+ sizeOnDisk = fs14__namespace.statSync(absolute).size;
6776
+ } catch {
6777
+ sizeOnDisk = null;
6778
+ }
6779
+ if (sizeOnDisk !== null && sizeOnDisk > BINARY_SCAN_BYTES && isBinaryFile(absolute)) {
5754
6780
  result.included = false;
5755
6781
  result.reason = "binary";
5756
6782
  result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
5757
6783
  return result;
5758
6784
  }
5759
- result.layer = classifyLayer(relative7, config.layers);
5760
- result.kind = classifyKind(relative7, config.kinds);
5761
- result.language = detectLanguage(relative7);
5762
- const layerRule = findRuleMatch(config.layers, relative7);
6785
+ result.layer = classifyLayer(relative8, config.layers);
6786
+ result.kind = classifyKind(relative8, config.kinds);
6787
+ result.language = detectLanguage(relative8);
6788
+ const layerRule = findRuleMatch(config.layers, relative8);
5763
6789
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
5764
- const kindRule = findRuleMatch(config.kinds, relative7);
6790
+ const kindRule = findRuleMatch(config.kinds, relative8);
5765
6791
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
5766
- if (fs12__namespace.existsSync(absolute) && fs12__namespace.statSync(absolute).isFile()) {
5767
- const buffer = fs12__namespace.readFileSync(absolute);
6792
+ if (fs14__namespace.existsSync(absolute) && fs14__namespace.statSync(absolute).isFile()) {
6793
+ const buffer = fs14__namespace.readFileSync(absolute);
5768
6794
  const decoded = decodeText(buffer);
5769
6795
  if (decoded.binary) {
5770
6796
  result.included = false;
@@ -5776,12 +6802,12 @@ function explainPath(config, target) {
5776
6802
  result.bytes = buffer.length;
5777
6803
  result.lines = countLines(text2);
5778
6804
  if (decoded.encoding !== "utf-8") result.encoding = decoded.encoding;
5779
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative7);
6805
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative8);
5780
6806
  result.secretCount = findings.length;
5781
6807
  result.secrets = findings.slice(0, 20).map((finding) => ({ rules: finding.rules, line: finding.line }));
5782
6808
  }
5783
6809
  const manifest = readManifest2(config);
5784
- const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative7) : void 0;
6810
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative8) : void 0;
5785
6811
  result.part = entry?.part ?? null;
5786
6812
  result.inclusionMode = entry?.inclusionMode;
5787
6813
  return result;
@@ -6012,14 +7038,14 @@ function compareManifests(before, after) {
6012
7038
  function loadManifest(target) {
6013
7039
  let isDirectory;
6014
7040
  try {
6015
- isDirectory = fs12__namespace.statSync(target).isDirectory();
7041
+ isDirectory = fs14__namespace.statSync(target).isDirectory();
6016
7042
  } catch {
6017
7043
  throw new Error(`No snapshot found at ${target}: the path does not exist.`);
6018
7044
  }
6019
- const manifestPath = isDirectory ? path13__namespace.join(target, "manifest.json") : target;
7045
+ const manifestPath = isDirectory ? path16__namespace.join(target, "manifest.json") : target;
6020
7046
  let raw;
6021
7047
  try {
6022
- raw = fs12__namespace.readFileSync(manifestPath, "utf8");
7048
+ raw = fs14__namespace.readFileSync(manifestPath, "utf8");
6023
7049
  } catch {
6024
7050
  throw new Error(`Could not read a manifest at ${manifestPath}.`);
6025
7051
  }
@@ -6076,15 +7102,15 @@ function renderDiffText(diff) {
6076
7102
  function appendGitignoreEntries(cwd, entries) {
6077
7103
  const wanted = [...new Set(entries)].filter((entry) => entry.trim().length > 0);
6078
7104
  if (wanted.length === 0) return { added: [], note: null };
6079
- const ignorePath = path13__namespace.join(cwd, ".gitignore");
7105
+ const ignorePath = path16__namespace.join(cwd, ".gitignore");
6080
7106
  const manual = `Add ${wanted.map((entry) => `"${entry}"`).join(" and ")} to your .gitignore yourself.`;
6081
7107
  let existing = "";
6082
7108
  try {
6083
- if (fs12__namespace.existsSync(ignorePath)) {
6084
- if (fs12__namespace.statSync(ignorePath).isDirectory()) {
7109
+ if (fs14__namespace.existsSync(ignorePath)) {
7110
+ if (fs14__namespace.statSync(ignorePath).isDirectory()) {
6085
7111
  return { added: [], note: `${ignorePath} is a directory, so it was not updated. ${manual}` };
6086
7112
  }
6087
- existing = fs12__namespace.readFileSync(ignorePath, "utf8");
7113
+ existing = fs14__namespace.readFileSync(ignorePath, "utf8");
6088
7114
  }
6089
7115
  } catch (err) {
6090
7116
  return {
@@ -6097,7 +7123,7 @@ function appendGitignoreEntries(cwd, entries) {
6097
7123
  if (added.length === 0) return { added: [], note: null };
6098
7124
  const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
6099
7125
  try {
6100
- fs12__namespace.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
7126
+ fs14__namespace.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
6101
7127
  `, "utf8");
6102
7128
  } catch (err) {
6103
7129
  return { added: [], note: `${err.message}. ${manual}` };
@@ -6110,7 +7136,12 @@ var DEFAULT_ANSWERS = {
6110
7136
  preset: "generic",
6111
7137
  branch: "main",
6112
7138
  maxPartBytes: "300k",
7139
+ maxPartTokens: null,
6113
7140
  largeFileBytes: "200k",
7141
+ maxMonolithBytes: null,
7142
+ discovery: "auto",
7143
+ skeleton: false,
7144
+ sarif: false,
6114
7145
  include: [],
6115
7146
  excludePatterns: [],
6116
7147
  artifacts: {
@@ -6163,6 +7194,9 @@ function renderConfig(answers) {
6163
7194
  // \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
6164
7195
  // Hard ceiling per part. "300k", 307200 and "1.5mb" are all accepted.
6165
7196
  maxPartBytes: ${size(answers.maxPartBytes)},
7197
+ // The same ceiling expressed in tokens; it narrows maxPartBytes rather than
7198
+ // replacing it. null = not set. Minimum 1024.
7199
+ maxPartTokens: ${answers.maxPartTokens === null ? "null" : String(answers.maxPartTokens)},
6166
7200
  // Files above this size get a head/tail preview instead of the full body.
6167
7201
  largeFileBytes: ${size(answers.largeFileBytes)},
6168
7202
  previewHeadLines: 80,
@@ -6173,6 +7207,22 @@ function renderConfig(answers) {
6173
7207
  byLayer: {},
6174
7208
  },
6175
7209
 
7210
+ // \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
7211
+ // Replace function and method bodies with \`{ /* ... */ }\` in TypeScript files.
7212
+ // Signatures and comments stay, so the structure of the file survives.
7213
+ skeleton: ${answers.skeleton},
7214
+ // Also write reports/audit.sarif (SARIF 2.1.0) for a CI security gate. The
7215
+ // matched value is never included \u2014 only the path, the line and the rule.
7216
+ sarif: ${answers.sarif},
7217
+ // Snapshot only the files that differ from a git ref (for example "main"),
7218
+ // plus untracked ones. null and false both mean "snapshot everything".
7219
+ changedSince: null,
7220
+ // Snapshot only what is staged in the index: the pre-commit counterpart.
7221
+ staged: false,
7222
+ // With changedSince or staged, also include the files that import a changed
7223
+ // one, one hop out. Meaningless without a scope.
7224
+ withDependents: false,
7225
+
6176
7226
  // \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
6177
7227
  // Allow-list. Empty means "everything that is not ignored".
6178
7228
  // Example: ["src/**", "packages/*/src/**"]
@@ -6182,7 +7232,7 @@ function renderConfig(answers) {
6182
7232
  // How candidate files are found: "auto" lists them through git when git can
6183
7233
  // and walks the tree otherwise; "git" and "walk" force one path, and "walk" is
6184
7234
  // the escape hatch when .git exists but git cannot list it.
6185
- discovery: "auto",
7235
+ discovery: ${JSON.stringify(answers.discovery)},
6186
7236
 
6187
7237
  // \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
6188
7238
  // "merge" = the preset's rules plus yours (yours win). "replace" = yours only.
@@ -6213,7 +7263,7 @@ function renderConfig(answers) {
6213
7263
  monolith: ${artifacts.monolith}, // full-codebase.json + .md (roughly doubles output)
6214
7264
  },
6215
7265
  // Skip the monolith above this size. null = no limit.
6216
- maxMonolithBytes: null,
7266
+ maxMonolithBytes: ${answers.maxMonolithBytes === null ? "null" : size(answers.maxMonolithBytes)},
6217
7267
  // Top-level switch for the monolith, equivalent to artifacts.monolith.
6218
7268
  // When both are present the artifact entry wins; this one exists so the
6219
7269
  // generated file lists every setting the tool accepts.
@@ -6590,10 +7640,10 @@ var CONFIG_CANDIDATES = [
6590
7640
  ];
6591
7641
  function chooseConfigFileName(cwd) {
6592
7642
  if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
6593
- const pkgPath = path13__namespace.join(cwd, "package.json");
6594
- if (!fs12__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
7643
+ const pkgPath = path16__namespace.join(cwd, "package.json");
7644
+ if (!fs14__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
6595
7645
  try {
6596
- const parsed = JSON.parse(fs12__namespace.readFileSync(pkgPath, "utf8"));
7646
+ const parsed = JSON.parse(fs14__namespace.readFileSync(pkgPath, "utf8"));
6597
7647
  return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
6598
7648
  } catch {
6599
7649
  return "snapshot.config.mjs";
@@ -6607,7 +7657,7 @@ var SIZE_CHOICES = [
6607
7657
  ];
6608
7658
  function packageJsonHasConfig(cwd) {
6609
7659
  try {
6610
- const parsed = JSON.parse(fs12__namespace.readFileSync(path13__namespace.join(cwd, "package.json"), "utf8"));
7660
+ const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
6611
7661
  return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
6612
7662
  } catch {
6613
7663
  return false;
@@ -6624,11 +7674,12 @@ function artifactAnswers(options) {
6624
7674
  const keep = new Set(options.onlyArtifacts);
6625
7675
  for (const name of ARTIFACT_NAMES) artifacts[name] = keep.has(name);
6626
7676
  }
7677
+ if (options.includeMonolith === true) artifacts.monolith = true;
6627
7678
  return artifacts;
6628
7679
  }
6629
7680
  function topLevelDirs(cwd) {
6630
7681
  try {
6631
- return fs12__namespace.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
7682
+ return fs14__namespace.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
6632
7683
  } catch {
6633
7684
  return [];
6634
7685
  }
@@ -6641,7 +7692,7 @@ function describeJsonValue(value) {
6641
7692
  }
6642
7693
  function writeFileOrExplain(target, content) {
6643
7694
  try {
6644
- fs12__namespace.writeFileSync(target, content, "utf8");
7695
+ fs14__namespace.writeFileSync(target, content, "utf8");
6645
7696
  } catch (err) {
6646
7697
  const code = err.code;
6647
7698
  const reason = code === "EACCES" || code === "EPERM" || code === "EROFS" ? `permission denied (${code})` : code === "EISDIR" ? "it is a directory" : `${err.message}`;
@@ -6652,7 +7703,7 @@ function canWriteModuleConfig(name, cwd) {
6652
7703
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
6653
7704
  if (!name.endsWith(".js")) return false;
6654
7705
  try {
6655
- const parsed = JSON.parse(fs12__namespace.readFileSync(path13__namespace.join(cwd, "package.json"), "utf8"));
7706
+ const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
6656
7707
  return parsed.type === "module";
6657
7708
  } catch {
6658
7709
  return false;
@@ -6687,19 +7738,19 @@ function serializePackageJson(raw, value) {
6687
7738
  return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
6688
7739
  }
6689
7740
  function updatePackageJson(cwd, notes) {
6690
- const packagePath = path13__namespace.join(cwd, "package.json");
7741
+ const packagePath = path16__namespace.join(cwd, "package.json");
6691
7742
  const unchanged = {
6692
7743
  scripts: [],
6693
7744
  dependency: { name: GENERATOR_NAME, range: null, added: false }
6694
7745
  };
6695
- if (!fs12__namespace.existsSync(packagePath)) {
7746
+ if (!fs14__namespace.existsSync(packagePath)) {
6696
7747
  notes.push("No package.json found, so npm scripts and the devDependency were not added.");
6697
7748
  return unchanged;
6698
7749
  }
6699
7750
  let raw;
6700
7751
  let parsed;
6701
7752
  try {
6702
- raw = fs12__namespace.readFileSync(packagePath, "utf8");
7753
+ raw = fs14__namespace.readFileSync(packagePath, "utf8");
6703
7754
  parsed = JSON.parse(raw);
6704
7755
  } catch {
6705
7756
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
@@ -6724,17 +7775,26 @@ function updatePackageJson(cwd, notes) {
6724
7775
  scriptsWereBroken = true;
6725
7776
  }
6726
7777
  const added = [];
7778
+ const isSelfRepository = parsed.name === GENERATOR_NAME;
7779
+ const scriptPrefix = isSelfRepository ? "node ./dist/bin.js" : `npx ${GENERATOR_NAME}`;
6727
7780
  if (!scripts.snapshot) {
6728
- scripts.snapshot = `npx ${GENERATOR_NAME} generate`;
7781
+ scripts.snapshot = `${scriptPrefix} generate`;
6729
7782
  added.push("snapshot");
6730
7783
  }
6731
7784
  if (!scripts["snapshot:validate"]) {
6732
- scripts["snapshot:validate"] = `npx ${GENERATOR_NAME} validate`;
7785
+ scripts["snapshot:validate"] = `${scriptPrefix} validate`;
6733
7786
  added.push("snapshot:validate");
6734
7787
  }
6735
7788
  const declared = declaredRange(parsed);
6736
7789
  let dependency;
6737
- if (declared !== null) {
7790
+ if (isSelfRepository) {
7791
+ if (declared !== null) {
7792
+ notes.push(
7793
+ `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.`
7794
+ );
7795
+ }
7796
+ dependency = { name: GENERATOR_NAME, range: declared, added: false };
7797
+ } else if (declared !== null) {
6738
7798
  dependency = { name: GENERATOR_NAME, range: declared, added: false };
6739
7799
  } else {
6740
7800
  const range = `^${GENERATOR_VERSION}`;
@@ -6784,7 +7844,15 @@ async function runInit(options) {
6784
7844
  // `validate` warn about a mismatch the user never caused.
6785
7845
  branch: getGitInfo(options.cwd).branch,
6786
7846
  maxPartBytes: options.maxPartBytes ?? detected.recommendedBudget ?? DEFAULT_ANSWERS.maxPartBytes,
6787
- largeFileBytes: DEFAULT_ANSWERS.largeFileBytes,
7847
+ // Every setting the caller named is written down, not just forwarded. A config that omits
7848
+ // what the run used is drift the next `validate` reports, and `setup` is the one command
7849
+ // that is supposed to leave a project in a consistent state.
7850
+ maxPartTokens: options.maxPartTokens ?? DEFAULT_ANSWERS.maxPartTokens,
7851
+ largeFileBytes: options.largeFileBytes ?? DEFAULT_ANSWERS.largeFileBytes,
7852
+ maxMonolithBytes: options.maxMonolithBytes ?? DEFAULT_ANSWERS.maxMonolithBytes,
7853
+ discovery: options.discovery ?? DEFAULT_ANSWERS.discovery,
7854
+ skeleton: options.skeleton ?? DEFAULT_ANSWERS.skeleton,
7855
+ sarif: options.sarif ?? DEFAULT_ANSWERS.sarif,
6788
7856
  include: options.include ?? [],
6789
7857
  excludePatterns: options.exclude ?? [],
6790
7858
  artifacts: artifactAnswers(options),
@@ -6991,7 +8059,7 @@ async function runInit(options) {
6991
8059
  const configText = renderConfig(answers);
6992
8060
  const preferredName = chooseConfigFileName(options.cwd);
6993
8061
  const presentConfigs = CONFIG_CANDIDATES.filter(
6994
- (name) => fs12__namespace.existsSync(path13__namespace.join(options.cwd, name))
8062
+ (name) => fs14__namespace.existsSync(path16__namespace.join(options.cwd, name))
6995
8063
  );
6996
8064
  const present = presentConfigs[0] ?? null;
6997
8065
  if (presentConfigs.length > 1) {
@@ -7000,7 +8068,7 @@ async function runInit(options) {
7000
8068
  );
7001
8069
  }
7002
8070
  const configName = present !== null && options.force ? present : preferredName;
7003
- const configPath = path13__namespace.join(options.cwd, configName);
8071
+ const configPath = path16__namespace.join(options.cwd, configName);
7004
8072
  const reused = present !== null && options.force !== true && options.reuseConfig === true;
7005
8073
  if (reused) notes.push(`Kept the existing ${present} instead of writing a new one.`);
7006
8074
  if (options.dryRun) {
@@ -7015,7 +8083,8 @@ async function runInit(options) {
7015
8083
  scripts: [],
7016
8084
  gitignoreEntries: [],
7017
8085
  dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
7018
- notes
8086
+ notes,
8087
+ outDir: answers.outDir
7019
8088
  };
7020
8089
  }
7021
8090
  if (present !== null && present !== preferredName && !options.force && !reused) {
@@ -7033,7 +8102,7 @@ async function runInit(options) {
7033
8102
  '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.'
7034
8103
  );
7035
8104
  }
7036
- if (fs12__namespace.existsSync(configPath) && !options.force && !reused) {
8105
+ if (fs14__namespace.existsSync(configPath) && !options.force && !reused) {
7037
8106
  throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
7038
8107
  }
7039
8108
  if (!reused) writeFileOrExplain(configPath, configText);
@@ -7051,14 +8120,15 @@ async function runInit(options) {
7051
8120
  scripts,
7052
8121
  gitignoreEntries: ignore.added,
7053
8122
  dependency,
7054
- notes
8123
+ notes,
8124
+ outDir: answers.outDir
7055
8125
  };
7056
8126
  }
7057
8127
  function describeTarget(cwd) {
7058
- const relative7 = path13__namespace.relative(process.cwd(), cwd);
7059
- if (relative7 === "") return "this directory";
7060
- if (relative7.startsWith("..") || path13__namespace.isAbsolute(relative7)) return cwd;
7061
- return relative7;
8128
+ const relative8 = path16__namespace.relative(process.cwd(), cwd);
8129
+ if (relative8 === "") return "this directory";
8130
+ if (relative8.startsWith("..") || path16__namespace.isAbsolute(relative8)) return cwd;
8131
+ return relative8;
7062
8132
  }
7063
8133
  function firstRunMessage(reason, cwd) {
7064
8134
  const where = describeTarget(cwd);
@@ -7081,7 +8151,7 @@ function planFirstRun(facts) {
7081
8151
  var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
7082
8152
  function isEffectivelyEmpty(cwd) {
7083
8153
  try {
7084
- return fs12__namespace.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
8154
+ return fs14__namespace.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
7085
8155
  } catch {
7086
8156
  return false;
7087
8157
  }
@@ -7089,19 +8159,132 @@ function isEffectivelyEmpty(cwd) {
7089
8159
  function hasPriorSnapshot(cwd, outDir) {
7090
8160
  let parsed;
7091
8161
  try {
7092
- parsed = JSON.parse(fs12__namespace.readFileSync(path13__namespace.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
8162
+ parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
7093
8163
  } catch {
7094
8164
  return false;
7095
8165
  }
7096
8166
  if (parsed === null || typeof parsed !== "object") return false;
7097
8167
  return isOurGeneratorName(parsed.generator);
7098
8168
  }
8169
+ var HOOK_MARKER = `# ${GENERATOR_NAME}: pre-commit snapshot check`;
8170
+ function shellQuote(value) {
8171
+ return value.replace(/[\\"`$]/g, (character) => `\\${character}`);
8172
+ }
8173
+ function hookScript(outDir) {
8174
+ const dir = shellQuote(outDir.replace(/\\/g, "/"));
8175
+ return `#!/bin/sh
8176
+ ${HOOK_MARKER}
8177
+ #
8178
+ # Refuse the commit when the snapshot is out of date with the tree.
8179
+ #
8180
+ # Remove this file to stop the check, or edit it \u2014 a hook this tool did not write is never
8181
+ # overwritten, and neither is one it did: run \`${GENERATOR_NAME} init --install-hook\` again
8182
+ # to restore the body below.
8183
+ set -e
8184
+
8185
+ SNAPSHOT_DIR="${dir}"
8186
+
8187
+ if [ ! -f "$SNAPSHOT_DIR/manifest.json" ]; then
8188
+ echo "${GENERATOR_NAME}: no snapshot in $SNAPSHOT_DIR, skipping the staleness check." >&2
8189
+ echo " Run \\"npx ${GENERATOR_NAME} generate\\" to create one." >&2
8190
+ exit 0
8191
+ fi
8192
+
8193
+ if ! npx --no-install ${GENERATOR_NAME} validate >/dev/null 2>&1; then
8194
+ echo "${GENERATOR_NAME}: the snapshot is out of date." >&2
8195
+ echo " Run \\"npx ${GENERATOR_NAME} generate\\", stage the change, and commit again." >&2
8196
+ echo " (npx --no-install is used on purpose: this hook must not install anything)" >&2
8197
+ exit 1
8198
+ fi
8199
+ `;
8200
+ }
8201
+ function gitDirectory(cwd) {
8202
+ const dotGit = path16__namespace.join(cwd, ".git");
8203
+ let stats;
8204
+ try {
8205
+ stats = fs14__namespace.statSync(dotGit);
8206
+ } catch {
8207
+ return null;
8208
+ }
8209
+ if (stats.isDirectory()) return dotGit;
8210
+ if (!stats.isFile()) return null;
8211
+ try {
8212
+ const contents = fs14__namespace.readFileSync(dotGit, "utf8");
8213
+ const match = /^gitdir:\s*(.+)$/m.exec(contents);
8214
+ if (!match) return null;
8215
+ const target = match[1];
8216
+ const resolved = path16__namespace.isAbsolute(target.trim()) ? target.trim() : path16__namespace.join(cwd, target.trim());
8217
+ return fs14__namespace.statSync(resolved).isDirectory() ? resolved : null;
8218
+ } catch {
8219
+ return null;
8220
+ }
8221
+ }
8222
+ function installPreCommitHook(cwd, outDir = "snapshot") {
8223
+ const gitDir = gitDirectory(cwd);
8224
+ if (gitDir === null) {
8225
+ return {
8226
+ path: null,
8227
+ written: false,
8228
+ notes: [
8229
+ "Not a git working tree (no .git directory or gitdir link), so no pre-commit hook was installed."
8230
+ ]
8231
+ };
8232
+ }
8233
+ const hooksDir = path16__namespace.join(gitDir, "hooks");
8234
+ const hookPath = path16__namespace.join(hooksDir, "pre-commit");
8235
+ try {
8236
+ fs14__namespace.mkdirSync(hooksDir, { recursive: true });
8237
+ } catch (err) {
8238
+ return {
8239
+ path: hookPath,
8240
+ written: false,
8241
+ notes: [`Could not create ${hooksDir}: ${err.message}`]
8242
+ };
8243
+ }
8244
+ let existing = null;
8245
+ try {
8246
+ existing = fs14__namespace.readFileSync(hookPath, "utf8");
8247
+ } catch {
8248
+ existing = null;
8249
+ }
8250
+ if (existing !== null && !existing.includes(HOOK_MARKER)) {
8251
+ return {
8252
+ path: hookPath,
8253
+ written: false,
8254
+ notes: [
8255
+ `${hookPath} already exists and was not written by ${GENERATOR_NAME}; it was left untouched.`,
8256
+ `Add \`npx ${GENERATOR_NAME} validate\` to it to get the same check.`
8257
+ ]
8258
+ };
8259
+ }
8260
+ try {
8261
+ fs14__namespace.writeFileSync(hookPath, hookScript(outDir), "utf8");
8262
+ fs14__namespace.chmodSync(hookPath, 493);
8263
+ } catch (err) {
8264
+ return {
8265
+ path: hookPath,
8266
+ written: false,
8267
+ notes: [`Could not write ${hookPath}: ${err.message}`]
8268
+ };
8269
+ }
8270
+ return {
8271
+ path: hookPath,
8272
+ written: true,
8273
+ notes: [
8274
+ existing === null ? `Installed ${hookPath}: commits are refused while the snapshot is out of date.` : `Updated ${hookPath} to the current ${GENERATOR_NAME} check.`
8275
+ ]
8276
+ };
8277
+ }
8278
+ function workerEntry(directory, isCjs) {
8279
+ const entry = path16__namespace.join(directory, isCjs ? "index.cjs" : "index.js");
8280
+ return { path: entry, url: url.pathToFileURL(entry).href };
8281
+ }
7099
8282
 
7100
8283
  // src/index.ts
7101
8284
  var SCHEMA_VERSION = "3.0.0";
7102
8285
  var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
7103
8286
  function sha256Hex2(input) {
7104
- return crypto__namespace.createHash("sha256").update(input).digest("hex");
8287
+ return crypto2__namespace.createHash("sha256").update(input).digest("hex");
7105
8288
  }
7106
8289
  var TS_EXTENSIONS = [
7107
8290
  ".ts",
@@ -7140,71 +8323,15 @@ function hasStructuralSummary(relativePath) {
7140
8323
  function bump(counts, key) {
7141
8324
  counts[key] = (counts[key] ?? 0) + 1;
7142
8325
  }
7143
- var RELATIVE_SPECIFIER = /^\.{1,2}\//;
7144
- var IMPORT_CANDIDATE_SUFFIXES = [
7145
- ".ts",
7146
- ".tsx",
7147
- ".mts",
7148
- ".cts",
7149
- ".js",
7150
- ".jsx",
7151
- ".mjs",
7152
- ".cjs",
7153
- "/index.ts",
7154
- "/index.tsx",
7155
- "/index.js",
7156
- "/index.mjs"
7157
- ];
7158
- var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
7159
- ".ts",
7160
- ".tsx",
7161
- ".mts",
7162
- ".cts",
7163
- ".js",
7164
- ".jsx",
7165
- ".mjs",
7166
- ".cjs"
7167
- ]);
7168
- function buildImportResolver(files) {
7169
- const byPath = /* @__PURE__ */ new Map();
7170
- const byPathFolded = /* @__PURE__ */ new Map();
7171
- const byStem = /* @__PURE__ */ new Map();
7172
- const byStemFolded = /* @__PURE__ */ new Map();
7173
- const remember = (map, key, value) => {
7174
- if (!map.has(key)) map.set(key, value);
7175
- };
7176
- for (const file of files) {
7177
- const relativePath = file.relativePath;
7178
- remember(byPath, relativePath, relativePath);
7179
- remember(byPathFolded, relativePath.toLowerCase(), relativePath);
7180
- const extension = path13__namespace.posix.extname(relativePath);
7181
- if (!SOURCE_EXTENSIONS.has(extension)) continue;
7182
- const stem = relativePath.slice(0, -extension.length);
7183
- remember(byStem, stem, relativePath);
7184
- remember(byStemFolded, stem.toLowerCase(), relativePath);
7185
- if (path13__namespace.posix.basename(stem) === "index") {
7186
- const directory = path13__namespace.posix.dirname(stem);
7187
- remember(byStem, directory, relativePath);
7188
- remember(byStemFolded, directory.toLowerCase(), relativePath);
8326
+ function retain(items, keep) {
8327
+ let write = 0;
8328
+ for (const item of items) {
8329
+ if (keep(item)) {
8330
+ items[write] = item;
8331
+ write += 1;
7189
8332
  }
7190
8333
  }
7191
- return { byPath, byPathFolded, byStem, byStemFolded };
7192
- }
7193
- function resolveSpecifier(fromFile, specifier, resolver) {
7194
- if (!RELATIVE_SPECIFIER.test(specifier)) return null;
7195
- const joined = path13__namespace.posix.normalize(
7196
- path13__namespace.posix.join(path13__namespace.posix.dirname(fromFile), specifier)
7197
- );
7198
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
7199
- const direct = exact(joined);
7200
- if (direct) return direct;
7201
- for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
7202
- const hit = exact(`${joined}${suffix}`);
7203
- if (hit) return hit;
7204
- }
7205
- const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
7206
- const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
7207
- return byStem(joined) ?? byStem(stem);
8334
+ items.length = write;
7208
8335
  }
7209
8336
  function buildIndexes(files) {
7210
8337
  const resolver = buildImportResolver(files);
@@ -7256,7 +8383,7 @@ function buildIndexes(files) {
7256
8383
  }))
7257
8384
  };
7258
8385
  }
7259
- function verifyInMemory(files, parts, maxPartBytes) {
8386
+ function verifyInMemory(files, parts) {
7260
8387
  const errors = [];
7261
8388
  const partNames = new Set(parts.map((part) => part.filename));
7262
8389
  for (const file of files) {
@@ -7272,8 +8399,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
7272
8399
  if (actualBytes !== part.metadata.byteSize) {
7273
8400
  errors.push(`${part.filename} byteSize ${part.metadata.byteSize} != actual ${actualBytes}.`);
7274
8401
  }
7275
- if (actualBytes > maxPartBytes) {
7276
- errors.push(`${part.filename} (${actualBytes} B) exceeds the part budget of ${maxPartBytes} B.`);
8402
+ const partBudget = part.metadata.budgetBytes;
8403
+ if (actualBytes > partBudget) {
8404
+ errors.push(
8405
+ `${part.filename} (${actualBytes} B) exceeds its part budget of ${partBudget} B.`
8406
+ );
7277
8407
  }
7278
8408
  if (part.metadata.includedFiles.length !== part.metadata.fileCount) {
7279
8409
  errors.push(`${part.filename} fileCount does not match its included files.`);
@@ -7286,11 +8416,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
7286
8416
  return errors;
7287
8417
  }
7288
8418
  function resolveSafeOutputDirs(config) {
7289
- const cwd = path13__namespace.resolve(config.cwd);
7290
- const outDir = path13__namespace.resolve(cwd, config.outDir);
7291
- const tmpDir = path13__namespace.resolve(cwd, config.tmpDir);
7292
- const root = path13__namespace.parse(cwd).root;
7293
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path13__namespace.sep}`);
8419
+ const cwd = path16__namespace.resolve(config.cwd);
8420
+ const outDir = path16__namespace.resolve(cwd, config.outDir);
8421
+ const tmpDir = path16__namespace.resolve(cwd, config.tmpDir);
8422
+ const root = path16__namespace.parse(cwd).root;
8423
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path16__namespace.sep}`);
7294
8424
  const reject = (reason) => {
7295
8425
  throw new Error(
7296
8426
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -7346,14 +8476,14 @@ function listEntries(dir) {
7346
8476
  const walk = (absolute, prefix) => {
7347
8477
  let children;
7348
8478
  try {
7349
- children = fs12__namespace.readdirSync(absolute, { withFileTypes: true });
8479
+ children = fs14__namespace.readdirSync(absolute, { withFileTypes: true });
7350
8480
  } catch {
7351
8481
  return;
7352
8482
  }
7353
8483
  for (const child of children) {
7354
- const relative7 = prefix === "" ? child.name : `${prefix}/${child.name}`;
7355
- entries.push(relative7);
7356
- if (child.isDirectory()) walk(path13__namespace.join(absolute, child.name), relative7);
8484
+ const relative8 = prefix === "" ? child.name : `${prefix}/${child.name}`;
8485
+ entries.push(relative8);
8486
+ if (child.isDirectory()) walk(path16__namespace.join(absolute, child.name), relative8);
7357
8487
  }
7358
8488
  };
7359
8489
  walk(dir, "");
@@ -7370,28 +8500,33 @@ function markerPayload(dir, state, id, createdAt) {
7370
8500
  pid: process.pid,
7371
8501
  entries: listEntries(dir)
7372
8502
  };
7373
- const manifestPath = path13__namespace.join(dir, "manifest.json");
7374
- if (fs12__namespace.existsSync(manifestPath)) {
7375
- marker2.manifestSha256 = sha256Hex2(fs12__namespace.readFileSync(manifestPath));
8503
+ const manifestPath = path16__namespace.join(dir, "manifest.json");
8504
+ if (fs14__namespace.existsSync(manifestPath)) {
8505
+ marker2.manifestSha256 = sha256Hex2(fs14__namespace.readFileSync(manifestPath));
7376
8506
  }
7377
8507
  return marker2;
7378
8508
  }
7379
8509
  function writeOwnershipMarker(dir, marker2) {
7380
- fs12__namespace.writeFileSync(
7381
- path13__namespace.join(dir, OWNERSHIP_MARKER),
8510
+ fs14__namespace.writeFileSync(
8511
+ path16__namespace.join(dir, OWNERSHIP_MARKER),
7382
8512
  `${JSON.stringify(marker2, null, 2)}
7383
8513
  `,
7384
8514
  "utf8"
7385
8515
  );
7386
8516
  }
8517
+ function markerBelongsTo(dir, id) {
8518
+ const evidence = readOwnershipMarker(dir);
8519
+ if (evidence.marker === null) return true;
8520
+ return evidence.marker.id === id;
8521
+ }
7387
8522
  function readOwnershipMarker(dir) {
7388
- const markerPath = path13__namespace.join(dir, OWNERSHIP_MARKER);
7389
- if (!fs12__namespace.existsSync(markerPath)) {
8523
+ const markerPath = path16__namespace.join(dir, OWNERSHIP_MARKER);
8524
+ if (!fs14__namespace.existsSync(markerPath)) {
7390
8525
  return { marker: null, problem: "it holds no .codebase-snapshot marker" };
7391
8526
  }
7392
8527
  let raw;
7393
8528
  try {
7394
- raw = fs12__namespace.readFileSync(markerPath, "utf8");
8529
+ raw = fs14__namespace.readFileSync(markerPath, "utf8");
7395
8530
  } catch {
7396
8531
  return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
7397
8532
  }
@@ -7459,11 +8594,11 @@ function assessSnapshotDirectory(dir) {
7459
8594
  marker: marker2
7460
8595
  };
7461
8596
  }
7462
- const manifestPath = path13__namespace.join(dir, "manifest.json");
7463
- if (fs12__namespace.existsSync(manifestPath)) {
8597
+ const manifestPath = path16__namespace.join(dir, "manifest.json");
8598
+ if (fs14__namespace.existsSync(manifestPath)) {
7464
8599
  let raw;
7465
8600
  try {
7466
- raw = fs12__namespace.readFileSync(manifestPath, "utf8");
8601
+ raw = fs14__namespace.readFileSync(manifestPath, "utf8");
7467
8602
  } catch {
7468
8603
  return {
7469
8604
  owned: false,
@@ -7545,13 +8680,13 @@ function containsNoFiles(dir) {
7545
8680
  const current = pending.pop();
7546
8681
  let entries;
7547
8682
  try {
7548
- entries = fs12__namespace.readdirSync(current, { withFileTypes: true });
8683
+ entries = fs14__namespace.readdirSync(current, { withFileTypes: true });
7549
8684
  } catch {
7550
8685
  return false;
7551
8686
  }
7552
8687
  for (const entry of entries) {
7553
8688
  if (entry.isDirectory()) {
7554
- pending.push(path13__namespace.join(current, entry.name));
8689
+ pending.push(path16__namespace.join(current, entry.name));
7555
8690
  } else if (!IGNORABLE_ENTRIES.has(entry.name)) {
7556
8691
  return false;
7557
8692
  }
@@ -7560,13 +8695,13 @@ function containsNoFiles(dir) {
7560
8695
  return true;
7561
8696
  }
7562
8697
  function assertReplaceable(target, label, secretRules) {
7563
- if (!fs12__namespace.existsSync(target)) return;
7564
- if (!fs12__namespace.statSync(target).isDirectory()) {
8698
+ if (!fs14__namespace.existsSync(target)) return;
8699
+ if (!fs14__namespace.statSync(target).isDirectory()) {
7565
8700
  throw new Error(
7566
8701
  `Refusing to write into ${label} ${target}: it exists but is not a directory.`
7567
8702
  );
7568
8703
  }
7569
- const entries = fs12__namespace.readdirSync(target);
8704
+ const entries = fs14__namespace.readdirSync(target);
7570
8705
  if (entries.length === 0) return;
7571
8706
  if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
7572
8707
  if (containsNoFiles(target)) return;
@@ -7586,18 +8721,18 @@ function assertReplaceable(target, label, secretRules) {
7586
8721
  );
7587
8722
  }
7588
8723
  function assertStagingRootUsable(tmpDir, secretRules) {
7589
- if (!fs12__namespace.existsSync(tmpDir)) return;
7590
- if (!fs12__namespace.statSync(tmpDir).isDirectory()) {
8724
+ if (!fs14__namespace.existsSync(tmpDir)) return;
8725
+ if (!fs14__namespace.statSync(tmpDir).isDirectory()) {
7591
8726
  throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
7592
8727
  }
7593
- const entries = fs12__namespace.readdirSync(tmpDir);
8728
+ const entries = fs14__namespace.readdirSync(tmpDir);
7594
8729
  if (entries.length === 0) return;
7595
8730
  const foreign = entries.filter((name) => {
7596
- const full = path13__namespace.join(tmpDir, name);
8731
+ const full = path16__namespace.join(tmpDir, name);
7597
8732
  if (!name.startsWith(RUN_DIR_PREFIX)) return true;
7598
8733
  let stats;
7599
8734
  try {
7600
- stats = fs12__namespace.statSync(full);
8735
+ stats = fs14__namespace.statSync(full);
7601
8736
  } catch {
7602
8737
  return true;
7603
8738
  }
@@ -7605,7 +8740,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7605
8740
  const { marker: marker2 } = readOwnershipMarker(full);
7606
8741
  if (marker2) return false;
7607
8742
  try {
7608
- return fs12__namespace.readdirSync(full).length > 0;
8743
+ return fs14__namespace.readdirSync(full).length > 0;
7609
8744
  } catch {
7610
8745
  return true;
7611
8746
  }
@@ -7620,13 +8755,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
7620
8755
  function cleanStaleRunDirs(tmpDir) {
7621
8756
  let entries;
7622
8757
  try {
7623
- entries = fs12__namespace.readdirSync(tmpDir);
8758
+ entries = fs14__namespace.readdirSync(tmpDir);
7624
8759
  } catch {
7625
8760
  return;
7626
8761
  }
7627
8762
  for (const name of entries) {
7628
8763
  if (!name.startsWith(RUN_DIR_PREFIX)) continue;
7629
- const full = path13__namespace.join(tmpDir, name);
8764
+ const full = path16__namespace.join(tmpDir, name);
7630
8765
  const { marker: marker2 } = readOwnershipMarker(full);
7631
8766
  if (marker2) {
7632
8767
  const created = Date.parse(marker2.createdAt);
@@ -7634,33 +8769,33 @@ function cleanStaleRunDirs(tmpDir) {
7634
8769
  const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
7635
8770
  if (aged || abandoned) {
7636
8771
  try {
7637
- fs12__namespace.rmSync(full, { recursive: true, force: true });
8772
+ fs14__namespace.rmSync(full, { recursive: true, force: true });
7638
8773
  } catch {
7639
8774
  }
7640
8775
  }
7641
8776
  continue;
7642
8777
  }
7643
8778
  try {
7644
- if (fs12__namespace.readdirSync(full).length === 0) fs12__namespace.rmSync(full, { recursive: true, force: true });
8779
+ if (fs14__namespace.readdirSync(full).length === 0) fs14__namespace.rmSync(full, { recursive: true, force: true });
7645
8780
  } catch {
7646
8781
  }
7647
8782
  }
7648
8783
  }
7649
8784
  function removeIfEmpty(dir) {
7650
8785
  try {
7651
- if (fs12__namespace.readdirSync(dir).length === 0) fs12__namespace.rmSync(dir, { recursive: true, force: true });
8786
+ if (fs14__namespace.readdirSync(dir).length === 0) fs14__namespace.rmSync(dir, { recursive: true, force: true });
7652
8787
  } catch {
7653
8788
  }
7654
8789
  }
7655
8790
  function ensureParentDirectory(target, label) {
7656
- const parent = path13__namespace.dirname(target);
7657
- if (fs12__namespace.existsSync(parent) && !fs12__namespace.statSync(parent).isDirectory()) {
8791
+ const parent = path16__namespace.dirname(target);
8792
+ if (fs14__namespace.existsSync(parent) && !fs14__namespace.statSync(parent).isDirectory()) {
7658
8793
  throw new Error(
7659
8794
  `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
7660
8795
  );
7661
8796
  }
7662
8797
  try {
7663
- fs12__namespace.mkdirSync(parent, { recursive: true });
8798
+ fs14__namespace.mkdirSync(parent, { recursive: true });
7664
8799
  } catch (err) {
7665
8800
  throw new Error(
7666
8801
  `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
@@ -7668,8 +8803,7 @@ function ensureParentDirectory(target, label) {
7668
8803
  }
7669
8804
  }
7670
8805
  function acquireRunLock(stagingRoot) {
7671
- const key = lockKeyFor(stagingRoot);
7672
- const lockPath = path13__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${key}.lock`);
8806
+ const lockPath = lockPathFor(stagingRoot);
7673
8807
  const payload = `${JSON.stringify({
7674
8808
  pid: process.pid,
7675
8809
  stagingRoot,
@@ -7678,22 +8812,22 @@ function acquireRunLock(stagingRoot) {
7678
8812
  `;
7679
8813
  for (let attempt = 0; attempt < 2; attempt += 1) {
7680
8814
  try {
7681
- const handle = fs12__namespace.openSync(lockPath, "wx");
8815
+ const handle = fs14__namespace.openSync(lockPath, "wx");
7682
8816
  try {
7683
- fs12__namespace.writeSync(handle, payload);
8817
+ fs14__namespace.writeSync(handle, payload);
7684
8818
  } finally {
7685
- fs12__namespace.closeSync(handle);
8819
+ fs14__namespace.closeSync(handle);
7686
8820
  }
7687
8821
  return () => {
7688
8822
  try {
7689
- fs12__namespace.rmSync(lockPath, { force: true });
8823
+ fs14__namespace.rmSync(lockPath, { force: true });
7690
8824
  } catch {
7691
8825
  }
7692
8826
  };
7693
8827
  } catch (err) {
7694
8828
  if (err.code !== "EEXIST") throw err;
7695
8829
  if (attempt === 0 && lockOwnerIsGone(lockPath)) {
7696
- fs12__namespace.rmSync(lockPath, { force: true });
8830
+ fs14__namespace.rmSync(lockPath, { force: true });
7697
8831
  continue;
7698
8832
  }
7699
8833
  throw new Error(
@@ -7706,16 +8840,19 @@ function acquireRunLock(stagingRoot) {
7706
8840
  function lockKeyFor(stagingRoot) {
7707
8841
  let key = stagingRoot;
7708
8842
  try {
7709
- key = fs12__namespace.realpathSync(stagingRoot);
8843
+ key = fs14__namespace.realpathSync(stagingRoot);
7710
8844
  } catch {
7711
8845
  }
7712
8846
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
7713
8847
  return sha256Hex2(key).slice(0, 16);
7714
8848
  }
8849
+ function lockPathFor(stagingRoot) {
8850
+ return path16__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${lockKeyFor(stagingRoot)}.lock`);
8851
+ }
7715
8852
  function lockOwnerIsGone(lockPath) {
7716
8853
  let raw;
7717
8854
  try {
7718
- raw = fs12__namespace.readFileSync(lockPath, "utf8");
8855
+ raw = fs14__namespace.readFileSync(lockPath, "utf8");
7719
8856
  } catch {
7720
8857
  return false;
7721
8858
  }
@@ -7723,9 +8860,9 @@ function lockOwnerIsGone(lockPath) {
7723
8860
  try {
7724
8861
  pid = JSON.parse(raw).pid;
7725
8862
  } catch {
7726
- return false;
8863
+ return lockFileIsAncient(lockPath);
7727
8864
  }
7728
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
8865
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return lockFileIsAncient(lockPath);
7729
8866
  try {
7730
8867
  process.kill(pid, 0);
7731
8868
  return false;
@@ -7733,14 +8870,21 @@ function lockOwnerIsGone(lockPath) {
7733
8870
  return err.code === "ESRCH";
7734
8871
  }
7735
8872
  }
8873
+ function lockFileIsAncient(lockPath) {
8874
+ try {
8875
+ return Date.now() - fs14__namespace.statSync(lockPath).mtimeMs > STALE_RUN_MS;
8876
+ } catch {
8877
+ return false;
8878
+ }
8879
+ }
7736
8880
  function writeManifestAtomically(manifestPath, manifest) {
7737
8881
  const tempPath = `${manifestPath}.${process.pid}.tmp`;
7738
- fs12__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
8882
+ fs14__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
7739
8883
  try {
7740
- fs12__namespace.renameSync(tempPath, manifestPath);
8884
+ fs14__namespace.renameSync(tempPath, manifestPath);
7741
8885
  } catch (err) {
7742
8886
  try {
7743
- fs12__namespace.rmSync(tempPath, { force: true });
8887
+ fs14__namespace.rmSync(tempPath, { force: true });
7744
8888
  } catch {
7745
8889
  }
7746
8890
  throw err;
@@ -7763,31 +8907,31 @@ function swapIntoPlace(stagingDir, outDir) {
7763
8907
  }
7764
8908
  function swapOnce(stagingDir, outDir) {
7765
8909
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
7766
- const hadPrevious = fs12__namespace.existsSync(outDir);
7767
- if (hadPrevious) fs12__namespace.renameSync(outDir, backupDir);
8910
+ const hadPrevious = fs14__namespace.existsSync(outDir);
8911
+ if (hadPrevious) fs14__namespace.renameSync(outDir, backupDir);
7768
8912
  try {
7769
8913
  try {
7770
- fs12__namespace.renameSync(stagingDir, outDir);
8914
+ fs14__namespace.renameSync(stagingDir, outDir);
7771
8915
  } catch (err) {
7772
8916
  if (err.code !== "EXDEV") throw err;
7773
8917
  const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
7774
8918
  try {
7775
- fs12__namespace.cpSync(stagingDir, partialDir, { recursive: true });
7776
- fs12__namespace.renameSync(partialDir, outDir);
7777
- fs12__namespace.rmSync(stagingDir, { recursive: true, force: true });
8919
+ fs14__namespace.cpSync(stagingDir, partialDir, { recursive: true });
8920
+ fs14__namespace.renameSync(partialDir, outDir);
8921
+ fs14__namespace.rmSync(stagingDir, { recursive: true, force: true });
7778
8922
  } catch (copyErr) {
7779
8923
  try {
7780
- fs12__namespace.rmSync(partialDir, { recursive: true, force: true });
8924
+ fs14__namespace.rmSync(partialDir, { recursive: true, force: true });
7781
8925
  } catch {
7782
8926
  }
7783
8927
  throw copyErr;
7784
8928
  }
7785
8929
  }
7786
- if (hadPrevious) fs12__namespace.rmSync(backupDir, { recursive: true, force: true });
8930
+ if (hadPrevious) fs14__namespace.rmSync(backupDir, { recursive: true, force: true });
7787
8931
  } catch (err) {
7788
- if (hadPrevious && fs12__namespace.existsSync(backupDir)) {
7789
- if (fs12__namespace.existsSync(outDir)) fs12__namespace.rmSync(outDir, { recursive: true, force: true });
7790
- fs12__namespace.renameSync(backupDir, outDir);
8932
+ if (hadPrevious && fs14__namespace.existsSync(backupDir)) {
8933
+ if (fs14__namespace.existsSync(outDir)) fs14__namespace.rmSync(outDir, { recursive: true, force: true });
8934
+ fs14__namespace.renameSync(backupDir, outDir);
7791
8935
  }
7792
8936
  throw err;
7793
8937
  }
@@ -7796,7 +8940,7 @@ function sleepSync(milliseconds) {
7796
8940
  const buffer = new Int32Array(new SharedArrayBuffer(4));
7797
8941
  Atomics.wait(buffer, 0, 0, milliseconds);
7798
8942
  }
7799
- function artifactWarnings(artifacts) {
8943
+ function artifactWarnings(artifacts, sarif) {
7800
8944
  const messages = [];
7801
8945
  if (!artifacts.manifest) {
7802
8946
  messages.push("manifest.json is disabled: `validate` and `report` cannot inspect this snapshot.");
@@ -7806,7 +8950,7 @@ function artifactWarnings(artifacts) {
7806
8950
  "parts are disabled but navigation.md is enabled: its links point at part files that were not written."
7807
8951
  );
7808
8952
  }
7809
- if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
8953
+ if (!sarif && ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
7810
8954
  messages.push(
7811
8955
  "every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
7812
8956
  );
@@ -7878,13 +9022,25 @@ function generateSnapshot(configInput, options) {
7878
9022
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
7879
9023
  includeSensitiveFiles: config.includeSensitiveFiles,
7880
9024
  secretRules: config.secretRules,
9025
+ discovery: config.discovery,
7881
9026
  // The output and staging directories this run is about to write, relative to the root. A
7882
9027
  // first run cannot have marked them yet, so the name alone keeps a run from recording its own
7883
9028
  // output — but only at these exact paths, not at every directory that shares the name.
7884
9029
  toolOwnedPaths: [config.outDir, config.tmpDir].map(
7885
- (directory) => path13__namespace.relative(config.cwd, path13__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
7886
- ).filter((relative7) => relative7.length > 0 && !relative7.startsWith(".."))
9030
+ (directory) => path16__namespace.relative(config.cwd, path16__namespace.resolve(config.cwd, directory)).replace(/\\/g, "/")
9031
+ ).filter((relative8) => relative8.length > 0 && !relative8.startsWith(".."))
7887
9032
  });
9033
+ const changedScope = config.changedSince !== void 0 || config.staged === true ? resolveChangedScope(config.cwd, {
9034
+ ...config.changedSince === void 0 ? {} : { changedSince: config.changedSince },
9035
+ ...config.staged === true ? { staged: true } : {}
9036
+ }) : null;
9037
+ const changedPaths = changedScope === null ? null : new Set(changedScope.paths);
9038
+ const unchangedPaths = [];
9039
+ const candidateFiles = changedPaths !== null && config.withDependents !== true ? collection.files.filter((relativePath) => {
9040
+ if (changedPaths.has(relativePath)) return true;
9041
+ unchangedPaths.push(relativePath);
9042
+ return false;
9043
+ }) : collection.files;
7888
9044
  const files = [];
7889
9045
  const monolithBodies = /* @__PURE__ */ new Map();
7890
9046
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
@@ -7895,6 +9051,7 @@ function generateSnapshot(configInput, options) {
7895
9051
  return files.some((file) => file.isPreviewTruncated && !file.isLarge);
7896
9052
  };
7897
9053
  const encodingNotes = [];
9054
+ const fileWarnings = [];
7898
9055
  const oversizeSummaryPaths = [];
7899
9056
  const previewSummaryPaths = [];
7900
9057
  const layerCounts = {};
@@ -7902,13 +9059,13 @@ function generateSnapshot(configInput, options) {
7902
9059
  let totalLines = 0;
7903
9060
  let totalSizeBytes = 0;
7904
9061
  let largeFileCount = 0;
7905
- for (const relativePath of collection.files) {
7906
- const absolutePath = path13__namespace.join(config.cwd, relativePath);
9062
+ for (const relativePath of candidateFiles) {
9063
+ const absolutePath = path16__namespace.join(config.cwd, relativePath);
7907
9064
  let stats;
7908
9065
  try {
7909
- stats = fs12__namespace.statSync(absolutePath);
9066
+ stats = fs14__namespace.statSync(absolutePath);
7910
9067
  if (!stats.isFile()) {
7911
- const nestedRepository = stats.isDirectory() && fs12__namespace.existsSync(path13__namespace.join(absolutePath, ".git"));
9068
+ const nestedRepository = stats.isDirectory() && fs14__namespace.existsSync(path16__namespace.join(absolutePath, ".git"));
7912
9069
  skipped.push(
7913
9070
  nestedRepository ? {
7914
9071
  path: relativePath,
@@ -7922,13 +9079,17 @@ function generateSnapshot(configInput, options) {
7922
9079
  skipped.push({ path: relativePath, reason: "read-error" });
7923
9080
  continue;
7924
9081
  }
7925
- if (isBinaryFile(absolutePath)) {
9082
+ if (isBinaryExtension(absolutePath)) {
9083
+ skipped.push({ path: relativePath, reason: "binary", detail: `Size: ${stats.size} B` });
9084
+ continue;
9085
+ }
9086
+ if (stats.size > BINARY_SCAN_BYTES && isBinaryFile(absolutePath)) {
7926
9087
  skipped.push({ path: relativePath, reason: "binary", detail: `Size: ${stats.size} B` });
7927
9088
  continue;
7928
9089
  }
7929
9090
  let buffer;
7930
9091
  try {
7931
- buffer = fs12__namespace.readFileSync(absolutePath);
9092
+ buffer = fs14__namespace.readFileSync(absolutePath);
7932
9093
  } catch {
7933
9094
  skipped.push({ path: relativePath, reason: "read-error" });
7934
9095
  continue;
@@ -7983,7 +9144,16 @@ function generateSnapshot(configInput, options) {
7983
9144
  let finalContent = redacted;
7984
9145
  let isPreviewTruncated = false;
7985
9146
  let includedLines = lineCount;
7986
- if (isLarge) {
9147
+ let inclusionMode = "full";
9148
+ if (config.skeleton === true && isSkeletonizable(relativePath)) {
9149
+ const skeleton = skeletonizeTypeScript(redacted, relativePath);
9150
+ if (skeleton.collapsed > 0) {
9151
+ finalContent = skeleton.text;
9152
+ inclusionMode = "signatures";
9153
+ includedLines = countLines(skeleton.text);
9154
+ }
9155
+ }
9156
+ if (isLarge && inclusionMode !== "signatures") {
7987
9157
  const preview = truncatePreview(
7988
9158
  redacted,
7989
9159
  config.previewHeadLines,
@@ -7992,8 +9162,9 @@ function generateSnapshot(configInput, options) {
7992
9162
  finalContent = preview.content;
7993
9163
  isPreviewTruncated = preview.isTruncated;
7994
9164
  includedLines = preview.includedLines;
9165
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
7995
9166
  }
7996
- if (config.includeMonolith && isPreviewTruncated) {
9167
+ if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
7997
9168
  monolithBodies.set(relativePath, redacted);
7998
9169
  }
7999
9170
  let summary;
@@ -8008,14 +9179,19 @@ function generateSnapshot(configInput, options) {
8008
9179
  summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
8009
9180
  summaryQuality = outcome?.quality;
8010
9181
  if (isSourceSummary(summary) && summary.parseErrors) {
8011
- warnings.push(
8012
- `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
8013
- );
9182
+ fileWarnings.push({
9183
+ path: relativePath,
9184
+ message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
9185
+ });
8014
9186
  }
8015
9187
  } catch (err) {
8016
- warnings.push(
8017
- `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
8018
- );
9188
+ fileWarnings.push({
9189
+ path: relativePath,
9190
+ message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
9191
+ /\s+/g,
9192
+ " "
9193
+ )
9194
+ });
8019
9195
  }
8020
9196
  }
8021
9197
  files.push({
@@ -8027,7 +9203,7 @@ function generateSnapshot(configInput, options) {
8027
9203
  layer: layer2,
8028
9204
  kind,
8029
9205
  language,
8030
- inclusionMode: isPreviewTruncated ? "preview" : "full",
9206
+ inclusionMode,
8031
9207
  isLarge,
8032
9208
  isPreviewTruncated,
8033
9209
  originalLines: lineCount,
@@ -8039,6 +9215,58 @@ function generateSnapshot(configInput, options) {
8039
9215
  content: finalContent
8040
9216
  });
8041
9217
  }
9218
+ if (changedScope !== null && changedPaths !== null && config.withDependents === true) {
9219
+ const keep = new Set(changedPaths);
9220
+ const resolver = buildImportResolver(files);
9221
+ for (const file of files) {
9222
+ if (!isSourceSummary(file.summary)) continue;
9223
+ if (importsChangedFile(file.relativePath, file.summary.imports, changedPaths, resolver)) {
9224
+ keep.add(file.relativePath);
9225
+ }
9226
+ }
9227
+ for (const file of files) {
9228
+ if (!keep.has(file.relativePath)) unchangedPaths.push(file.relativePath);
9229
+ }
9230
+ const kept = files.filter((file) => keep.has(file.relativePath));
9231
+ files.length = 0;
9232
+ files.push(...kept);
9233
+ totalLines = files.reduce((total, file) => total + file.lines, 0);
9234
+ totalSizeBytes = files.reduce((total, file) => total + file.sizeBytes, 0);
9235
+ largeFileCount = 0;
9236
+ for (const key of Object.keys(layerCounts)) delete layerCounts[key];
9237
+ for (const key of Object.keys(kindCounts)) delete kindCounts[key];
9238
+ for (const file of files) {
9239
+ bump(layerCounts, file.layer);
9240
+ bump(kindCounts, file.kind);
9241
+ if (file.isLarge) largeFileCount += 1;
9242
+ }
9243
+ retain(
9244
+ secretFindings,
9245
+ (finding) => finding.path === void 0 || keep.has(finding.path)
9246
+ );
9247
+ retain(encodingNotes, (note) => keep.has(note.path));
9248
+ retain(oversizeSummaryPaths, (path17) => keep.has(path17));
9249
+ retain(previewSummaryPaths, (path17) => keep.has(path17));
9250
+ retain(fileWarnings, (warning) => keep.has(warning.path));
9251
+ for (const path17 of [...monolithBodies.keys()]) {
9252
+ if (!keep.has(path17)) monolithBodies.delete(path17);
9253
+ }
9254
+ }
9255
+ if (changedScope !== null) {
9256
+ for (const relativePath of unchangedPaths) {
9257
+ skipped.push({
9258
+ path: relativePath,
9259
+ reason: "unchanged",
9260
+ detail: `not ${changedScope.detail}`
9261
+ });
9262
+ }
9263
+ }
9264
+ for (const warning of fileWarnings) warnings.push(warning.message);
9265
+ if (changedScope !== null && files.length === 0) {
9266
+ warnings.push(
9267
+ `No file is ${changedScope.detail}, so the snapshot is empty. Check the ref, or run without --changed-since to scan the whole repository.`
9268
+ );
9269
+ }
8042
9270
  const encodingBuckets = /* @__PURE__ */ new Map();
8043
9271
  for (const note of encodingNotes) {
8044
9272
  const bucket = encodingBuckets.get(note.encoding) ?? { paths: [], invalidBytes: 0 };
@@ -8089,13 +9317,8 @@ function generateSnapshot(configInput, options) {
8089
9317
  );
8090
9318
  }
8091
9319
  const parts = partitionByBudget(files, config, fingerprint);
8092
- const maxEffectiveBudget = Math.max(
8093
- config.maxPartBytes,
8094
- config.budgets.default,
8095
- ...Object.values(config.budgets.byLayer)
8096
- );
8097
9320
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
8098
- const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
9321
+ const validationErrors = verifyInMemory(files, parts);
8099
9322
  const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
8100
9323
  const maskName = (value) => maskPathText(value, config.secretRules);
8101
9324
  const manifest = {
@@ -8115,12 +9338,26 @@ function generateSnapshot(configInput, options) {
8115
9338
  config: {
8116
9339
  digest: configDigest(config),
8117
9340
  maxPartBytes: config.maxPartBytes,
9341
+ // The ceiling the user named, in their own unit. `maxPartBytes` above is what the
9342
+ // partitioner enforced; both are recorded so the manifest can explain the gap.
9343
+ ...config.maxPartTokens === void 0 ? {} : { maxPartTokens: config.maxPartTokens },
8118
9344
  largeFileBytes: config.largeFileBytes,
8119
9345
  previewHeadLines: config.previewHeadLines,
8120
9346
  previewTailLines: config.previewTailLines,
8121
9347
  includeMonolith: config.includeMonolith,
9348
+ // A ceiling that decides whether `full-codebase.*` is written at all, so it belongs
9349
+ // in the record of what the run enforced. Omitting it left `--max-monolith-bytes`
9350
+ // invisible in the manifest that is supposed to describe the run.
9351
+ maxMonolithBytes: config.maxMonolithBytes ?? null,
8122
9352
  includeSensitiveFiles: config.includeSensitiveFiles,
8123
9353
  discovery: config.discovery ?? "auto",
9354
+ // A changed-files scope decides which files are in the snapshot, so the manifest that
9355
+ // describes the run has to name it.
9356
+ ...config.changedSince === void 0 ? {} : { changedSince: config.changedSince },
9357
+ ...config.staged === true ? { staged: true } : {},
9358
+ ...config.withDependents === true ? { withDependents: true } : {},
9359
+ ...config.skeleton === true ? { skeleton: true } : {},
9360
+ ...config.sarif === true ? { sarif: true } : {},
8124
9361
  artifacts: { ...config.artifacts },
8125
9362
  include: [...config.include].map(maskName),
8126
9363
  preset: config.preset,
@@ -8152,6 +9389,9 @@ function generateSnapshot(configInput, options) {
8152
9389
  skippedFiles: skipped.length,
8153
9390
  totalSourceBytes: totalSizeBytes,
8154
9391
  totalContentBytes,
9392
+ // The parts are what a model is handed, so the estimate is over the parts rather than
9393
+ // over the sources: previews and the byte budget both shrink the number.
9394
+ estimatedTokens: parts.reduce((total, part) => total + part.metadata.estimatedTokens, 0),
8155
9395
  partCount: parts.length,
8156
9396
  warningCount: 0,
8157
9397
  redactedLines: 0
@@ -8190,7 +9430,9 @@ function generateSnapshot(configInput, options) {
8190
9430
  );
8191
9431
  manifest.totals.warningCount = manifest.warnings.length;
8192
9432
  }
8193
- for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
9433
+ for (const message of artifactWarnings(config.artifacts, config.sarif === true)) {
9434
+ manifest.warnings.push(message);
9435
+ }
8194
9436
  manifest.totals.warningCount = manifest.warnings.length;
8195
9437
  let projectTree = null;
8196
9438
  if (config.artifacts.projectTree) {
@@ -8203,6 +9445,13 @@ function generateSnapshot(configInput, options) {
8203
9445
  manifest.totals.warningCount = manifest.warnings.length;
8204
9446
  }
8205
9447
  }
9448
+ const sarifText = config.sarif === true ? generateSarifReport(manifest, secretFindings, config.secretRules, GENERATOR_VERSION) : null;
9449
+ if (sarifText !== null) {
9450
+ manifest.artifacts.sarif = {
9451
+ byteSize: Buffer.byteLength(sarifText, "utf8"),
9452
+ sha256: sha256Hex2(sarifText)
9453
+ };
9454
+ }
8206
9455
  let monolithJson = null;
8207
9456
  let monolithMarkdown = null;
8208
9457
  if (config.includeMonolith) {
@@ -8253,17 +9502,17 @@ function generateSnapshot(configInput, options) {
8253
9502
  const lockKeys = Array.from(/* @__PURE__ */ new Set([tmpDir, outDir])).sort();
8254
9503
  const releases = lockKeys.map((key) => acquireRunLock(key));
8255
9504
  const releaseLock = () => {
8256
- for (const release of releases.reverse()) release();
9505
+ for (const release of [...releases].reverse()) release();
8257
9506
  };
8258
9507
  let stagingDir = null;
8259
9508
  try {
8260
9509
  ensureParentDirectory(outDir, "outDir");
8261
- fs12__namespace.mkdirSync(tmpDir, { recursive: true });
9510
+ fs14__namespace.mkdirSync(tmpDir, { recursive: true });
8262
9511
  cleanStaleRunDirs(tmpDir);
8263
- const snapshotId = crypto__namespace.randomUUID();
9512
+ const snapshotId = crypto2__namespace.randomUUID();
8264
9513
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8265
- stagingDir = path13__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
8266
- fs12__namespace.mkdirSync(stagingDir);
9514
+ stagingDir = path16__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
9515
+ fs14__namespace.mkdirSync(stagingDir);
8267
9516
  writeOwnershipMarker(stagingDir, {
8268
9517
  generator: GENERATOR_NAME,
8269
9518
  version: GENERATOR_VERSION,
@@ -8273,41 +9522,44 @@ function generateSnapshot(configInput, options) {
8273
9522
  id: snapshotId,
8274
9523
  pid: process.pid
8275
9524
  });
8276
- const partsDir = path13__namespace.join(stagingDir, "parts");
8277
- const reportsDir = path13__namespace.join(stagingDir, "reports");
8278
- if (artifacts.parts) fs12__namespace.mkdirSync(partsDir, { recursive: true });
8279
- if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
8280
- fs12__namespace.mkdirSync(reportsDir, { recursive: true });
9525
+ const partsDir = path16__namespace.join(stagingDir, "parts");
9526
+ const reportsDir = path16__namespace.join(stagingDir, "reports");
9527
+ if (artifacts.parts) fs14__namespace.mkdirSync(partsDir, { recursive: true });
9528
+ if (artifacts.navigation || artifacts.projectTree || artifacts.audit || config.sarif === true) {
9529
+ fs14__namespace.mkdirSync(reportsDir, { recursive: true });
8281
9530
  }
8282
9531
  if (artifacts.parts) {
8283
9532
  for (const part of parts) {
8284
- fs12__namespace.writeFileSync(path13__namespace.join(partsDir, part.filename), part.markdown, "utf8");
9533
+ fs14__namespace.writeFileSync(path16__namespace.join(partsDir, part.filename), part.markdown, "utf8");
8285
9534
  }
8286
9535
  }
8287
9536
  if (projectTree !== null) {
8288
- fs12__namespace.writeFileSync(path13__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
9537
+ fs14__namespace.writeFileSync(path16__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
8289
9538
  }
8290
9539
  if (artifacts.navigation) {
8291
- fs12__namespace.writeFileSync(path13__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
9540
+ fs14__namespace.writeFileSync(path16__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
8292
9541
  }
8293
9542
  if (artifacts.audit) {
8294
- fs12__namespace.writeFileSync(
8295
- path13__namespace.join(reportsDir, "audit.md"),
9543
+ fs14__namespace.writeFileSync(
9544
+ path16__namespace.join(reportsDir, "audit.md"),
8296
9545
  generateAuditReport(manifest, secretFindings, config.secretRules),
8297
9546
  "utf8"
8298
9547
  );
8299
9548
  }
9549
+ if (sarifText !== null) {
9550
+ fs14__namespace.writeFileSync(path16__namespace.join(reportsDir, "audit.sarif"), sarifText, "utf8");
9551
+ }
8300
9552
  if (artifacts.readme) {
8301
- fs12__namespace.writeFileSync(path13__namespace.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
9553
+ fs14__namespace.writeFileSync(path16__namespace.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
8302
9554
  }
8303
9555
  if (artifacts.manifest) {
8304
- fs12__namespace.writeFileSync(path13__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
9556
+ fs14__namespace.writeFileSync(path16__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
8305
9557
  }
8306
9558
  if (monolithJson !== null) {
8307
- fs12__namespace.writeFileSync(path13__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
9559
+ fs14__namespace.writeFileSync(path16__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
8308
9560
  }
8309
9561
  if (monolithMarkdown !== null) {
8310
- fs12__namespace.writeFileSync(path13__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
9562
+ fs14__namespace.writeFileSync(path16__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
8311
9563
  }
8312
9564
  writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
8313
9565
  swapIntoPlace(stagingDir, outDir);
@@ -8315,31 +9567,41 @@ function generateSnapshot(configInput, options) {
8315
9567
  const warningsBeforeHook = manifest.warnings.length;
8316
9568
  const pendingHook = runAfterGenerate(config, manifest);
8317
9569
  const shipHookState = () => {
9570
+ if (!markerBelongsTo(outDir, snapshotId)) return;
8318
9571
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
8319
- writeManifestAtomically(path13__namespace.join(outDir, "manifest.json"), manifest);
9572
+ writeManifestAtomically(path16__namespace.join(outDir, "manifest.json"), manifest);
8320
9573
  }
8321
9574
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
8322
9575
  };
8323
9576
  shipHookState();
8324
9577
  if (pendingHook) {
8325
- pendingHook.catch((err) => {
8326
- const message = maskPathText(
8327
- err instanceof Error ? err.message : String(err),
8328
- config.secretRules
8329
- );
8330
- manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
8331
- manifest.totals.warningCount = manifest.warnings.length;
8332
- try {
8333
- shipHookState();
8334
- } catch {
9578
+ pendingHook.then(
9579
+ () => {
9580
+ try {
9581
+ shipHookState();
9582
+ } catch {
9583
+ }
9584
+ },
9585
+ (err) => {
9586
+ const message = maskPathText(
9587
+ err instanceof Error ? err.message : String(err),
9588
+ config.secretRules
9589
+ );
9590
+ manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
9591
+ manifest.totals.warningCount = manifest.warnings.length;
9592
+ try {
9593
+ shipHookState();
9594
+ } catch {
9595
+ }
8335
9596
  }
8336
- });
9597
+ );
9598
+ pendingHooks.set(outDir, pendingHook);
8337
9599
  }
8338
9600
  return manifest;
8339
9601
  } catch (err) {
8340
9602
  if (stagingDir !== null) {
8341
9603
  try {
8342
- fs12__namespace.rmSync(stagingDir, { recursive: true, force: true });
9604
+ fs14__namespace.rmSync(stagingDir, { recursive: true, force: true });
8343
9605
  } catch {
8344
9606
  }
8345
9607
  }
@@ -8349,19 +9611,31 @@ function generateSnapshot(configInput, options) {
8349
9611
  releaseLock();
8350
9612
  }
8351
9613
  }
9614
+ var pendingHooks = /* @__PURE__ */ new Map();
8352
9615
  async function generateSnapshotAsync(configInput, options) {
8353
- return generateSnapshot(configInput, options);
9616
+ const manifest = generateSnapshot(configInput, options);
9617
+ const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput ?? null);
9618
+ const { outDir } = resolveSafeOutputDirs(config);
9619
+ const pending = pendingHooks.get(outDir);
9620
+ if (pending !== void 0) {
9621
+ pendingHooks.delete(outDir);
9622
+ await pending.catch(() => void 0);
9623
+ }
9624
+ return manifest;
8354
9625
  }
8355
9626
 
8356
9627
  exports.ARTIFACT_NAMES = ARTIFACT_NAMES;
8357
9628
  exports.CHECK_INTERVAL_MS = CHECK_INTERVAL_MS;
9629
+ exports.CONFIG_KEYS = CONFIG_KEYS;
8358
9630
  exports.DEFAULT_ANSWERS = DEFAULT_ANSWERS;
8359
9631
  exports.DEFAULT_SECRET_PATTERNS = DEFAULT_SECRET_PATTERNS;
8360
9632
  exports.FAILURE_INTERVAL_MS = FAILURE_INTERVAL_MS;
8361
9633
  exports.GENERATOR_NAME = GENERATOR_NAME;
8362
9634
  exports.GENERATOR_VERSION = GENERATOR_VERSION;
9635
+ exports.HOOK_MARKER = HOOK_MARKER;
8363
9636
  exports.LANGUAGE_PROFILES = LANGUAGE_PROFILES;
8364
9637
  exports.MIN_MAX_PART_BYTES = MIN_MAX_PART_BYTES;
9638
+ exports.MIN_MAX_PART_TOKENS = MIN_MAX_PART_TOKENS;
8365
9639
  exports.PACKAGE_LATEST_URL = PACKAGE_LATEST_URL;
8366
9640
  exports.PRESETS = PRESETS;
8367
9641
  exports.PromptBack = PromptBack;
@@ -8371,31 +9645,47 @@ exports.SCHEMA_VERSION = SCHEMA_VERSION;
8371
9645
  exports.anchorForPath = anchorForPath;
8372
9646
  exports.assertReplaceable = assertReplaceable;
8373
9647
  exports.assertUsableConfigInput = assertUsableConfigInput;
9648
+ exports.buildImportResolver = buildImportResolver;
9649
+ exports.bytesForTokens = bytesForTokens;
8374
9650
  exports.cacheDirectory = cacheDirectory;
8375
9651
  exports.cacheFilePath = cacheFilePath;
8376
9652
  exports.checkForUpdate = checkForUpdate;
9653
+ exports.classifyKind = classifyKind;
9654
+ exports.classifyLayer = classifyLayer;
8377
9655
  exports.collectFiles = collectFiles;
8378
9656
  exports.compareManifests = compareManifests;
9657
+ exports.configDigest = configDigest;
8379
9658
  exports.countInvalidUtf8Bytes = countInvalidUtf8Bytes;
8380
9659
  exports.countLines = countLines;
8381
9660
  exports.decodeText = decodeText;
8382
9661
  exports.defineConfig = defineConfig;
8383
9662
  exports.detectLanguage = detectLanguage;
8384
9663
  exports.detectPreset = detectPreset;
9664
+ exports.displayPath = displayPath;
8385
9665
  exports.displayWidth = displayWidth;
9666
+ exports.enforceBudget = enforceBudget;
9667
+ exports.estimateTokens = estimateTokens;
8386
9668
  exports.explainPath = explainPath;
8387
9669
  exports.fenceForContent = fenceForContent;
8388
9670
  exports.fenceForText = fenceForText;
8389
9671
  exports.fetchLatestVersion = fetchLatestVersion;
9672
+ exports.findSecretInPath = findSecretInPath;
8390
9673
  exports.firstRunMessage = firstRunMessage;
9674
+ exports.formatTokens = formatTokens;
8391
9675
  exports.formatUpdateNotice = formatUpdateNotice;
8392
9676
  exports.generateProjectTree = generateProjectTree;
9677
+ exports.generateSarifReport = generateSarifReport;
8393
9678
  exports.generateSnapshot = generateSnapshot;
8394
9679
  exports.generateSnapshotAsync = generateSnapshotAsync;
9680
+ exports.getGitInfo = getGitInfo;
8395
9681
  exports.getPreset = getPreset;
8396
9682
  exports.globToRegExp = globToRegExp;
8397
9683
  exports.hasPriorSnapshot = hasPriorSnapshot;
9684
+ exports.importsChangedFile = importsChangedFile;
9685
+ exports.importsOfFile = importsOfFile;
9686
+ exports.importsOfText = importsOfText;
8398
9687
  exports.installKind = installKind;
9688
+ exports.installPreCommitHook = installPreCommitHook;
8399
9689
  exports.isBinaryBuffer = isBinaryBuffer;
8400
9690
  exports.isBinaryExtension = isBinaryExtension;
8401
9691
  exports.isBinaryFile = isBinaryFile;
@@ -8404,10 +9694,13 @@ exports.isInteractive = isInteractive;
8404
9694
  exports.isNewer = isNewer;
8405
9695
  exports.isResolvedConfig = isResolvedConfig;
8406
9696
  exports.isSensitivePath = isSensitivePath;
9697
+ exports.isSkeletonizable = isSkeletonizable;
9698
+ exports.isTsxFileName = isTsxFileName;
8407
9699
  exports.listPresetNames = listPresetNames;
8408
9700
  exports.loadConfigFile = loadConfigFile;
8409
9701
  exports.loadConfigFileDetailed = loadConfigFileDetailed;
8410
9702
  exports.loadManifest = loadManifest;
9703
+ exports.lockPathFor = lockPathFor;
8411
9704
  exports.markResolvedConfig = markResolvedConfig;
8412
9705
  exports.matchRule = matchRule;
8413
9706
  exports.matchesExclude = matchesExclude;
@@ -8420,11 +9713,14 @@ exports.renderConfig = renderConfig;
8420
9713
  exports.renderDiffText = renderDiffText;
8421
9714
  exports.renderExplainText = renderExplainText;
8422
9715
  exports.renumberParts = renumberParts;
9716
+ exports.resolveChangedScope = resolveChangedScope;
8423
9717
  exports.resolveConfig = resolveConfig;
8424
9718
  exports.resolveSafeOutputDirs = resolveSafeOutputDirs;
9719
+ exports.resolveSpecifier = resolveSpecifier;
8425
9720
  exports.runInit = runInit;
8426
9721
  exports.scanAndRedactSecrets = scanAndRedactSecrets;
8427
9722
  exports.shouldCheck = shouldCheck;
9723
+ exports.skeletonizeTypeScript = skeletonizeTypeScript;
8428
9724
  exports.stripJsonComments = stripJsonComments;
8429
9725
  exports.summarizeJson = summarizeJson;
8430
9726
  exports.summarizeMarkdown = summarizeMarkdown;
@@ -8435,6 +9731,5 @@ exports.truncatePreview = truncatePreview;
8435
9731
  exports.truncateToBytes = truncateToBytes;
8436
9732
  exports.updateCommand = updateCommand;
8437
9733
  exports.validateSnapshot = validateSnapshot;
9734
+ exports.workerEntry = workerEntry;
8438
9735
  exports.wrapLines = wrapLines;
8439
- //# sourceMappingURL=index.cjs.map
8440
- //# sourceMappingURL=index.cjs.map