codebase-snapshot 0.9.12 → 0.9.14

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.
@@ -9,11 +9,70 @@ import { createRequire } from 'module';
9
9
  import { pathToFileURL, fileURLToPath } from 'url';
10
10
  import * as childProcess from 'child_process';
11
11
  import { execFileSync } from 'child_process';
12
+ import * as http from 'http';
12
13
  import * as https from 'https';
13
14
  import * as readline from 'readline';
14
15
  import { Worker } from 'worker_threads';
15
16
 
16
17
  // src/index.ts
18
+ var hookStdoutScope = new AsyncLocalStorage();
19
+ var captureScope = new AsyncLocalStorage();
20
+ var installs = 0;
21
+ var underlying = null;
22
+ var router = null;
23
+ var asText = (chunk) => typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
24
+ var callbackIn = (rest) => {
25
+ for (const argument of rest) {
26
+ if (typeof argument === "function") return argument;
27
+ }
28
+ return void 0;
29
+ };
30
+ function installStdoutRouter() {
31
+ if (installs === 0) {
32
+ underlying = process.stdout.write;
33
+ const realStdout = underlying.bind(process.stdout);
34
+ const realStderr = process.stderr.write.bind(process.stderr);
35
+ router = ((...args) => {
36
+ if (hookStdoutScope.getStore() === true) return realStderr(...args);
37
+ const capture = captureScope.getStore();
38
+ if (capture !== void 0) {
39
+ capture.write(asText(args[0]));
40
+ const callback = callbackIn(args.slice(1));
41
+ if (callback) callback(null);
42
+ return true;
43
+ }
44
+ return realStdout(...args);
45
+ });
46
+ process.stdout.write = router;
47
+ }
48
+ installs += 1;
49
+ let released = false;
50
+ return () => {
51
+ if (released) return;
52
+ released = true;
53
+ installs -= 1;
54
+ if (installs === 0) {
55
+ if (process.stdout.write === router && underlying !== null) {
56
+ process.stdout.write = underlying;
57
+ }
58
+ underlying = null;
59
+ router = null;
60
+ }
61
+ };
62
+ }
63
+ async function runWithStdoutCapture(fn) {
64
+ let text2 = "";
65
+ const release = installStdoutRouter();
66
+ try {
67
+ const value = await captureScope.run(
68
+ { write: (chunk) => void (text2 += chunk) },
69
+ fn
70
+ );
71
+ return { value, text: text2 };
72
+ } finally {
73
+ release();
74
+ }
75
+ }
17
76
 
18
77
  // src/ast/profiles/shared.ts
19
78
  var MAX_SUMMARY_CHARS = 1e6;
@@ -69,7 +128,7 @@ function isCapitalizedName(name) {
69
128
  }
70
129
 
71
130
  // src/ast/profiles/cfamily.ts
72
- var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
131
+ var INCLUDE = /^#\s*include\s*([<"])([^>"]+)[>"]/;
73
132
  var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
74
133
  var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
75
134
  var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
@@ -88,7 +147,7 @@ var cfamilyProfile = {
88
147
  }
89
148
  const included = INCLUDE.exec(trimmed);
90
149
  if (included) {
91
- summary.imports.push(included[1]);
150
+ summary.imports.push(included[1] === '"' ? `./${included[2]}` : included[2]);
92
151
  return;
93
152
  }
94
153
  const used = USING.exec(trimmed);
@@ -492,7 +551,15 @@ var pythonProfile = {
492
551
  }
493
552
  const from = FROM_IMPORT.exec(trimmed);
494
553
  if (from) {
495
- summary.imports.push(from[1]);
554
+ const module = from[1];
555
+ if (/^\.+$/.test(module)) {
556
+ for (const name of from[2].split(",")) {
557
+ const imported2 = name.replace(/[()]/g, "").trim().split(/\s+as\s+/)[0].trim();
558
+ if (/^[A-Za-z_]\w*$/.test(imported2)) summary.imports.push(`${module}${imported2}`);
559
+ }
560
+ } else {
561
+ summary.imports.push(module);
562
+ }
496
563
  routePending = false;
497
564
  return;
498
565
  }
@@ -541,7 +608,8 @@ var pythonProfile = {
541
608
  };
542
609
 
543
610
  // src/ast/profiles/ruby.ts
544
- var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
611
+ var REQUIRE_RELATIVE = /^require_relative\s+["']([^"']+)["']/;
612
+ var REQUIRE = /^require\s+["']([^"']+)["']/;
545
613
  var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
546
614
  var METHOD2 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
547
615
  var rubyProfile = {
@@ -552,6 +620,11 @@ var rubyProfile = {
552
620
  eachLine(content, (line) => {
553
621
  const trimmed = line.trim();
554
622
  if (!trimmed || trimmed.startsWith("#")) return;
623
+ const relative8 = REQUIRE_RELATIVE.exec(trimmed);
624
+ if (relative8) {
625
+ summary.imports.push(`./${relative8[1]}`);
626
+ return;
627
+ }
555
628
  const required = REQUIRE.exec(trimmed);
556
629
  if (required) {
557
630
  summary.imports.push(required[1]);
@@ -748,13 +821,23 @@ function summarizeTypeScriptSource(relativePath, content) {
748
821
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
749
822
  summary.imports.push(node.moduleSpecifier.text);
750
823
  }
824
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression !== void 0 && ts.isStringLiteral(node.moduleReference.expression)) {
825
+ summary.imports.push(node.moduleReference.expression.text);
826
+ }
827
+ if (ts.isCallExpression(node) && node.arguments.length >= 1) {
828
+ const firstArgument = node.arguments[0];
829
+ if (firstArgument !== void 0 && ts.isStringLiteral(firstArgument)) {
830
+ const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
831
+ const isRequire = ts.isIdentifier(node.expression) && node.expression.text === "require";
832
+ if (isDynamicImport || isRequire) summary.imports.push(firstArgument.text);
833
+ }
834
+ }
751
835
  if (ts.isExportDeclaration(node)) {
752
836
  if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
753
837
  summary.imports.push(node.moduleSpecifier.text);
838
+ summary.reExports.push(node.moduleSpecifier.text);
754
839
  if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
755
840
  summary.exports.push(node.exportClause.name.text);
756
- } else if (!node.exportClause) {
757
- summary.reExports.push(node.moduleSpecifier.text);
758
841
  }
759
842
  }
760
843
  if (node.exportClause && ts.isNamedExports(node.exportClause)) {
@@ -846,7 +929,8 @@ function summarizeTypeScriptSource(relativePath, content) {
846
929
  }
847
930
 
848
931
  // src/ast/profiles/sfc.ts
849
- var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
932
+ var SCRIPT_BLOCK = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
933
+ var JSX_LANG = /\blang\s*=\s*["']?[tj]sx\b/i;
850
934
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
851
935
  function componentName(path17) {
852
936
  const base = path17.split("/").pop() ?? path17;
@@ -857,13 +941,15 @@ var sfcProfile = {
857
941
  extensions: [".vue", ".svelte", ".astro"],
858
942
  summarize(path17, content) {
859
943
  let script = "";
944
+ let usesJsx = path17.toLowerCase().endsWith(".astro");
860
945
  if (path17.toLowerCase().endsWith(".astro")) {
861
946
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
862
947
  if (frontmatter) script += `${frontmatter[1]}
863
948
  `;
864
949
  }
865
950
  for (const match of content.matchAll(SCRIPT_BLOCK)) {
866
- script += `${match[1]}
951
+ if (JSX_LANG.test(match[1] ?? "")) usesJsx = true;
952
+ script += `${match[2]}
867
953
  `;
868
954
  }
869
955
  const name = componentName(path17);
@@ -875,7 +961,7 @@ var sfcProfile = {
875
961
  }
876
962
  return { summary: finalizeSummary(summary2), quality: "heuristic" };
877
963
  }
878
- const summary = summarizeTypeScriptSource(`${name}.ts`, script);
964
+ const summary = summarizeTypeScriptSource(`${name}.${usesJsx ? "tsx" : "ts"}`, script);
879
965
  summary.reactComponents.push(name);
880
966
  summary.exports.push(name);
881
967
  return { summary: finalizeSummary(summary), quality: "ast" };
@@ -1067,124 +1153,421 @@ function summarizeWithProfile(path17, content) {
1067
1153
  const profile = profileForPath(path17);
1068
1154
  return profile ? profile.summarize(path17, content) : null;
1069
1155
  }
1070
-
1071
- // src/ast/structured.ts
1072
- function stripJsonComments(content) {
1073
- const text2 = content.replace(/^\uFEFF/, "");
1074
- let out = "";
1075
- let inString = false;
1076
- let inLineComment = false;
1077
- let inBlockComment = false;
1078
- for (let i = 0; i < text2.length; i++) {
1079
- const char = text2[i];
1080
- const next = text2[i + 1];
1081
- if (inLineComment) {
1082
- if (char === "\n") {
1083
- inLineComment = false;
1084
- out += char;
1085
- }
1156
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
1157
+ ".png",
1158
+ ".jpg",
1159
+ ".jpeg",
1160
+ ".gif",
1161
+ ".webp",
1162
+ ".avif",
1163
+ ".ico",
1164
+ ".bmp",
1165
+ ".tiff",
1166
+ ".pdf",
1167
+ ".zip",
1168
+ ".tar",
1169
+ ".gz",
1170
+ ".tgz",
1171
+ ".br",
1172
+ ".bz2",
1173
+ ".xz",
1174
+ ".zst",
1175
+ ".lz4",
1176
+ ".lzma",
1177
+ ".7z",
1178
+ ".rar",
1179
+ ".mp3",
1180
+ ".mp4",
1181
+ ".wav",
1182
+ ".ogg",
1183
+ ".webm",
1184
+ ".mov",
1185
+ ".avi",
1186
+ ".woff",
1187
+ ".woff2",
1188
+ ".ttf",
1189
+ ".otf",
1190
+ ".eot",
1191
+ ".wasm",
1192
+ ".node",
1193
+ ".exe",
1194
+ ".dll",
1195
+ ".dylib",
1196
+ ".so"
1197
+ ]);
1198
+ function isBinaryExtension(filePath) {
1199
+ const ext = path16.extname(filePath).toLowerCase();
1200
+ return BINARY_EXTENSIONS.has(ext);
1201
+ }
1202
+ function isBinaryBuffer(buffer) {
1203
+ return buffer.includes(0);
1204
+ }
1205
+ function countControlBytes(buffer) {
1206
+ let count = 0;
1207
+ for (let index = 0; index < buffer.length; index++) {
1208
+ const byte = buffer[index];
1209
+ if (byte < 32) {
1210
+ if (byte !== 9 && byte !== 10 && byte !== 13) count += 1;
1086
1211
  continue;
1087
1212
  }
1088
- if (inBlockComment) {
1089
- if (char === "*" && next === "/") {
1090
- inBlockComment = false;
1091
- i += 1;
1092
- }
1213
+ if (byte === 127) {
1214
+ count += 1;
1093
1215
  continue;
1094
1216
  }
1095
- if (inString) {
1096
- out += char;
1097
- if (char === "\\") {
1098
- if (next !== void 0) {
1099
- out += next;
1100
- i += 1;
1101
- }
1102
- } else if (char === '"') {
1103
- inString = false;
1217
+ if (byte === 194 && index + 1 < buffer.length) {
1218
+ const next = buffer[index + 1];
1219
+ if (next >= 128 && next <= 159) {
1220
+ count += 1;
1221
+ index += 1;
1104
1222
  }
1223
+ }
1224
+ }
1225
+ return count;
1226
+ }
1227
+ var CONTROL_BYTES_FLOOR = 4;
1228
+ function countInvalidUtf8Bytes(buffer) {
1229
+ let invalid = 0;
1230
+ let index = 0;
1231
+ const length = buffer.length;
1232
+ while (index < length) {
1233
+ const lead = buffer[index];
1234
+ if (lead < 128) {
1235
+ index += 1;
1105
1236
  continue;
1106
1237
  }
1107
- if (char === '"') {
1108
- inString = true;
1109
- out += char;
1238
+ let continuation = 0;
1239
+ let codePoint = 0;
1240
+ let minimum = 0;
1241
+ if ((lead & 224) === 192) {
1242
+ continuation = 1;
1243
+ codePoint = lead & 31;
1244
+ minimum = 128;
1245
+ } else if ((lead & 240) === 224) {
1246
+ continuation = 2;
1247
+ codePoint = lead & 15;
1248
+ minimum = 2048;
1249
+ } else if ((lead & 248) === 240) {
1250
+ continuation = 3;
1251
+ codePoint = lead & 7;
1252
+ minimum = 65536;
1253
+ } else {
1254
+ invalid += 1;
1255
+ index += 1;
1110
1256
  continue;
1111
1257
  }
1112
- if (char === "/" && next === "/") {
1113
- inLineComment = true;
1114
- i += 1;
1258
+ if (index + continuation >= length) {
1259
+ invalid += 1;
1260
+ index += 1;
1115
1261
  continue;
1116
1262
  }
1117
- if (char === "/" && next === "*") {
1118
- inBlockComment = true;
1119
- i += 1;
1263
+ let wellFormed = true;
1264
+ for (let offset = 1; offset <= continuation; offset++) {
1265
+ const next = buffer[index + offset];
1266
+ if ((next & 192) !== 128) {
1267
+ wellFormed = false;
1268
+ break;
1269
+ }
1270
+ codePoint = codePoint << 6 | next & 63;
1271
+ }
1272
+ const isSurrogate = codePoint >= 55296 && codePoint <= 57343;
1273
+ if (!wellFormed || codePoint < minimum || codePoint > 1114111 || isSurrogate) {
1274
+ invalid += 1;
1275
+ index += 1;
1120
1276
  continue;
1121
1277
  }
1122
- out += char;
1278
+ index += continuation + 1;
1123
1279
  }
1124
- return out;
1280
+ return invalid;
1125
1281
  }
1126
- function summarizeJson(content) {
1127
- try {
1128
- const parsed = JSON.parse(stripJsonComments(content));
1129
- if (Array.isArray(parsed)) {
1130
- const sample = parsed[0];
1131
- const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
1132
- return {
1133
- shape: "array",
1134
- itemCount: parsed.length,
1135
- keys: [],
1136
- sampleKeys
1137
- };
1282
+ function readBomEncoding(buffer) {
1283
+ if (buffer.length >= 2) {
1284
+ if (buffer[0] === 255 && buffer[1] === 254) return "utf-16le";
1285
+ if (buffer[0] === 254 && buffer[1] === 255) return "utf-16be";
1286
+ }
1287
+ return null;
1288
+ }
1289
+ function looksLikeUtf16(buffer) {
1290
+ if (buffer.length < 8 || buffer.length % 2 !== 0) return null;
1291
+ const sample = Math.min(buffer.length, 4096);
1292
+ let nulInLowByte = 0;
1293
+ let nulInHighByte = 0;
1294
+ const units = sample / 2;
1295
+ for (let index = 0; index < sample; index += 2) {
1296
+ if (buffer[index] === 0) nulInLowByte += 1;
1297
+ if (buffer[index + 1] === 0) nulInHighByte += 1;
1298
+ }
1299
+ if (nulInHighByte > units * 0.3 && nulInLowByte < units * 0.05) return "utf-16le";
1300
+ if (nulInLowByte > units * 0.3 && nulInHighByte < units * 0.05) return "utf-16be";
1301
+ return null;
1302
+ }
1303
+ function decodeUtf16(buffer, encoding) {
1304
+ const littleEndian = encoding === "utf-16le" ? buffer : Buffer.from(buffer.subarray(0, buffer.length - buffer.length % 2)).swap16();
1305
+ return new TextDecoder("utf-16le").decode(littleEndian);
1306
+ }
1307
+ function decodeText(buffer) {
1308
+ const bomEncoding = readBomEncoding(buffer);
1309
+ if (bomEncoding) {
1310
+ const text2 = decodeUtf16(buffer.subarray(2), bomEncoding);
1311
+ return { text: text2, encoding: bomEncoding, invalidBytes: 0, binary: false };
1312
+ }
1313
+ if (buffer.includes(0)) {
1314
+ const shape = looksLikeUtf16(buffer);
1315
+ if (shape) {
1316
+ return { text: decodeUtf16(buffer, shape), encoding: shape, invalidBytes: 0, binary: false };
1138
1317
  }
1139
- if (parsed && typeof parsed === "object") {
1140
- const keys = Object.keys(parsed).sort();
1318
+ const offset = buffer.indexOf(0);
1319
+ return {
1320
+ text: "",
1321
+ encoding: "utf-8",
1322
+ invalidBytes: 0,
1323
+ binary: true,
1324
+ binaryReason: `NUL byte at offset ${offset}`
1325
+ };
1326
+ }
1327
+ const invalidBytes = countInvalidUtf8Bytes(buffer);
1328
+ if (invalidBytes === 0) {
1329
+ const controlBytes = countControlBytes(buffer);
1330
+ if (controlBytes > CONTROL_BYTES_FLOOR && controlBytes * 100 > buffer.length) {
1141
1331
  return {
1142
- shape: "object",
1143
- itemCount: keys.length,
1144
- keys: keys.slice(0, 30),
1145
- sampleKeys: []
1332
+ text: "",
1333
+ encoding: "utf-8",
1334
+ invalidBytes: 0,
1335
+ binary: true,
1336
+ binaryReason: `${controlBytes} control byte(s) in ${buffer.length} B; valid UTF-8 but not text`
1146
1337
  };
1147
1338
  }
1148
1339
  return {
1149
- shape: typeof parsed,
1150
- itemCount: null,
1151
- keys: [],
1152
- sampleKeys: []
1340
+ text: new TextDecoder("utf-8").decode(buffer),
1341
+ encoding: "utf-8",
1342
+ invalidBytes: 0,
1343
+ binary: false
1153
1344
  };
1154
- } catch {
1345
+ }
1346
+ if (invalidBytes >= 32 && invalidBytes * 3 >= buffer.length) {
1155
1347
  return {
1156
- shape: "invalid-json",
1157
- itemCount: null,
1158
- keys: [],
1159
- sampleKeys: []
1348
+ text: "",
1349
+ encoding: "utf-8",
1350
+ invalidBytes,
1351
+ binary: true,
1352
+ binaryReason: `${invalidBytes} of ${buffer.length} bytes are not UTF-8; treated as binary`
1160
1353
  };
1161
1354
  }
1355
+ return {
1356
+ text: buffer.toString("utf8"),
1357
+ encoding: "utf-8-lossy",
1358
+ invalidBytes,
1359
+ binary: false
1360
+ };
1162
1361
  }
1163
- function summarizeMarkdown(content) {
1164
- const headings = [];
1165
- const frontmatterKeys = [];
1166
- const lines = content.replace(/^\uFEFF/, "").split("\n");
1167
- let inFrontmatter = false;
1168
- let fence = null;
1169
- let closesFrontmatter = false;
1170
- for (let index = 1; index < lines.length; index++) {
1171
- if (lines[index].trim() === "---") {
1172
- closesFrontmatter = true;
1173
- break;
1174
- }
1175
- }
1362
+ var BINARY_SCAN_BYTES = 8 * 1024 * 1024;
1363
+ var CHUNK = 64 * 1024;
1364
+ var SHAPE_SAMPLE_BYTES = 4096;
1365
+ function isBinaryFile(absolutePath) {
1366
+ if (isBinaryExtension(absolutePath)) return true;
1367
+ let fd;
1368
+ try {
1369
+ fd = fs14.openSync(absolutePath, "r");
1370
+ const buffer = Buffer.alloc(CHUNK);
1371
+ let sample;
1372
+ let position = 0;
1373
+ while (position < BINARY_SCAN_BYTES) {
1374
+ const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
1375
+ const bytesRead = fs14.readSync(fd, buffer, 0, wanted, position);
1376
+ if (bytesRead === 0) return false;
1377
+ const chunk = buffer.subarray(0, bytesRead);
1378
+ if (sample === void 0) {
1379
+ sample = Buffer.from(chunk.subarray(0, SHAPE_SAMPLE_BYTES));
1380
+ }
1381
+ if (chunk.includes(0)) {
1382
+ const shape = readBomEncoding(sample) ?? looksLikeUtf16(sample);
1383
+ return shape === null;
1384
+ }
1385
+ position += bytesRead;
1386
+ }
1387
+ return false;
1388
+ } catch {
1389
+ return false;
1390
+ } finally {
1391
+ if (fd !== void 0) {
1392
+ try {
1393
+ fs14.closeSync(fd);
1394
+ } catch {
1395
+ }
1396
+ }
1397
+ }
1398
+ }
1399
+
1400
+ // src/ast/imports.ts
1401
+ var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
1402
+ var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1403
+ var isTsSource = (relativePath) => {
1404
+ const lower = relativePath.toLowerCase();
1405
+ return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
1406
+ };
1407
+ function importsOfText(relativePath, text2) {
1408
+ if (Buffer.byteLength(text2, "utf8") > SUMMARY_MAX_SOURCE_BYTES) return [];
1409
+ if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
1410
+ return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
1411
+ }
1412
+ function importsOfFile(relativePath, absolutePath) {
1413
+ let buffer;
1414
+ try {
1415
+ const stats = fs14.statSync(absolutePath);
1416
+ if (!stats.isFile() || stats.size > SUMMARY_MAX_SOURCE_BYTES) return [];
1417
+ buffer = fs14.readFileSync(absolutePath);
1418
+ } catch {
1419
+ return [];
1420
+ }
1421
+ const decoded = decodeText(buffer);
1422
+ if (decoded.binary) return [];
1423
+ return importsOfText(relativePath, decoded.text);
1424
+ }
1425
+
1426
+ // src/ast/structured.ts
1427
+ function stripJsonComments(content) {
1428
+ const text2 = content.replace(/^\uFEFF/, "");
1429
+ const withoutComments = stripComments(text2);
1430
+ return stripTrailingCommas(withoutComments);
1431
+ }
1432
+ function stripComments(text2) {
1433
+ let out = "";
1434
+ let inString = false;
1435
+ let escaped = false;
1436
+ for (let index = 0; index < text2.length; index += 1) {
1437
+ const char = text2[index];
1438
+ if (inString) {
1439
+ out += char;
1440
+ if (escaped) escaped = false;
1441
+ else if (char === "\\") escaped = true;
1442
+ else if (char === '"') inString = false;
1443
+ continue;
1444
+ }
1445
+ if (char === '"') {
1446
+ inString = true;
1447
+ out += char;
1448
+ continue;
1449
+ }
1450
+ if (char === "/" && text2[index + 1] === "/") {
1451
+ while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
1452
+ index -= 1;
1453
+ continue;
1454
+ }
1455
+ if (char === "/" && text2[index + 1] === "*") {
1456
+ index += 2;
1457
+ while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
1458
+ if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
1459
+ index += 1;
1460
+ }
1461
+ index += 1;
1462
+ continue;
1463
+ }
1464
+ out += char;
1465
+ }
1466
+ return out;
1467
+ }
1468
+ function stripTrailingCommas(text2) {
1469
+ let out = "";
1470
+ let inString = false;
1471
+ let escaped = false;
1472
+ for (let index = 0; index < text2.length; index += 1) {
1473
+ const char = text2[index];
1474
+ if (inString) {
1475
+ out += char;
1476
+ if (escaped) escaped = false;
1477
+ else if (char === "\\") escaped = true;
1478
+ else if (char === '"') inString = false;
1479
+ continue;
1480
+ }
1481
+ if (char === '"') {
1482
+ inString = true;
1483
+ out += char;
1484
+ continue;
1485
+ }
1486
+ if (char === ",") {
1487
+ let next = index + 1;
1488
+ while (next < text2.length && /\s/.test(text2[next])) next += 1;
1489
+ if (text2[next] === "}" || text2[next] === "]") continue;
1490
+ }
1491
+ out += char;
1492
+ }
1493
+ return out;
1494
+ }
1495
+ function summarizeJson(content) {
1496
+ try {
1497
+ const parsed = JSON.parse(stripJsonComments(content));
1498
+ if (Array.isArray(parsed)) {
1499
+ const sample = parsed[0];
1500
+ const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
1501
+ return {
1502
+ shape: "array",
1503
+ itemCount: parsed.length,
1504
+ keys: [],
1505
+ sampleKeys
1506
+ };
1507
+ }
1508
+ if (parsed && typeof parsed === "object") {
1509
+ const keys = Object.keys(parsed).sort();
1510
+ return {
1511
+ shape: "object",
1512
+ itemCount: keys.length,
1513
+ keys: keys.slice(0, 30),
1514
+ sampleKeys: []
1515
+ };
1516
+ }
1517
+ return {
1518
+ shape: typeof parsed,
1519
+ itemCount: null,
1520
+ keys: [],
1521
+ sampleKeys: []
1522
+ };
1523
+ } catch {
1524
+ return {
1525
+ shape: "invalid-json",
1526
+ itemCount: null,
1527
+ keys: [],
1528
+ sampleKeys: []
1529
+ };
1530
+ }
1531
+ }
1532
+ function frontMatterEnd(lines) {
1533
+ if (lines.length < 3 || lines[0].trim() !== "---") return 0;
1534
+ for (let index = 1; index < lines.length; index += 1) {
1535
+ const line = lines[index];
1536
+ const candidate = line.trim();
1537
+ if (candidate === "---" || candidate === "...") {
1538
+ return index > 1 ? index : 0;
1539
+ }
1540
+ if (candidate === "") continue;
1541
+ if (/^#(\s|$)/.test(candidate)) continue;
1542
+ if (/^(`{3,}|~{3,})/.test(candidate)) return 0;
1543
+ if (/^#{1,6}\s+\S/.test(candidate)) return 0;
1544
+ const isYamlMapping = /^[A-Za-z0-9_.-]+\s*:(\s|$)/.test(candidate);
1545
+ const isListItem = /^-(?:(\s|$))/.test(candidate);
1546
+ const isIndented = /^[ \t]/.test(line);
1547
+ if (!isYamlMapping && !isListItem && !isIndented) return 0;
1548
+ }
1549
+ return 0;
1550
+ }
1551
+ function summarizeMarkdown(content) {
1552
+ const headings = [];
1553
+ const frontmatterKeys = [];
1554
+ const lines = content.replace(/^\uFEFF/, "").split(/\r\n|\n|\r/);
1555
+ let inFrontmatter = false;
1556
+ let fence = null;
1557
+ const frontMatterEndLine = frontMatterEnd(lines);
1176
1558
  for (let index = 0; index < lines.length; index++) {
1177
1559
  const line = lines[index];
1178
- if (index === 0 && line.trim() === "---" && closesFrontmatter) {
1560
+ if (index === 0 && frontMatterEndLine > 0) {
1179
1561
  inFrontmatter = true;
1180
1562
  continue;
1181
1563
  }
1182
1564
  if (inFrontmatter) {
1183
- if (line.trim() === "---") {
1565
+ const trimmedFrontmatter = line.trim();
1566
+ if (trimmedFrontmatter === "---" || trimmedFrontmatter === "...") {
1184
1567
  inFrontmatter = false;
1185
1568
  continue;
1186
1569
  }
1187
- const match = /^([a-zA-Z0-9_-]+):/.exec(line);
1570
+ const match = /^([A-Za-z0-9_.-]+)\s*:/.exec(line);
1188
1571
  if (match) frontmatterKeys.push(match[1]);
1189
1572
  continue;
1190
1573
  }
@@ -1217,24 +1600,39 @@ function hasCollapsibleBody(node) {
1217
1600
  return body !== void 0 && ts.isBlock(body);
1218
1601
  }
1219
1602
  function isTsxFileName(fileName) {
1220
- return fileName.toLowerCase().endsWith(".tsx");
1603
+ const lower = fileName.toLowerCase();
1604
+ return lower.endsWith(".tsx") || lower.endsWith(".jsx");
1605
+ }
1606
+ function scriptKindFor(fileName) {
1607
+ const lower = fileName.toLowerCase();
1608
+ if (lower.endsWith(".tsx")) return ts.ScriptKind.TSX;
1609
+ if (lower.endsWith(".jsx")) return ts.ScriptKind.JSX;
1610
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs")) {
1611
+ return ts.ScriptKind.JS;
1612
+ }
1613
+ return ts.ScriptKind.TS;
1221
1614
  }
1222
1615
  function skeletonizeTypeScript(source, fileName = "file.ts") {
1223
- const scriptKind = isTsxFileName(fileName) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
1224
1616
  const sourceFile = ts.createSourceFile(
1225
1617
  fileName,
1226
1618
  source,
1227
1619
  ts.ScriptTarget.Latest,
1228
- true,
1229
- scriptKind
1620
+ // `setParentNodes: false`. Nothing here reads `.parent` — the one `getStart` call is given the
1621
+ // source file explicitly, which is the reason that argument exists. Asking for parent pointers
1622
+ // makes the parser run a second full pass over every node in the file to set them, on top of
1623
+ // the summary parse this same file already pays for.
1624
+ false,
1625
+ scriptKindFor(fileName)
1230
1626
  );
1231
1627
  const spans = [];
1232
1628
  const visit = (node) => {
1233
1629
  if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && hasCollapsibleBody(node)) {
1234
1630
  const body = node.body;
1235
- const start = body.getStart(sourceFile);
1236
- const end = body.getEnd();
1237
- if (end > start) spans.push({ start, end });
1631
+ if (body.statements.length > 0) {
1632
+ const start = body.getStart(sourceFile);
1633
+ const end = body.getEnd();
1634
+ if (end > start) spans.push({ start, end });
1635
+ }
1238
1636
  }
1239
1637
  ts.forEachChild(node, visit);
1240
1638
  };
@@ -1246,16 +1644,21 @@ function skeletonizeTypeScript(source, fileName = "file.ts") {
1246
1644
  if (last && span.start < last.end) continue;
1247
1645
  outermost.push(span);
1248
1646
  }
1249
- let text2 = source;
1250
- for (let index = outermost.length - 1; index >= 0; index -= 1) {
1251
- const span = outermost[index];
1252
- text2 = `${text2.slice(0, span.start)}{ /* ... */ }${text2.slice(span.end)}`;
1647
+ const chunks = [];
1648
+ let cursor = 0;
1649
+ for (const span of outermost) {
1650
+ chunks.push(source.slice(cursor, span.start), "{ /* ... */ }");
1651
+ cursor = span.end;
1253
1652
  }
1254
- return { text: text2, collapsed: outermost.length };
1653
+ chunks.push(source.slice(cursor));
1654
+ return { text: chunks.join(""), collapsed: outermost.length };
1255
1655
  }
1256
1656
  function isSkeletonizable(filePath) {
1257
1657
  const lower = filePath.toLowerCase();
1258
- return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts");
1658
+ if (lower.endsWith(".d.ts") || lower.endsWith(".d.mts") || lower.endsWith(".d.cts")) {
1659
+ return false;
1660
+ }
1661
+ return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts") || lower.endsWith(".js") || lower.endsWith(".jsx") || lower.endsWith(".mjs") || lower.endsWith(".cjs");
1259
1662
  }
1260
1663
  var MANIFEST_FILES = [
1261
1664
  "package.json",
@@ -2493,9 +2896,18 @@ var DEFAULT_SECRET_PATTERNS = [
2493
2896
  // The unquoted twin for YAML, docker-compose and `.env`: those formats never
2494
2897
  // quote their values, so the rules above cannot see them. `valueGroup` keeps
2495
2898
  // `POSTGRES_PASSWORD:` in the output.
2899
+ //
2900
+ // This is the one unquoted rule without `NOT_AFTER_QUOTE`, and the lookbehind is a
2901
+ // string-literal guard rather than a precision rule: a credential assignment *inside* a
2902
+ // literal — `const dsn = "password=hunter2;host=db"`, `curl -d "password=hunter2"` — was
2903
+ // rejected before it could be considered, and a DSN is a credential in a string. What made the
2904
+ // lookbehind redundant is the shape of `UNQUOTED_LITERAL`: a value has to start with a word
2905
+ // character, so `["awsSecret:", "apiKey:"]` — the list of key names the lookbehind was added
2906
+ // for — cannot match here anyway. `redact.ts` still refuses a match whose separator is inside
2907
+ // the literal, which is the case that list actually is.
2496
2908
  rule: "credential-assignment-unquoted",
2497
2909
  pattern: new RegExp(
2498
- `${NOT_AFTER_QUOTE}${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2910
+ `${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2499
2911
  "i"
2500
2912
  ),
2501
2913
  valueGroup: 1
@@ -2530,6 +2942,27 @@ var DEFAULT_SECRET_PATTERNS = [
2530
2942
  pattern: /(?<!["'])(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*(?<![=!<>])[:=](?!=)\s*["']([^"'\s]{8,256})["']/i,
2531
2943
  valueGroup: 1
2532
2944
  },
2945
+ {
2946
+ // `${VAR:-fallback}` — a default value in a shell script, a `.env`, docker-compose or a
2947
+ // GitHub Actions workflow.
2948
+ //
2949
+ // This shape defeated every rule above it, and each refusal was individually reasonable:
2950
+ // the quoted rules need a quote after the separator, and the unquoted ones need the value to
2951
+ // begin with a word character — a fallback begins with `$`. So
2952
+ // `POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-MyProdPassword123!}` was emitted whole, `.env` files
2953
+ // and compose files included, which is where that idiom is the norm.
2954
+ //
2955
+ // The **fallback** is what gets masked, not the whole expansion: the variable name is the
2956
+ // readable context, exactly as `POSTGRES_PASSWORD:` is kept by the rule above. A single
2957
+ // interpolation with no operator (`${DB_PASSWORD}`) still does not match — there is nothing
2958
+ // between the braces to mask, and `isTemplateInterpolation` keeps such a value readable.
2959
+ rule: "interpolation-fallback-secret",
2960
+ pattern: new RegExp(
2961
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${NOT_AFTER_QUOTE}${STRONG_CREDENTIAL_NAME})\\s{0,8}${ASSIGN}\\s{0,8}["'\`]?\\$\\{[A-Za-z_][A-Za-z0-9_]*:?[-=?+]([^}"'\`\\r\\n]{2,512})\\}["'\`]?`,
2962
+ "i"
2963
+ ),
2964
+ valueGroup: 1
2965
+ },
2533
2966
  {
2534
2967
  // Covers `.env` / shell assignment syntax, which never quotes its values and
2535
2968
  // was therefore invisible to the rule above. Anchored to the line start and
@@ -2557,8 +2990,8 @@ var DEFAULT_SECRET_PATTERNS = [
2557
2990
  valueGroup: 1
2558
2991
  }
2559
2992
  ];
2560
- var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
2561
- var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
2993
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:(?!-----BEGIN)[\s\S])*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
2994
+ var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r\n|\n|\r)(?:(?:[ \t]*(?:Proc-Type|DEK-Info|Comment|Version):[^\r\n]*|[ \t]*)(?:\r\n|\n|\r))*(?:[ \t]*[A-Za-z0-9+/=]{8,}[ \t]*(?:\r\n|\n|\r|$))+/g;
2562
2995
 
2563
2996
  // src/security/redact.ts
2564
2997
  var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
@@ -2652,6 +3085,20 @@ function toGlobal(rule) {
2652
3085
  return new RegExp(rule.pattern.source, [...flags].join(""));
2653
3086
  }
2654
3087
  var LINE_BREAK = /(\r\n|\n|\r)/;
3088
+ var LINE_TERMINATOR = /\r\n|\n|\r/g;
3089
+ var COMPILED_RULES = /* @__PURE__ */ new WeakMap();
3090
+ function compiledRuleFor(rule) {
3091
+ let compiled = COMPILED_RULES.get(rule);
3092
+ if (compiled === void 0) {
3093
+ compiled = {
3094
+ rule: rule.rule,
3095
+ pattern: toGlobal(rule),
3096
+ ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
3097
+ };
3098
+ COMPILED_RULES.set(rule, compiled);
3099
+ }
3100
+ return compiled;
3101
+ }
2655
3102
  function scanAndRedactSecrets(content, rules, filePath) {
2656
3103
  const findings = [];
2657
3104
  let redacted = content;
@@ -2663,18 +3110,15 @@ function scanAndRedactSecrets(content, rules, filePath) {
2663
3110
  blockPattern.lastIndex += 1;
2664
3111
  continue;
2665
3112
  }
2666
- const line = redacted.slice(0, match.index).split("\n").length;
2667
- const newlines = (match[0].match(/\n/g) ?? []).length;
3113
+ const before = redacted.slice(0, match.index);
3114
+ const line = (before.match(LINE_TERMINATOR) ?? []).length + 1;
3115
+ const newlines = (match[0].match(LINE_TERMINATOR) ?? []).length;
2668
3116
  findings.push({ rules: ["private-key-block"], line, path: filePath });
2669
- redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
3117
+ redacted = before + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
2670
3118
  blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
2671
3119
  }
2672
3120
  }
2673
- const compiled = rules.map((rule) => ({
2674
- rule: rule.rule,
2675
- pattern: toGlobal(rule),
2676
- ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
2677
- }));
3121
+ const compiled = rules.map(compiledRuleFor);
2678
3122
  const segments = redacted.split(LINE_BREAK);
2679
3123
  let lineNumber = 1;
2680
3124
  for (let index = 0; index < segments.length; index += 2) {
@@ -2692,8 +3136,10 @@ function scanAndRedactSecrets(content, rules, filePath) {
2692
3136
  }
2693
3137
  const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
2694
3138
  if (group && isSelfNamingConstant(line, group[0], group[1])) continue;
2695
- if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index)) continue;
2696
- if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index)) {
3139
+ if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index, group?.[0])) {
3140
+ continue;
3141
+ }
3142
+ if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index, filePath)) {
2697
3143
  continue;
2698
3144
  }
2699
3145
  if (group && isTemplateInterpolation(line, group[0], group[1])) continue;
@@ -2726,14 +3172,20 @@ function expandOverToken(line, start, end) {
2726
3172
  while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
2727
3173
  return { start: left, end: right };
2728
3174
  }
2729
- function startsInsideStringLiteral(line, matchIndex) {
3175
+ function startsInsideStringLiteral(line, matchIndex, valueStart) {
2730
3176
  let runStart = matchIndex;
2731
3177
  while (runStart > 0 && /[A-Za-z0-9_-]/.test(line[runStart - 1])) runStart -= 1;
2732
3178
  const before = runStart > 0 ? line[runStart - 1] : "";
2733
- return before === '"' || before === "'" || before === "`";
3179
+ if (before !== '"' && before !== "'" && before !== "`") return false;
3180
+ if (valueStart === void 0) return true;
3181
+ return /["'`]/.test(line.slice(matchIndex, valueStart));
2734
3182
  }
2735
3183
  function isTemplateInterpolation(line, start, end) {
2736
- return line[start] === "$" && line[start + 1] === "{" && line[end - 1] === "}";
3184
+ if (line[start] !== "$" || line[start + 1] !== "{" || line[end - 1] !== "}") {
3185
+ return false;
3186
+ }
3187
+ const inner = line.slice(start + 2, end - 1).trim();
3188
+ return /^[A-Za-z_$][A-Za-z0-9_$.()[\]]*$/.test(inner);
2737
3189
  }
2738
3190
  function isSelfNamingConstant(line, start, end) {
2739
3191
  const value = line.slice(start, end);
@@ -2758,9 +3210,19 @@ function isUnquotedValue(line, start) {
2758
3210
  const before = start > 0 ? line[start - 1] : "";
2759
3211
  return before !== '"' && before !== "'" && before !== "`";
2760
3212
  }
2761
- var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|declare|export|using)\s+$/;
3213
+ var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|using)\s+$/;
3214
+ var ASSIGNMENT_FILE = /\.(?:sh|bash|zsh|ksh|fish|env|ya?ml|toml|ini|conf|cfg|properties|tf|tfvars|service)$/i;
3215
+ var ASSIGNMENT_BASENAME = /^(?:\.env(?:\.[^/]*)?|\.envrc|\.profile|\.bashrc|\.zshrc|\.bash_profile)$/i;
3216
+ function isAssignmentFile(filePath) {
3217
+ if (filePath === void 0) return false;
3218
+ const normalised = filePath.replace(/\\/g, "/");
3219
+ if (ASSIGNMENT_FILE.test(normalised)) return true;
3220
+ const base = normalised.slice(normalised.lastIndexOf("/") + 1);
3221
+ return ASSIGNMENT_BASENAME.test(base);
3222
+ }
2762
3223
  var DECLARATION_WINDOW = 40;
2763
- function isDeclaredBinding(line, matchIndex) {
3224
+ function isDeclaredBinding(line, matchIndex, filePath) {
3225
+ if (isAssignmentFile(filePath)) return false;
2764
3226
  let runStart = matchIndex;
2765
3227
  while (runStart > 0 && /[A-Za-z0-9_$]/.test(line[runStart - 1])) runStart -= 1;
2766
3228
  const from = Math.max(0, runStart - DECLARATION_WINDOW);
@@ -2802,6 +3264,15 @@ function readLinkTarget(linkPath, fallback) {
2802
3264
  return fallback;
2803
3265
  }
2804
3266
  }
3267
+ function brokenLinkRecord(linkPath, relativePath, rootReal) {
3268
+ const rawTarget = readLinkTarget(linkPath, linkPath);
3269
+ const resolvedFallback = path16.resolve(path16.dirname(linkPath), rawTarget);
3270
+ return isInside(rootReal, resolvedFallback) ? { path: relativePath, reason: "read-error", detail: "broken symlink" } : {
3271
+ path: relativePath,
3272
+ reason: "outside-root",
3273
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
3274
+ };
3275
+ }
2805
3276
  function isInside(root, candidate) {
2806
3277
  const relative8 = path16.relative(root, candidate);
2807
3278
  return relative8 === "" || !relative8.startsWith(`..${path16.sep}`) && relative8 !== ".." && !path16.isAbsolute(relative8);
@@ -2891,6 +3362,13 @@ function isSensitivePath(relativePath) {
2891
3362
  return matchesSensitiveList(posixPath, base);
2892
3363
  }
2893
3364
  var GLOBAL_PATTERNS = /* @__PURE__ */ new WeakMap();
3365
+ var GLOB_REGEX_CACHE = /* @__PURE__ */ new Map();
3366
+ var GITIGNORE_REGEX_CACHE = /* @__PURE__ */ new Map();
3367
+ var REGEX_CACHE_LIMIT = 4096;
3368
+ function rememberCompiled(cache, key, value) {
3369
+ if (cache.size >= REGEX_CACHE_LIMIT) cache.clear();
3370
+ cache.set(key, value);
3371
+ }
2894
3372
  function globalPattern(pattern) {
2895
3373
  let compiled = GLOBAL_PATTERNS.get(pattern);
2896
3374
  if (compiled === void 0) {
@@ -2901,8 +3379,8 @@ function globalPattern(pattern) {
2901
3379
  }
2902
3380
  var DEFAULT_PATH_RULES = [...DEFAULT_SECRET_PATTERNS];
2903
3381
  function pathScanRules(extraRules) {
2904
- if (extraRules === void 0 || extraRules.length === 0) return DEFAULT_PATH_RULES;
2905
- return [...DEFAULT_SECRET_PATTERNS, ...extraRules];
3382
+ if (extraRules === void 0) return DEFAULT_PATH_RULES;
3383
+ return extraRules;
2906
3384
  }
2907
3385
  function findSecretInPath(relativePath, extraRules) {
2908
3386
  const posixPath = relativePath.replace(/\\/g, "/");
@@ -2951,11 +3429,22 @@ function maskSkipPaths(records, extraRules) {
2951
3429
  });
2952
3430
  }
2953
3431
  function globToRegExp(glob) {
3432
+ const cached = GLOB_REGEX_CACHE.get(glob);
3433
+ if (cached !== void 0) return cached;
2954
3434
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
2955
3435
  const anchored = normalized.startsWith("/");
2956
3436
  const body = anchored ? normalized.slice(1) : normalized;
2957
3437
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
2958
- return new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
3438
+ let compiled;
3439
+ try {
3440
+ compiled = new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
3441
+ } catch (error) {
3442
+ throw new Error(
3443
+ `"${glob}" is not a valid glob: ${error.message}. A character class must be ascending, as in "[a-z]".`
3444
+ );
3445
+ }
3446
+ rememberCompiled(GLOB_REGEX_CACHE, glob, compiled);
3447
+ return compiled;
2959
3448
  }
2960
3449
  function globBodyToRegExp(body, allowBraces) {
2961
3450
  let out = "";
@@ -3067,6 +3556,14 @@ function escapeRegExpChar(character) {
3067
3556
  return /[\\^$.*+?()[\]{}|/]/.test(character) ? `\\${character}` : character;
3068
3557
  }
3069
3558
  function matchGitignorePattern(target, pattern) {
3559
+ let compiled = GITIGNORE_REGEX_CACHE.get(pattern);
3560
+ if (compiled === void 0) {
3561
+ compiled = compileGitignorePattern(pattern);
3562
+ rememberCompiled(GITIGNORE_REGEX_CACHE, pattern, compiled);
3563
+ }
3564
+ return compiled.test(target);
3565
+ }
3566
+ function compileGitignorePattern(pattern) {
3070
3567
  const anchored = pattern.startsWith("/");
3071
3568
  const body = anchored ? pattern.slice(1) : pattern;
3072
3569
  let source = "";
@@ -3115,7 +3612,11 @@ function matchGitignorePattern(target, pattern) {
3115
3612
  source += escapeRegExpChar(character);
3116
3613
  }
3117
3614
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
3118
- return new RegExp(`${prefix}${source}(?:/.*)?$`).test(target);
3615
+ try {
3616
+ return new RegExp(`${prefix}${source}(?:/.*)?$`);
3617
+ } catch {
3618
+ return /$^/;
3619
+ }
3119
3620
  }
3120
3621
  function matchesGitignoreRule(rule, scoped, isDirectory) {
3121
3622
  if (!rule.dirOnly) return matchGitignorePattern(scoped, rule.pattern);
@@ -3145,14 +3646,31 @@ function gitignoreVerdict(layers, relativePath, isDirectory) {
3145
3646
  }
3146
3647
  return verdict;
3147
3648
  }
3148
- function defaultIgnoreReason(relativePath) {
3649
+ var DEFAULT_TOOL_DIR_NAMES = ["snapshot"];
3650
+ function toolDirNamesFor(toolOwnedPaths) {
3651
+ const names = new Set(DEFAULT_TOOL_DIR_NAMES);
3652
+ for (const entry of toolOwnedPaths) {
3653
+ const base = entry.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop();
3654
+ if (base !== void 0 && base.length > 0) names.add(base);
3655
+ }
3656
+ return [...names];
3657
+ }
3658
+ function isToolLeftoverName(name, dirNames) {
3659
+ for (const dirName of dirNames) {
3660
+ if (name.startsWith(`${dirName}.backup-`) || name.startsWith(`${dirName}.partial-`)) {
3661
+ return true;
3662
+ }
3663
+ }
3664
+ return false;
3665
+ }
3666
+ function defaultIgnoreReason(relativePath, toolDirNames = DEFAULT_TOOL_DIR_NAMES) {
3149
3667
  const posixPath = relativePath.replace(/\\/g, "/");
3150
3668
  const parts = posixPath.split("/");
3151
3669
  for (let i = 0; i < parts.length - 1; i++) {
3152
3670
  if (DEFAULT_IGNORED_DIRS.includes(parts[i])) {
3153
3671
  return `built-in ignore: directory "${parts[i]}"`;
3154
3672
  }
3155
- if (parts[i].startsWith("snapshot.backup-")) {
3673
+ if (isToolLeftoverName(parts[i], toolDirNames)) {
3156
3674
  return `built-in ignore: leftover snapshot backup "${parts[i]}"`;
3157
3675
  }
3158
3676
  }
@@ -3160,6 +3678,9 @@ function defaultIgnoreReason(relativePath) {
3160
3678
  if (DEFAULT_IGNORED_FILES.includes(base)) {
3161
3679
  return `built-in ignore: filename "${base}"`;
3162
3680
  }
3681
+ if (isToolLeftoverName(base, toolDirNames)) {
3682
+ return `built-in ignore: leftover snapshot backup "${base}"`;
3683
+ }
3163
3684
  return null;
3164
3685
  }
3165
3686
  function listIgnoredPaths(cwd) {
@@ -3228,41 +3749,72 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
3228
3749
  }
3229
3750
  const reported = /* @__PURE__ */ new Set();
3230
3751
  const kept = [];
3752
+ const report = (linkedPath, record) => {
3753
+ if (reported.has(linkedPath)) return;
3754
+ reported.add(linkedPath);
3755
+ skipped.push(record);
3756
+ };
3231
3757
  for (const candidate of candidates) {
3232
3758
  const parts = candidate.split("/");
3233
- let linkedDir = null;
3759
+ let linkedPath = null;
3234
3760
  for (let depth = 1; depth <= parts.length; depth += 1) {
3235
3761
  const prefix = parts.slice(0, depth).join("/");
3236
3762
  if (isLink(path16.join(cwd, prefix))) {
3237
- linkedDir = prefix;
3763
+ linkedPath = prefix;
3238
3764
  break;
3239
3765
  }
3240
3766
  }
3241
- if (linkedDir === null) {
3767
+ if (linkedPath === null) {
3242
3768
  kept.push(candidate);
3243
3769
  continue;
3244
3770
  }
3245
- if (reported.has(linkedDir)) continue;
3246
- reported.add(linkedDir);
3247
- const absolute = path16.join(cwd, linkedDir);
3248
- let targetReal = absolute;
3771
+ const absolute = path16.join(cwd, linkedPath);
3772
+ const rawTarget = readLinkTarget(absolute, absolute);
3773
+ let targetReal = null;
3249
3774
  try {
3250
3775
  targetReal = fs14.realpathSync(absolute);
3251
3776
  } catch {
3777
+ targetReal = null;
3252
3778
  }
3253
- const target = readLinkTarget(absolute, targetReal);
3254
- const what = linkedDir === candidate ? "symbolic link" : "directory link";
3255
- skipped.push(
3256
- isInside(rootReal, targetReal) ? {
3257
- path: linkedDir,
3258
- reason: "symlink",
3259
- detail: `${what} not followed (target: ${target})`
3260
- } : {
3261
- path: linkedDir,
3779
+ if (targetReal === null) {
3780
+ report(linkedPath, brokenLinkRecord(absolute, linkedPath, rootReal));
3781
+ continue;
3782
+ }
3783
+ if (!isInside(rootReal, targetReal)) {
3784
+ report(linkedPath, {
3785
+ path: linkedPath,
3262
3786
  reason: "outside-root",
3263
- detail: `link target resolves outside the scanned root (target: ${target}); not followed`
3264
- }
3265
- );
3787
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
3788
+ });
3789
+ continue;
3790
+ }
3791
+ let stats = null;
3792
+ try {
3793
+ stats = fs14.statSync(absolute);
3794
+ } catch {
3795
+ stats = null;
3796
+ }
3797
+ if (stats === null) {
3798
+ report(linkedPath, { path: linkedPath, reason: "read-error", detail: "unreadable symlink" });
3799
+ continue;
3800
+ }
3801
+ if (linkedPath !== candidate || stats.isDirectory()) {
3802
+ report(linkedPath, {
3803
+ path: linkedPath,
3804
+ reason: "symlink",
3805
+ detail: `directory link not followed (target: ${rawTarget})`
3806
+ });
3807
+ continue;
3808
+ }
3809
+ if (!stats.isFile()) {
3810
+ report(linkedPath, {
3811
+ path: linkedPath,
3812
+ reason: "symlink",
3813
+ detail: `link to neither a file nor a directory (target: ${rawTarget})`
3814
+ });
3815
+ continue;
3816
+ }
3817
+ kept.push(candidate);
3266
3818
  }
3267
3819
  return kept;
3268
3820
  }
@@ -3309,6 +3861,7 @@ function collectFiles(cwd, options = {}) {
3309
3861
  (options.toolOwnedPaths ?? []).map((entry) => entry.replace(/\\/g, "/"))
3310
3862
  );
3311
3863
  const isToolOwned = makeToolOwnedDecider(cwd, [...toolOwnedPaths]);
3864
+ const toolDirNames = toolDirNamesFor(toolOwnedPaths);
3312
3865
  let candidates;
3313
3866
  const skipped = [];
3314
3867
  const discovery = options.discovery ?? "auto";
@@ -3337,7 +3890,7 @@ function collectFiles(cwd, options = {}) {
3337
3890
  }
3338
3891
  const files = candidates.filter((file) => {
3339
3892
  if (isToolOwned(file)) return false;
3340
- const ignored = defaultIgnoreReason(file);
3893
+ const ignored = defaultIgnoreReason(file, toolDirNames);
3341
3894
  if (ignored) {
3342
3895
  skipped.push({ path: file, reason: "default-ignore", detail: ignored });
3343
3896
  return false;
@@ -3392,6 +3945,7 @@ function rootRealFor(root) {
3392
3945
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
3393
3946
  const results = { files: [], skipped: [] };
3394
3947
  const stack = [];
3948
+ const toolDirNames = toolDirNamesFor(reservedPaths);
3395
3949
  const relativeToBase = (target) => path16.relative(baseDir, target).replace(/\\/g, "/") || ".";
3396
3950
  const enter = (current, layers) => {
3397
3951
  if (visited.has(current)) return false;
@@ -3436,11 +3990,11 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3436
3990
  });
3437
3991
  continue;
3438
3992
  }
3439
- if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
3993
+ if (DEFAULT_IGNORED_FILES.includes(entry.name) || isToolLeftoverName(entry.name, toolDirNames)) {
3440
3994
  results.skipped.push({
3441
3995
  path: relPath,
3442
3996
  reason: "default-ignore",
3443
- detail: "built-in ignore: filename"
3997
+ detail: isToolLeftoverName(entry.name, toolDirNames) ? `built-in ignore: leftover snapshot backup "${entry.name}" (not descended)` : "built-in ignore: filename"
3444
3998
  });
3445
3999
  continue;
3446
4000
  }
@@ -3461,7 +4015,7 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3461
4015
  try {
3462
4016
  targetReal = fs14.realpathSync(fullPath);
3463
4017
  } catch {
3464
- results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
4018
+ results.skipped.push(brokenLinkRecord(fullPath, relPath, rootReal));
3465
4019
  continue;
3466
4020
  }
3467
4021
  let stats;
@@ -3549,8 +4103,9 @@ function findConfigFile(cwd) {
3549
4103
  const pkgJsonPath = path16.join(cwd, "package.json");
3550
4104
  if (!fs14.existsSync(pkgJsonPath)) return null;
3551
4105
  try {
3552
- const parsed = JSON.parse(fs14.readFileSync(pkgJsonPath, "utf8"));
3553
- return parsed.snapshot === void 0 ? null : pkgJsonPath;
4106
+ const parsed = JSON.parse(stripJsonComments2(fs14.readFileSync(pkgJsonPath, "utf8")));
4107
+ const snapshot = parsed?.snapshot;
4108
+ return typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot) ? pkgJsonPath : null;
3554
4109
  } catch {
3555
4110
  return null;
3556
4111
  }
@@ -3600,15 +4155,19 @@ function normalizeBudgets(budgets, maxPartBytes) {
3600
4155
  }
3601
4156
  function normalizeResolvedConfig(config) {
3602
4157
  const discovery = isDiscoveryMode(config.discovery) ? config.discovery : "auto";
4158
+ const monolithWanted = config.includeMonolith === true || config.artifacts?.monolith === true;
4159
+ const artifacts = resolveArtifacts(
4160
+ config.artifacts,
4161
+ void 0,
4162
+ monolithWanted,
4163
+ monolithWanted
4164
+ );
3603
4165
  return markResolvedConfig({
3604
4166
  ...config,
3605
4167
  discovery,
4168
+ includeMonolith: artifacts.monolith,
3606
4169
  secretRules: compileSecretRules(config.secretRules),
3607
- artifacts: resolveArtifacts(
3608
- config.artifacts,
3609
- void 0,
3610
- config.includeMonolith === true
3611
- ),
4170
+ artifacts,
3612
4171
  include: resolveInclude(config.include),
3613
4172
  preset: typeof config.preset === "string" ? config.preset : "generic",
3614
4173
  presetExclude: [
@@ -3687,9 +4246,9 @@ function stripJsonComments2(raw) {
3687
4246
  }
3688
4247
  out += char;
3689
4248
  }
3690
- return stripTrailingCommas(out);
4249
+ return stripTrailingCommas2(out);
3691
4250
  }
3692
- function stripTrailingCommas(text2) {
4251
+ function stripTrailingCommas2(text2) {
3693
4252
  let out = "";
3694
4253
  let inString = false;
3695
4254
  let escaped = false;
@@ -3817,11 +4376,21 @@ async function importConfigModule(fullPath) {
3817
4376
  url.searchParams.set("codebaseSnapshotLoad", String(moduleConfigLoadToken));
3818
4377
  return await import(url.href);
3819
4378
  }
3820
- async function loadConfigFileDetailed(cwd) {
3821
- const warnings = [];
3822
- const errors = [];
3823
- for (const filename of CONFIG_FILENAMES) {
3824
- const fullPath = path16.join(cwd, filename);
4379
+ function attachConfigDir(config, filePath) {
4380
+ if (typeof config === "object" && config !== null) {
4381
+ Object.defineProperty(config, CONFIG_DIR_SYMBOL, {
4382
+ value: path16.dirname(filePath),
4383
+ enumerable: false,
4384
+ configurable: true
4385
+ });
4386
+ }
4387
+ return config;
4388
+ }
4389
+ async function loadConfigFileDetailed(cwd) {
4390
+ const warnings = [];
4391
+ const errors = [];
4392
+ for (const filename of CONFIG_FILENAMES) {
4393
+ const fullPath = path16.join(cwd, filename);
3825
4394
  if (!fs14.existsSync(fullPath)) continue;
3826
4395
  if (filename.endsWith(".json") || filename === ".snapshotrc") {
3827
4396
  try {
@@ -3832,7 +4401,7 @@ async function loadConfigFileDetailed(cwd) {
3832
4401
  );
3833
4402
  }
3834
4403
  assertKnownConfigKeys(config, fullPath);
3835
- return { config, file: fullPath, warnings, errors };
4404
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
3836
4405
  } catch (err) {
3837
4406
  const raw = err.message;
3838
4407
  const message = raw.includes(fullPath) ? raw : `${fullPath}: ${raw}`;
@@ -3851,7 +4420,7 @@ async function loadConfigFileDetailed(cwd) {
3851
4420
  const imported = await importConfigModule(fullPath);
3852
4421
  const hasDefault = Object.prototype.hasOwnProperty.call(imported, "default");
3853
4422
  const config = readConfigExport(hasDefault ? imported.default : imported, fullPath);
3854
- return { config, file: fullPath, warnings, errors };
4423
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
3855
4424
  } catch (err) {
3856
4425
  const message = `Could not load ${fullPath}: ${err.message}`;
3857
4426
  warnings.push(message);
@@ -3869,7 +4438,12 @@ async function loadConfigFileDetailed(cwd) {
3869
4438
  snapshotKey,
3870
4439
  `${pkgJsonPath} (the "snapshot" key)`
3871
4440
  );
3872
- return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
4441
+ return {
4442
+ config: attachConfigDir(snapshotKey, pkgJsonPath),
4443
+ file: pkgJsonPath,
4444
+ warnings,
4445
+ errors
4446
+ };
3873
4447
  }
3874
4448
  if (snapshotKey !== void 0) {
3875
4449
  const kind = Array.isArray(snapshotKey) ? "array" : typeof snapshotKey;
@@ -3887,13 +4461,6 @@ async function loadConfigFileDetailed(cwd) {
3887
4461
  }
3888
4462
  async function loadConfigFile(cwd) {
3889
4463
  const loaded = await loadConfigFileDetailed(cwd);
3890
- if (loaded.config && loaded.file && typeof loaded.config === "object") {
3891
- Object.defineProperty(loaded.config, CONFIG_DIR_SYMBOL, {
3892
- value: path16.dirname(loaded.file),
3893
- enumerable: false,
3894
- configurable: true
3895
- });
3896
- }
3897
4464
  if (loaded.errors.length > 0) {
3898
4465
  throw new Error(`The config file could not be read: ${loaded.errors.join("; ")}`);
3899
4466
  }
@@ -3922,7 +4489,7 @@ var DEFAULT_ARTIFACTS = {
3922
4489
  function isArtifactName(value) {
3923
4490
  return ARTIFACT_NAMES.includes(value);
3924
4491
  }
3925
- function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
4492
+ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride, only) {
3926
4493
  const artifacts = {
3927
4494
  ...DEFAULT_ARTIFACTS,
3928
4495
  // `includeMonolith` predates this selection and stays the switch for it.
@@ -3947,6 +4514,21 @@ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride)
3947
4514
  if (monolithOverride !== void 0) {
3948
4515
  artifacts.monolith = monolithOverride;
3949
4516
  }
4517
+ if (only !== void 0 && only.length > 0) {
4518
+ const keep = /* @__PURE__ */ new Set();
4519
+ for (const name of only) {
4520
+ if (!isArtifactName(name)) {
4521
+ throw new Error(
4522
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
4523
+ );
4524
+ }
4525
+ keep.add(name);
4526
+ }
4527
+ for (const name of ARTIFACT_NAMES) {
4528
+ artifacts[name] = keep.has(name);
4529
+ }
4530
+ return artifacts;
4531
+ }
3950
4532
  for (const name of skipped ?? []) {
3951
4533
  if (!isArtifactName(name)) {
3952
4534
  throw new Error(
@@ -4247,7 +4829,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
4247
4829
  userConfig?.artifacts,
4248
4830
  cliOptions?.skipArtifacts,
4249
4831
  includeMonolith,
4250
- cliMonolith
4832
+ cliMonolith,
4833
+ cliOptions?.onlyArtifacts
4251
4834
  );
4252
4835
  const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
4253
4836
  const maxPartBytes = boundedBytes(
@@ -4421,7 +5004,18 @@ function configDigest(config) {
4421
5004
  excludePatterns: config.excludePatterns,
4422
5005
  preset: config.preset,
4423
5006
  layersMode: config.layersMode,
4424
- budgets: config.budgets,
5007
+ // `budgets.byLayer` is a plain object, and `JSON.stringify` preserves insertion order — so
5008
+ // `{ docs, api }` and `{ api, docs }` hashed differently despite being the same settings, and
5009
+ // `validate` reported configuration drift for a snapshot nobody had changed. The keys are
5010
+ // canonicalised here; nothing else about the object is order-dependent.
5011
+ budgets: {
5012
+ default: config.budgets.default,
5013
+ byLayer: Object.fromEntries(
5014
+ Object.entries(config.budgets.byLayer).sort(
5015
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
5016
+ )
5017
+ )
5018
+ },
4425
5019
  // The matcher is what makes a layer a layer. Hashing only the names meant that
4426
5020
  // moving `{ name: "api", match: "src/api/**" }` to `"src/v2/**"` produced the same
4427
5021
  // digest, so `validate` reported "valid and up to date" for a snapshot whose files
@@ -4705,9 +5299,21 @@ function generateSarifReport(manifest, findings, rules, toolVersion) {
4705
5299
  }
4706
5300
 
4707
5301
  // src/partitioner/preview.ts
5302
+ var LINE_ENDINGS = /\r\n|\r/g;
5303
+ var normaliseLineEndings = (content) => content.replace(LINE_ENDINGS, "\n");
4708
5304
  function countLines(content) {
4709
5305
  if (content.length === 0) return 0;
4710
- return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
5306
+ let breaks = 0;
5307
+ for (let index = 0; index < content.length; index += 1) {
5308
+ const code = content.charCodeAt(index);
5309
+ if (code === 10) {
5310
+ breaks += 1;
5311
+ continue;
5312
+ }
5313
+ if (code === 13 && content.charCodeAt(index + 1) !== 10) breaks += 1;
5314
+ }
5315
+ const last = content.charCodeAt(content.length - 1);
5316
+ return last === 10 || last === 13 ? breaks : breaks + 1;
4711
5317
  }
4712
5318
  function truncateToBytes(content, maxBytes) {
4713
5319
  if (maxBytes <= 0) return { content: "", isTruncated: content.length > 0 };
@@ -4718,17 +5324,18 @@ function truncateToBytes(content, maxBytes) {
4718
5324
  return { content: slice, isTruncated: true };
4719
5325
  }
4720
5326
  function truncatePreview(content, headLines, tailLines) {
4721
- const lines = content.split("\n");
4722
- const total = countLines(content);
5327
+ const normalised = normaliseLineEndings(content);
5328
+ const lines = normalised.split("\n");
5329
+ const total = countLines(normalised);
4723
5330
  if (total <= headLines + tailLines) {
4724
5331
  return {
4725
- content,
5332
+ content: normalised,
4726
5333
  isTruncated: false,
4727
5334
  originalLines: total,
4728
5335
  includedLines: total
4729
5336
  };
4730
5337
  }
4731
- const window = content.endsWith("\n") ? lines.slice(0, -1) : lines;
5338
+ const window = normalised.endsWith("\n") ? lines.slice(0, -1) : lines;
4732
5339
  const head = window.slice(0, headLines);
4733
5340
  const tail = tailLines > 0 ? window.slice(window.length - tailLines) : [];
4734
5341
  const omitted = total - head.length - tail.length;
@@ -4782,23 +5389,55 @@ function truncateWithBudgetMarker(content, maxBytes, budget, originalLines) {
4782
5389
  };
4783
5390
  }
4784
5391
  function partitionFiles(files, maxPartBytes, contentFingerprint) {
4785
- if (files.length === 0) return [];
4786
- const budget = Math.max(MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES, maxPartBytes);
4787
- const sectionBudget = budget - HEADER_RESERVE_BYTES;
4788
- const prepared = files.map((file) => prepareSection(file, sectionBudget));
4789
- let groups = greedyGroups(prepared, budget);
5392
+ return partitionFilesForBuckets([{ files, budgetBytes: maxPartBytes }], contentFingerprint);
5393
+ }
5394
+ function partitionFilesForBuckets(buckets, contentFingerprint) {
5395
+ const groups = [];
5396
+ for (const bucket of buckets) {
5397
+ if (bucket.files.length === 0) continue;
5398
+ const budget = Math.max(
5399
+ MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES,
5400
+ bucket.budgetBytes
5401
+ );
5402
+ const sectionBudget = budget - HEADER_RESERVE_BYTES;
5403
+ const prepared = bucket.files.map((file) => prepareSection(file, sectionBudget));
5404
+ for (const sections of greedyGroups(prepared, budget)) groups.push({ sections, budget });
5405
+ }
5406
+ if (groups.length === 0) return [];
5407
+ const measurements = /* @__PURE__ */ new Map();
5408
+ const widthOf = (total) => Math.max(3, String(total).length);
5409
+ const measure = (group, index, total) => {
5410
+ const cached = measurements.get(group);
5411
+ const width = widthOf(total);
5412
+ if (cached !== void 0 && cached.index === index && cached.width === width) {
5413
+ return cached.size;
5414
+ }
5415
+ const { content } = assembleGroup(group, index, total, contentFingerprint);
5416
+ const size2 = Buffer.byteLength(content, "utf8");
5417
+ measurements.set(group, { index, width, size: size2 });
5418
+ return size2;
5419
+ };
4790
5420
  for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
4791
- const rendered = renderGroups(groups, budget, contentFingerprint);
4792
- const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
4793
- if (oversized === -1) return rendered;
5421
+ const total = groups.length;
5422
+ let oversized = -1;
5423
+ for (let index = 0; index < total; index += 1) {
5424
+ const group2 = groups[index];
5425
+ if (measure(group2, index, total) > group2.budget) {
5426
+ oversized = index;
5427
+ break;
5428
+ }
5429
+ }
5430
+ if (oversized === -1) return renderGroups(groups, contentFingerprint);
4794
5431
  const group = groups[oversized];
4795
- if (group.length > 1) {
4796
- const moved = group.pop();
4797
- groups.splice(oversized + 1, 0, [moved]);
5432
+ if (group.sections.length > 1) {
5433
+ const moved = group.sections.pop();
5434
+ groups.splice(oversized + 1, 0, { sections: [moved], budget: group.budget });
5435
+ measurements.delete(group);
4798
5436
  continue;
4799
5437
  }
4800
- const [section] = group;
4801
- const overflow = rendered[oversized].metadata.byteSize - budget;
5438
+ const [section] = group.sections;
5439
+ if (section === void 0) return renderGroups(groups, contentFingerprint);
5440
+ const overflow = measure(group, oversized, total) - group.budget;
4802
5441
  const currentContent = section.file.content ?? "";
4803
5442
  const shrinkTo = Math.max(
4804
5443
  0,
@@ -4807,26 +5446,33 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
4807
5446
  const shrunk = truncateWithBudgetMarker(
4808
5447
  currentContent,
4809
5448
  shrinkTo,
4810
- budget,
5449
+ group.budget,
4811
5450
  section.file.originalLines
4812
5451
  );
4813
5452
  if (shrunk.isTruncated) {
4814
5453
  applyTruncatedContent(section.file, shrunk.content);
4815
5454
  section.text = renderFileSection(section.file);
4816
5455
  section.byteSize = Buffer.byteLength(section.text, "utf8");
5456
+ measurements.delete(group);
4817
5457
  continue;
4818
5458
  }
5459
+ const rendered = renderGroups(groups, contentFingerprint);
4819
5460
  const marker2 = TRUNCATION_MARKER;
4820
- assertBudgetHoldsMarker(budget);
5461
+ assertBudgetHoldsMarker(group.budget);
4821
5462
  const target = Math.max(
4822
5463
  0,
4823
- budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
5464
+ group.budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
4824
5465
  );
4825
5466
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
4826
- rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
5467
+ rendered[oversized] = rehashPart(
5468
+ rendered[oversized],
5469
+ clipped.content + marker2
5470
+ );
4827
5471
  return rendered;
4828
5472
  }
4829
- return enforceBudget(renderGroups(groups, budget, contentFingerprint), budget);
5473
+ return renderGroups(groups, contentFingerprint).flatMap(
5474
+ (part) => enforceBudget([part], part.metadata.budgetBytes)
5475
+ );
4830
5476
  }
4831
5477
  var TRUNCATION_MARKER = "\n... [part truncated: cannot fit budget] ...\n";
4832
5478
  function assertBudgetHoldsMarker(budget) {
@@ -4911,7 +5557,7 @@ function renumberParts(parts, contentFingerprint) {
4911
5557
  (part) => enforceBudget([part], part.metadata.budgetBytes)[0] ?? part
4912
5558
  );
4913
5559
  }
4914
- var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
5560
+ var SECTION_TRUNCATION_NOTICE = "... [section truncated: the fixed metadata and a code fence did not fit the part budget] ...";
4915
5561
  var METADATA_VALUE_MAX = 200;
4916
5562
  var DISPLAY_PATH_MAX = 300;
4917
5563
  function capValue(value, max) {
@@ -4953,13 +5599,57 @@ function prepareSection(file, sectionBudget) {
4953
5599
  shrinkBody();
4954
5600
  }
4955
5601
  if (byteSize > sectionBudget) {
4956
- const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
4957
- const truncated = truncateToBytes(text2, Math.max(0, target));
4958
- text2 = truncated.content + SECTION_TRUNCATION_NOTICE;
5602
+ applyTruncatedContent(file, SECTION_TRUNCATION_NOTICE);
5603
+ file.summary = void 0;
5604
+ file.summaryOmitted = true;
5605
+ text2 = renderFileSection(file);
4959
5606
  byteSize = Buffer.byteLength(text2, "utf8");
5607
+ if (byteSize > sectionBudget) {
5608
+ text2 = truncateRenderedSection(text2, sectionBudget, fenceForContent(file.content ?? ""));
5609
+ byteSize = Buffer.byteLength(text2, "utf8");
5610
+ }
4960
5611
  }
4961
5612
  return { file, text: text2, byteSize };
4962
5613
  }
5614
+ function truncateRenderedSection(text2, sectionBudget, fence) {
5615
+ const closing = `
5616
+ ${SECTION_TRUNCATION_NOTICE}
5617
+ ${fence}
5618
+ `;
5619
+ const bodyRoom = sectionBudget - Buffer.byteLength(closing, "utf8");
5620
+ if (bodyRoom > 0) {
5621
+ const cut2 = truncateToBytes(text2, bodyRoom).content;
5622
+ const lastBreak2 = cut2.lastIndexOf("\n");
5623
+ const kept = lastBreak2 >= 0 ? cut2.slice(0, lastBreak2 + 1) : "";
5624
+ if (fenceIsOpen(kept, fence)) return `${kept}${closing}`;
5625
+ }
5626
+ const probe = text2.slice(0, 512);
5627
+ const emptyFence = fenceForText(probe.slice(0, Math.max(0, probe.lastIndexOf("\n") + 1)), 4);
5628
+ const block = `
5629
+ ${emptyFence}
5630
+ ${SECTION_TRUNCATION_NOTICE}
5631
+ ${emptyFence}
5632
+ `;
5633
+ const prefixRoom = sectionBudget - Buffer.byteLength(block, "utf8");
5634
+ const cut = prefixRoom > 0 ? truncateToBytes(text2, prefixRoom).content : "";
5635
+ const lastBreak = cut.lastIndexOf("\n");
5636
+ const prefix = lastBreak >= 0 ? cut.slice(0, lastBreak + 1) : "";
5637
+ return `${dropTrailingOpenFence(prefix, fence)}${block}`;
5638
+ }
5639
+ function dropTrailingOpenFence(text2, fence) {
5640
+ if (!fenceIsOpen(text2, fence)) return text2;
5641
+ const lines = text2.split("\n");
5642
+ const opening = lines.findIndex((line) => line.startsWith(fence));
5643
+ if (opening < 0) return text2;
5644
+ return lines.slice(0, opening).join("\n") + (opening > 0 ? "\n" : "");
5645
+ }
5646
+ function fenceIsOpen(text2, fence) {
5647
+ let open = false;
5648
+ for (const line of text2.split("\n")) {
5649
+ if (line.startsWith(fence)) open = !open;
5650
+ }
5651
+ return open;
5652
+ }
4963
5653
  function applyTruncatedContent(file, content) {
4964
5654
  file.content = content;
4965
5655
  file.isPreviewTruncated = true;
@@ -5007,52 +5697,64 @@ function greedyGroups(sections, budget) {
5007
5697
  if (current.length > 0) groups.push(current);
5008
5698
  return groups;
5009
5699
  }
5010
- function renderGroups(groups, budget, contentFingerprint) {
5700
+ function assembleGroup(group, index, total, contentFingerprint) {
5701
+ const width = Math.max(3, String(total).length);
5702
+ const zeroPad = (value) => String(value).padStart(width, "0");
5703
+ const filename = `part-${zeroPad(index + 1)}.md`;
5704
+ const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
5705
+ const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
5706
+ for (const section of group.sections) {
5707
+ section.file.part = filename;
5708
+ }
5709
+ const layerCounts = {};
5710
+ const kindCounts = {};
5711
+ for (const section of group.sections) {
5712
+ layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
5713
+ kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
5714
+ }
5715
+ const pathLines = group.sections.map((s) => displayPath(s.file.relativePath));
5716
+ const listFence = fenceForText(pathLines.join("\n"), 3);
5717
+ const header = [
5718
+ `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
5719
+ "",
5720
+ `- Previous: ${prev}`,
5721
+ `- Next: ${next}`,
5722
+ `- Files: ${group.sections.length}`,
5723
+ `- Content Fingerprint: ${contentFingerprint || "none"}`,
5724
+ `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
5725
+ `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
5726
+ "",
5727
+ UNTRUSTED_CONTENT_NOTICE,
5728
+ "",
5729
+ "## Files",
5730
+ "",
5731
+ `${listFence}text`,
5732
+ ...pathLines,
5733
+ listFence,
5734
+ "",
5735
+ "## Contents",
5736
+ ""
5737
+ ].join("\n");
5738
+ const assembled = (header + group.sections.map((s) => s.text).join("")).replace(
5739
+ /\r\n|\r/g,
5740
+ "\n"
5741
+ );
5742
+ const estimatedTokens = estimateTokens(assembled);
5743
+ return { filename, content: insertTokenLine(assembled, estimatedTokens), estimatedTokens };
5744
+ }
5745
+ function renderGroups(groups, contentFingerprint) {
5011
5746
  const total = groups.length;
5012
- const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
5013
5747
  return groups.map((group, index) => {
5014
- const filename = `part-${zeroPad(index + 1)}.md`;
5015
- const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
5016
- const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
5017
- for (const section of group) {
5018
- section.file.part = filename;
5019
- }
5020
- const layersCovered = Array.from(new Set(group.map((s) => s.file.layer))).sort();
5021
- const layerCounts = {};
5022
- const kindCounts = {};
5023
- for (const section of group) {
5024
- layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
5025
- kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
5026
- }
5027
- const pathLines = group.map((s) => displayPath(s.file.relativePath));
5028
- const listFence = fenceForText(pathLines.join("\n"), 3);
5029
- const header = [
5030
- `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
5031
- "",
5032
- `- Previous: ${prev}`,
5033
- `- Next: ${next}`,
5034
- `- Files: ${group.length}`,
5035
- `- Content Fingerprint: ${contentFingerprint || "none"}`,
5036
- `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
5037
- `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
5038
- "",
5039
- UNTRUSTED_CONTENT_NOTICE,
5040
- "",
5041
- "## Files",
5042
- "",
5043
- `${listFence}text`,
5044
- ...pathLines,
5045
- listFence,
5046
- "",
5047
- "## Contents",
5048
- ""
5049
- ].join("\n");
5050
- const assembled = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
5051
- const estimatedTokens = estimateTokens(assembled);
5052
- const content = insertTokenLine(assembled, estimatedTokens);
5748
+ const { filename, content, estimatedTokens } = assembleGroup(
5749
+ group,
5750
+ index,
5751
+ total,
5752
+ contentFingerprint
5753
+ );
5053
5754
  const byteSize = Buffer.byteLength(content, "utf8");
5054
5755
  const sha256 = crypto2.createHash("sha256").update(content).digest("hex");
5055
- const includedFiles = group.map(({ file }) => ({
5756
+ const layersCovered = Array.from(new Set(group.sections.map((s) => s.file.layer))).sort();
5757
+ const includedFiles = group.sections.map(({ file }) => ({
5056
5758
  relativePath: file.relativePath,
5057
5759
  sourceBytes: file.sizeBytes,
5058
5760
  includedBytes: file.includedBytes,
@@ -5065,10 +5767,10 @@ function renderGroups(groups, budget, contentFingerprint) {
5065
5767
  filename,
5066
5768
  byteSize,
5067
5769
  estimatedTokens,
5068
- budgetBytes: budget,
5770
+ budgetBytes: group.budget,
5069
5771
  sha256,
5070
5772
  lines: countLines(content),
5071
- fileCount: group.length,
5773
+ fileCount: group.sections.length,
5072
5774
  layersCovered,
5073
5775
  includedFiles
5074
5776
  };
@@ -5076,7 +5778,7 @@ function renderGroups(groups, budget, contentFingerprint) {
5076
5778
  });
5077
5779
  }
5078
5780
  function emittedBody(content) {
5079
- const normalised = content.replace(/\r\n/g, "\n");
5781
+ const normalised = content.replace(/\r\n|\r/g, "\n");
5080
5782
  return normalised.endsWith("\n") ? normalised.slice(0, -1) : normalised;
5081
5783
  }
5082
5784
  function compactMetadata(file) {
@@ -5144,7 +5846,7 @@ function generateMonolithJson(manifest, filesWithContent) {
5144
5846
  }
5145
5847
  function generateMonolithMarkdown(manifest, filesWithContent) {
5146
5848
  const sections = filesWithContent.map((file) => {
5147
- const content = (file.content ?? "").replace(/\r\n/g, "\n");
5849
+ const content = (file.content ?? "").replace(/\r\n|\r/g, "\n");
5148
5850
  const fence = fenceForContent(content);
5149
5851
  return [
5150
5852
  `## File: ${escapeControls(file.relativePath)}`,
@@ -5309,322 +6011,79 @@ function generateReadme(manifest, artifacts) {
5309
6011
  `${readOrder.length + 1}. Inspect \`manifest.json\` for precise dependency and AST symbol maps.`
5310
6012
  );
5311
6013
  }
5312
- readOrder.push(
5313
- `${readOrder.length + 1}. Check \`manifest.validation.status\` before trusting the snapshot; \`snapshot validate\` re-checks it against disk.`
5314
- );
5315
- lines.push("", "## AI Agent Ingestion Guide", "", ...readOrder, "");
5316
- return lines.join("\n");
5317
- }
5318
-
5319
- // src/generator/tree.ts
5320
- var MAX_TREE_LINES = 2e4;
5321
- var MAX_TREE_DEPTH = 512;
5322
- function childKeys(node) {
5323
- return Array.from(node.children.keys()).sort((a, b) => {
5324
- const aIsDir = node.children.get(a).children.size > 0;
5325
- const bIsDir = node.children.get(b).children.size > 0;
5326
- if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
5327
- return a < b ? -1 : a > b ? 1 : 0;
5328
- });
5329
- }
5330
- function generateProjectTree(filePaths) {
5331
- const root = { children: /* @__PURE__ */ new Map() };
5332
- for (const filePath of filePaths) {
5333
- const parts = filePath.split("/").filter((part) => part.length > 0);
5334
- let current = root;
5335
- for (const part of parts) {
5336
- let child = current.children.get(part);
5337
- if (!child) {
5338
- child = { children: /* @__PURE__ */ new Map() };
5339
- current.children.set(part, child);
5340
- }
5341
- current = child;
5342
- }
5343
- }
5344
- const lines = ["."];
5345
- let truncatedAt = -1;
5346
- let depthTruncated = false;
5347
- const stack = [{ node: root, prefix: "", depth: 0, keys: childKeys(root), index: 0 }];
5348
- while (stack.length > 0) {
5349
- const frame = stack[stack.length - 1];
5350
- if (frame.index >= frame.keys.length) {
5351
- stack.pop();
5352
- continue;
5353
- }
5354
- const key = frame.keys[frame.index];
5355
- frame.index += 1;
5356
- const isLast = frame.index === frame.keys.length;
5357
- const child = frame.node.children.get(key);
5358
- const isDirectory = child.children.size > 0;
5359
- const label = escapeControls(key);
5360
- lines.push(`${frame.prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
5361
- if (lines.length >= MAX_TREE_LINES) {
5362
- truncatedAt = lines.length;
5363
- break;
5364
- }
5365
- if (!isDirectory) continue;
5366
- if (frame.depth + 1 > MAX_TREE_DEPTH) {
5367
- depthTruncated = true;
5368
- continue;
5369
- }
5370
- stack.push({
5371
- node: child,
5372
- prefix: frame.prefix + (isLast ? " " : "\u2502 "),
5373
- depth: frame.depth + 1,
5374
- keys: childKeys(child),
5375
- index: 0
5376
- });
5377
- }
5378
- if (truncatedAt !== -1) {
5379
- lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
5380
- } else if (depthTruncated) {
5381
- lines.push(`... [tree truncated at ${MAX_TREE_DEPTH} levels] ...`);
5382
- }
5383
- const fence = fenceForText(lines.join("\n"), 3);
5384
- return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
5385
- }
5386
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
5387
- ".png",
5388
- ".jpg",
5389
- ".jpeg",
5390
- ".gif",
5391
- ".webp",
5392
- ".avif",
5393
- ".ico",
5394
- ".bmp",
5395
- ".tiff",
5396
- ".pdf",
5397
- ".zip",
5398
- ".tar",
5399
- ".gz",
5400
- ".tgz",
5401
- ".br",
5402
- ".bz2",
5403
- ".xz",
5404
- ".zst",
5405
- ".lz4",
5406
- ".lzma",
5407
- ".7z",
5408
- ".rar",
5409
- ".mp3",
5410
- ".mp4",
5411
- ".wav",
5412
- ".ogg",
5413
- ".webm",
5414
- ".mov",
5415
- ".avi",
5416
- ".woff",
5417
- ".woff2",
5418
- ".ttf",
5419
- ".otf",
5420
- ".eot",
5421
- ".wasm",
5422
- ".node",
5423
- ".exe",
5424
- ".dll",
5425
- ".dylib",
5426
- ".so"
5427
- ]);
5428
- function isBinaryExtension(filePath) {
5429
- const ext = path16.extname(filePath).toLowerCase();
5430
- return BINARY_EXTENSIONS.has(ext);
5431
- }
5432
- function isBinaryBuffer(buffer) {
5433
- return buffer.includes(0);
5434
- }
5435
- function countControlBytes(buffer) {
5436
- let count = 0;
5437
- for (let index = 0; index < buffer.length; index++) {
5438
- const byte = buffer[index];
5439
- if (byte < 32) {
5440
- if (byte !== 9 && byte !== 10 && byte !== 13) count += 1;
5441
- continue;
5442
- }
5443
- if (byte === 127) {
5444
- count += 1;
5445
- continue;
5446
- }
5447
- if (byte === 194 && index + 1 < buffer.length) {
5448
- const next = buffer[index + 1];
5449
- if (next >= 128 && next <= 159) {
5450
- count += 1;
5451
- index += 1;
5452
- }
5453
- }
5454
- }
5455
- return count;
5456
- }
5457
- var CONTROL_BYTES_FLOOR = 4;
5458
- function countInvalidUtf8Bytes(buffer) {
5459
- let invalid = 0;
5460
- let index = 0;
5461
- const length = buffer.length;
5462
- while (index < length) {
5463
- const lead = buffer[index];
5464
- if (lead < 128) {
5465
- index += 1;
5466
- continue;
5467
- }
5468
- let continuation = 0;
5469
- let codePoint = 0;
5470
- let minimum = 0;
5471
- if ((lead & 224) === 192) {
5472
- continuation = 1;
5473
- codePoint = lead & 31;
5474
- minimum = 128;
5475
- } else if ((lead & 240) === 224) {
5476
- continuation = 2;
5477
- codePoint = lead & 15;
5478
- minimum = 2048;
5479
- } else if ((lead & 248) === 240) {
5480
- continuation = 3;
5481
- codePoint = lead & 7;
5482
- minimum = 65536;
5483
- } else {
5484
- invalid += 1;
5485
- index += 1;
5486
- continue;
5487
- }
5488
- if (index + continuation >= length) {
5489
- invalid += 1;
5490
- index += 1;
5491
- continue;
5492
- }
5493
- let wellFormed = true;
5494
- for (let offset = 1; offset <= continuation; offset++) {
5495
- const next = buffer[index + offset];
5496
- if ((next & 192) !== 128) {
5497
- wellFormed = false;
5498
- break;
5499
- }
5500
- codePoint = codePoint << 6 | next & 63;
5501
- }
5502
- const isSurrogate = codePoint >= 55296 && codePoint <= 57343;
5503
- if (!wellFormed || codePoint < minimum || codePoint > 1114111 || isSurrogate) {
5504
- invalid += 1;
5505
- index += 1;
5506
- continue;
5507
- }
5508
- index += continuation + 1;
5509
- }
5510
- return invalid;
5511
- }
5512
- function readBomEncoding(buffer) {
5513
- if (buffer.length >= 2) {
5514
- if (buffer[0] === 255 && buffer[1] === 254) return "utf-16le";
5515
- if (buffer[0] === 254 && buffer[1] === 255) return "utf-16be";
5516
- }
5517
- return null;
5518
- }
5519
- function looksLikeUtf16(buffer) {
5520
- if (buffer.length < 8 || buffer.length % 2 !== 0) return null;
5521
- const sample = Math.min(buffer.length, 4096);
5522
- let nulInLowByte = 0;
5523
- let nulInHighByte = 0;
5524
- const units = sample / 2;
5525
- for (let index = 0; index < sample; index += 2) {
5526
- if (buffer[index] === 0) nulInLowByte += 1;
5527
- if (buffer[index + 1] === 0) nulInHighByte += 1;
5528
- }
5529
- if (nulInHighByte > units * 0.3 && nulInLowByte < units * 0.05) return "utf-16le";
5530
- if (nulInLowByte > units * 0.3 && nulInHighByte < units * 0.05) return "utf-16be";
5531
- return null;
5532
- }
5533
- function decodeUtf16(buffer, encoding) {
5534
- const littleEndian = encoding === "utf-16le" ? buffer : Buffer.from(buffer.subarray(0, buffer.length - buffer.length % 2)).swap16();
5535
- return new TextDecoder("utf-16le").decode(littleEndian);
5536
- }
5537
- function decodeText(buffer) {
5538
- const bomEncoding = readBomEncoding(buffer);
5539
- if (bomEncoding) {
5540
- const text2 = decodeUtf16(buffer.subarray(2), bomEncoding);
5541
- return { text: text2, encoding: bomEncoding, invalidBytes: 0, binary: false };
5542
- }
5543
- if (buffer.includes(0)) {
5544
- const shape = looksLikeUtf16(buffer);
5545
- if (shape) {
5546
- return { text: decodeUtf16(buffer, shape), encoding: shape, invalidBytes: 0, binary: false };
5547
- }
5548
- const offset = buffer.indexOf(0);
5549
- return {
5550
- text: "",
5551
- encoding: "utf-8",
5552
- invalidBytes: 0,
5553
- binary: true,
5554
- binaryReason: `NUL byte at offset ${offset}`
5555
- };
5556
- }
5557
- const invalidBytes = countInvalidUtf8Bytes(buffer);
5558
- if (invalidBytes === 0) {
5559
- const controlBytes = countControlBytes(buffer);
5560
- if (controlBytes > CONTROL_BYTES_FLOOR && controlBytes * 100 > buffer.length) {
5561
- return {
5562
- text: "",
5563
- encoding: "utf-8",
5564
- invalidBytes: 0,
5565
- binary: true,
5566
- binaryReason: `${controlBytes} control byte(s) in ${buffer.length} B; valid UTF-8 but not text`
5567
- };
5568
- }
5569
- return {
5570
- text: new TextDecoder("utf-8").decode(buffer),
5571
- encoding: "utf-8",
5572
- invalidBytes: 0,
5573
- binary: false
5574
- };
5575
- }
5576
- if (invalidBytes >= 32 && invalidBytes * 3 >= buffer.length) {
5577
- return {
5578
- text: "",
5579
- encoding: "utf-8",
5580
- invalidBytes,
5581
- binary: true,
5582
- binaryReason: `${invalidBytes} of ${buffer.length} bytes are not UTF-8; treated as binary`
5583
- };
5584
- }
5585
- return {
5586
- text: buffer.toString("utf8"),
5587
- encoding: "utf-8-lossy",
5588
- invalidBytes,
5589
- binary: false
5590
- };
6014
+ readOrder.push(
6015
+ `${readOrder.length + 1}. Check \`manifest.validation.status\` before trusting the snapshot; \`snapshot validate\` re-checks it against disk.`
6016
+ );
6017
+ lines.push("", "## AI Agent Ingestion Guide", "", ...readOrder, "");
6018
+ return lines.join("\n");
5591
6019
  }
5592
- var BINARY_SCAN_BYTES = 8 * 1024 * 1024;
5593
- var CHUNK = 64 * 1024;
5594
- var SHAPE_SAMPLE_BYTES = 4096;
5595
- function isBinaryFile(absolutePath) {
5596
- if (isBinaryExtension(absolutePath)) return true;
5597
- let fd;
5598
- try {
5599
- fd = fs14.openSync(absolutePath, "r");
5600
- const buffer = Buffer.alloc(CHUNK);
5601
- let sample;
5602
- let position = 0;
5603
- while (position < BINARY_SCAN_BYTES) {
5604
- const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
5605
- const bytesRead = fs14.readSync(fd, buffer, 0, wanted, position);
5606
- if (bytesRead === 0) return false;
5607
- const chunk = buffer.subarray(0, bytesRead);
5608
- if (sample === void 0) {
5609
- sample = Buffer.from(chunk.subarray(0, SHAPE_SAMPLE_BYTES));
5610
- }
5611
- if (chunk.includes(0)) {
5612
- const shape = readBomEncoding(sample) ?? looksLikeUtf16(sample);
5613
- return shape === null;
6020
+
6021
+ // src/generator/tree.ts
6022
+ var MAX_TREE_LINES = 2e4;
6023
+ var MAX_TREE_DEPTH = 512;
6024
+ function childKeys(node) {
6025
+ return Array.from(node.children.keys()).sort((a, b) => {
6026
+ const aIsDir = node.children.get(a).children.size > 0;
6027
+ const bIsDir = node.children.get(b).children.size > 0;
6028
+ if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
6029
+ return a < b ? -1 : a > b ? 1 : 0;
6030
+ });
6031
+ }
6032
+ function generateProjectTree(filePaths) {
6033
+ const root = { children: /* @__PURE__ */ new Map() };
6034
+ for (const filePath of filePaths) {
6035
+ const parts = filePath.split("/").filter((part) => part.length > 0);
6036
+ let current = root;
6037
+ for (const part of parts) {
6038
+ let child = current.children.get(part);
6039
+ if (!child) {
6040
+ child = { children: /* @__PURE__ */ new Map() };
6041
+ current.children.set(part, child);
5614
6042
  }
5615
- position += bytesRead;
6043
+ current = child;
5616
6044
  }
5617
- return false;
5618
- } catch {
5619
- return false;
5620
- } finally {
5621
- if (fd !== void 0) {
5622
- try {
5623
- fs14.closeSync(fd);
5624
- } catch {
5625
- }
6045
+ }
6046
+ const lines = ["."];
6047
+ let truncatedAt = -1;
6048
+ let depthTruncated = false;
6049
+ const stack = [{ node: root, prefix: "", depth: 0, keys: childKeys(root), index: 0 }];
6050
+ while (stack.length > 0) {
6051
+ const frame = stack[stack.length - 1];
6052
+ if (frame.index >= frame.keys.length) {
6053
+ stack.pop();
6054
+ continue;
6055
+ }
6056
+ const key = frame.keys[frame.index];
6057
+ frame.index += 1;
6058
+ const isLast = frame.index === frame.keys.length;
6059
+ const child = frame.node.children.get(key);
6060
+ const isDirectory = child.children.size > 0;
6061
+ const label = escapeControls(key);
6062
+ lines.push(`${frame.prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
6063
+ if (lines.length >= MAX_TREE_LINES) {
6064
+ truncatedAt = lines.length;
6065
+ break;
6066
+ }
6067
+ if (!isDirectory) continue;
6068
+ if (frame.depth + 1 > MAX_TREE_DEPTH) {
6069
+ depthTruncated = true;
6070
+ continue;
5626
6071
  }
6072
+ stack.push({
6073
+ node: child,
6074
+ prefix: frame.prefix + (isLast ? " " : "\u2502 "),
6075
+ depth: frame.depth + 1,
6076
+ keys: childKeys(child),
6077
+ index: 0
6078
+ });
6079
+ }
6080
+ if (truncatedAt !== -1) {
6081
+ lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
6082
+ } else if (depthTruncated) {
6083
+ lines.push(`... [tree truncated at ${MAX_TREE_DEPTH} levels] ...`);
5627
6084
  }
6085
+ const fence = fenceForText(lines.join("\n"), 3);
6086
+ return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
5628
6087
  }
5629
6088
  function gitPaths(cwd, args) {
5630
6089
  let output;
@@ -5711,6 +6170,10 @@ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5711
6170
  ".mjs",
5712
6171
  ".cjs"
5713
6172
  ]);
6173
+ var PYTHON_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".pyi"]);
6174
+ var RUBY_EXTENSIONS = /* @__PURE__ */ new Set([".rb", ".rake", ".gemspec"]);
6175
+ var C_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"]);
6176
+ var PYTHON_SUFFIXES = [".py", ".pyi", "/__init__.py", "/__init__.pyi"];
5714
6177
  function buildImportResolver(files) {
5715
6178
  const byPath = /* @__PURE__ */ new Map();
5716
6179
  const byPathFolded = /* @__PURE__ */ new Map();
@@ -5737,11 +6200,45 @@ function buildImportResolver(files) {
5737
6200
  return { byPath, byPathFolded, byStem, byStemFolded };
5738
6201
  }
5739
6202
  function resolveSpecifier(fromFile, specifier, resolver) {
6203
+ const extension = path16.posix.extname(fromFile).toLowerCase();
6204
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
6205
+ const firstOf = (candidates) => {
6206
+ for (const candidate of candidates) {
6207
+ const hit = exact(candidate);
6208
+ if (hit) return hit;
6209
+ }
6210
+ return null;
6211
+ };
6212
+ if (PYTHON_EXTENSIONS.has(extension) && specifier.startsWith(".")) {
6213
+ const dots = /^\.+/.exec(specifier)?.[0].length ?? 0;
6214
+ const rest = specifier.slice(dots).replace(/\./g, "/");
6215
+ let base = path16.posix.dirname(fromFile);
6216
+ for (let up = 1; up < dots; up += 1) base = path16.posix.dirname(base);
6217
+ const joined2 = rest ? path16.posix.normalize(path16.posix.join(base, rest)) : base;
6218
+ return firstOf([joined2, ...PYTHON_SUFFIXES.map((suffix) => `${joined2}${suffix}`)]);
6219
+ }
6220
+ if (RUBY_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
6221
+ const joined2 = path16.posix.normalize(
6222
+ path16.posix.join(path16.posix.dirname(fromFile), specifier)
6223
+ );
6224
+ return firstOf([`${joined2}.rb`, joined2]);
6225
+ }
6226
+ if (C_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
6227
+ const joined2 = path16.posix.normalize(
6228
+ path16.posix.join(path16.posix.dirname(fromFile), specifier)
6229
+ );
6230
+ return exact(joined2);
6231
+ }
6232
+ if (extension === ".dart" && !specifier.includes(":")) {
6233
+ const joined2 = path16.posix.normalize(
6234
+ path16.posix.join(path16.posix.dirname(fromFile), specifier)
6235
+ );
6236
+ return firstOf([joined2, `${joined2}.dart`]);
6237
+ }
5740
6238
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5741
6239
  const joined = path16.posix.normalize(
5742
6240
  path16.posix.join(path16.posix.dirname(fromFile), specifier)
5743
6241
  );
5744
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5745
6242
  const direct = exact(joined);
5746
6243
  if (direct) return direct;
5747
6244
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
@@ -5822,28 +6319,6 @@ function detectLanguage(filePath) {
5822
6319
  const ext = path16.posix.extname(base).toLowerCase();
5823
6320
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
5824
6321
  }
5825
- var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5826
- var isTsSource = (relativePath) => {
5827
- const lower = relativePath.toLowerCase();
5828
- return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
5829
- };
5830
- function importsOfText(relativePath, text2) {
5831
- if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
5832
- return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
5833
- }
5834
- function importsOfFile(relativePath, absolutePath) {
5835
- let buffer;
5836
- try {
5837
- buffer = fs14.readFileSync(absolutePath);
5838
- } catch {
5839
- return [];
5840
- }
5841
- const decoded = decodeText(buffer);
5842
- if (decoded.binary) return [];
5843
- return importsOfText(relativePath, decoded.text);
5844
- }
5845
-
5846
- // src/validator/validate.ts
5847
6322
  function maskedPath(value, secretRules) {
5848
6323
  return findSecretInPath(value, secretRules)?.masked ?? value;
5849
6324
  }
@@ -5964,9 +6439,13 @@ function describeManifestShapeProblem(value) {
5964
6439
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5965
6440
  return 'a "parts" entry is not an object';
5966
6441
  }
5967
- if (typeof entry.filename !== "string") {
6442
+ const filename = entry.filename;
6443
+ if (typeof filename !== "string" || filename.trim() === "") {
5968
6444
  return 'a "parts" entry has no "filename"';
5969
6445
  }
6446
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("\0") || filename === "." || filename === ".." || path16.isAbsolute(filename)) {
6447
+ return 'a "parts" entry "filename" must be a bare filename inside parts/';
6448
+ }
5970
6449
  }
5971
6450
  if (!candidate.layers || typeof candidate.layers !== "object" || Array.isArray(candidate.layers)) {
5972
6451
  return 'missing "layers" object';
@@ -6077,7 +6556,7 @@ function validateSnapshot(configInput) {
6077
6556
  });
6078
6557
  for (const diskFile of collection.files) {
6079
6558
  const snapshotHash = manifestFileMap.get(diskFile);
6080
- if (!snapshotHash) {
6559
+ if (snapshotHash === void 0) {
6081
6560
  const skipReason = skippedReasonOnDisk(diskFile);
6082
6561
  if (skipReason === "unchanged") {
6083
6562
  if (unchangedScope !== null) {
@@ -6091,6 +6570,11 @@ function validateSnapshot(configInput) {
6091
6570
  drift.missingInSnapshot.push(diskFile);
6092
6571
  continue;
6093
6572
  }
6573
+ if (unchangedScope !== null && !unchangedScope.has(diskFile) && !becameDependent(diskFile)) {
6574
+ drift.configMismatches.push(
6575
+ `scope: ${maskedPath(diskFile, config.secretRules)} is in the snapshot but is no longer in the changed-files scope`
6576
+ );
6577
+ }
6094
6578
  let diskHash;
6095
6579
  try {
6096
6580
  diskHash = sha256Hex(fs14.readFileSync(path16.join(config.cwd, diskFile)));
@@ -6231,6 +6715,41 @@ function validateSnapshot(configInput) {
6231
6715
  `includeSensitiveFiles: manifest ${recorded.includeSensitiveFiles} vs current ${config.includeSensitiveFiles}`
6232
6716
  );
6233
6717
  }
6718
+ if ((recorded.maxPartTokens ?? void 0) !== (config.maxPartTokens ?? void 0)) {
6719
+ drift.configMismatches.push(
6720
+ `maxPartTokens: manifest ${String(recorded.maxPartTokens ?? "unset")} vs current ${String(config.maxPartTokens ?? "unset")}`
6721
+ );
6722
+ }
6723
+ if ((recorded.maxMonolithBytes ?? null) !== (config.maxMonolithBytes ?? null)) {
6724
+ drift.configMismatches.push(
6725
+ `maxMonolithBytes: manifest ${String(recorded.maxMonolithBytes ?? null)} vs current ${String(config.maxMonolithBytes ?? null)}`
6726
+ );
6727
+ }
6728
+ if (Boolean(recorded.skeleton) !== Boolean(config.skeleton)) {
6729
+ drift.configMismatches.push(
6730
+ `skeleton: manifest ${Boolean(recorded.skeleton)} vs current ${Boolean(config.skeleton)}`
6731
+ );
6732
+ }
6733
+ if (Boolean(recorded.sarif) !== Boolean(config.sarif)) {
6734
+ drift.configMismatches.push(
6735
+ `sarif: manifest ${Boolean(recorded.sarif)} vs current ${Boolean(config.sarif)}`
6736
+ );
6737
+ }
6738
+ if ((recorded.changedSince ?? void 0) !== (config.changedSince ?? void 0)) {
6739
+ drift.configMismatches.push(
6740
+ `changedSince: manifest ${String(recorded.changedSince ?? "unset")} vs current ${String(config.changedSince ?? "unset")}`
6741
+ );
6742
+ }
6743
+ if (Boolean(recorded.staged) !== Boolean(config.staged)) {
6744
+ drift.configMismatches.push(
6745
+ `staged: manifest ${Boolean(recorded.staged)} vs current ${Boolean(config.staged)}`
6746
+ );
6747
+ }
6748
+ if (Boolean(recorded.withDependents) !== Boolean(config.withDependents)) {
6749
+ drift.configMismatches.push(
6750
+ `withDependents: manifest ${Boolean(recorded.withDependents)} vs current ${Boolean(config.withDependents)}`
6751
+ );
6752
+ }
6234
6753
  const effectiveDiscovery = config.discovery ?? "auto";
6235
6754
  const recordedDiscovery = recorded.discovery;
6236
6755
  if (recordedDiscovery !== void 0 && recordedDiscovery !== effectiveDiscovery) {
@@ -6451,7 +6970,7 @@ function validateSnapshot(configInput) {
6451
6970
 
6452
6971
  // src/version.ts
6453
6972
  var GENERATOR_NAME = "codebase-snapshot";
6454
- var GENERATOR_VERSION = "0.9.12";
6973
+ var GENERATOR_VERSION = "0.9.14";
6455
6974
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
6456
6975
  function isOurGeneratorName(name) {
6457
6976
  if (typeof name !== "string") return false;
@@ -6478,22 +6997,28 @@ function updateCommand(kind) {
6478
6997
  if (kind === "local") return "npm install codebase-snapshot@latest";
6479
6998
  return "npm install -g codebase-snapshot";
6480
6999
  }
6481
- function versionParts(value) {
6482
- const core = value.trim().replace(/^v/i, "").split("-")[0];
6483
- return core.split(".").map((part) => {
7000
+ function parseSemver(value) {
7001
+ const cleaned = value.trim().replace(/^v/i, "").split("+")[0] ?? "";
7002
+ const dash = cleaned.indexOf("-");
7003
+ const corePart = dash === -1 ? cleaned : cleaned.slice(0, dash);
7004
+ const prerelease = dash === -1 ? null : cleaned.slice(dash + 1);
7005
+ const core = corePart.split(".").map((part) => {
6484
7006
  const parsed = Number(part);
6485
7007
  return Number.isFinite(parsed) ? parsed : 0;
6486
7008
  });
7009
+ return { core, prerelease };
6487
7010
  }
6488
7011
  function isNewer(candidate, current) {
6489
- const left = versionParts(candidate);
6490
- const right = versionParts(current);
6491
- const length = Math.max(left.length, right.length);
7012
+ const left = parseSemver(candidate);
7013
+ const right = parseSemver(current);
7014
+ if (left.prerelease !== null && right.prerelease === null) return false;
7015
+ const length = Math.max(left.core.length, right.core.length);
6492
7016
  for (let index = 0; index < length; index += 1) {
6493
- const a = left[index] ?? 0;
6494
- const b = right[index] ?? 0;
7017
+ const a = left.core[index] ?? 0;
7018
+ const b = right.core[index] ?? 0;
6495
7019
  if (a !== b) return a > b;
6496
7020
  }
7021
+ if (left.prerelease === null && right.prerelease !== null) return true;
6497
7022
  return false;
6498
7023
  }
6499
7024
  function cacheDirectory(env = process.env, platform = process.platform) {
@@ -6561,14 +7086,22 @@ function writeCache(file, entry) {
6561
7086
  }
6562
7087
  }
6563
7088
  function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOUT_MS) {
6564
- return new Promise((resolve5) => {
7089
+ return new Promise((resolve6) => {
6565
7090
  let settled = false;
7091
+ let request = null;
6566
7092
  const finish = (value) => {
6567
7093
  if (settled) return;
6568
7094
  settled = true;
6569
- resolve5(value);
7095
+ clearTimeout(deadline);
7096
+ resolve6(value);
6570
7097
  };
6571
- const request = https.get(
7098
+ const deadline = setTimeout(() => {
7099
+ request?.destroy();
7100
+ finish(null);
7101
+ }, timeoutMs);
7102
+ deadline.unref();
7103
+ const transport = url.startsWith("http:") ? http : https;
7104
+ request = transport.get(
6572
7105
  url,
6573
7106
  {
6574
7107
  headers: {
@@ -6587,7 +7120,7 @@ function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOU
6587
7120
  response.on("data", (chunk) => {
6588
7121
  body += chunk;
6589
7122
  if (body.length > 64 * 1024) {
6590
- request.destroy();
7123
+ request?.destroy();
6591
7124
  finish(null);
6592
7125
  }
6593
7126
  });
@@ -6602,10 +7135,6 @@ function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOU
6602
7135
  }
6603
7136
  );
6604
7137
  request.on("error", () => finish(null));
6605
- request.setTimeout(timeoutMs, () => {
6606
- request.destroy();
6607
- finish(null);
6608
- });
6609
7138
  });
6610
7139
  }
6611
7140
  async function checkForUpdate(options) {
@@ -6649,10 +7178,13 @@ function describeMatcher(matcher) {
6649
7178
  return String(matcher);
6650
7179
  }
6651
7180
  function normalizeTarget(cwd, target) {
6652
- const absolute = path16.resolve(cwd, target);
6653
- const relative8 = path16.relative(cwd, absolute);
6654
- if (relative8.startsWith("..") || path16.isAbsolute(relative8)) return target;
6655
- return relative8.split(path16.sep).join("/");
7181
+ const resolvedCwd = path16.resolve(cwd);
7182
+ const absolute = path16.resolve(resolvedCwd, target);
7183
+ const relative8 = path16.relative(resolvedCwd, absolute);
7184
+ if (relative8 === "" || relative8 === ".." || relative8.startsWith(`..${path16.sep}`) || path16.isAbsolute(relative8)) {
7185
+ return { relative: target.replace(/\\/g, "/"), outsideRoot: true };
7186
+ }
7187
+ return { relative: relative8.split(path16.sep).join("/"), outsideRoot: false };
6656
7188
  }
6657
7189
  function findRuleMatch(rules, relativePath) {
6658
7190
  return rules.find(
@@ -6679,9 +7211,19 @@ function readManifest2(config) {
6679
7211
  }
6680
7212
  }
6681
7213
  function explainPath(config, target) {
6682
- const relative8 = normalizeTarget(config.cwd, target);
6683
- const absolute = path16.join(config.cwd, relative8);
7214
+ const normalized = normalizeTarget(config.cwd, target);
7215
+ const relative8 = normalized.relative;
6684
7216
  const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
7217
+ if (normalized.outsideRoot) {
7218
+ return {
7219
+ path: maskedPath2,
7220
+ exists: false,
7221
+ included: false,
7222
+ reason: "outside-root",
7223
+ detail: "path resolves outside the configured repository root and was not inspected"
7224
+ };
7225
+ }
7226
+ const absolute = path16.join(config.cwd, relative8);
6685
7227
  const result = {
6686
7228
  path: maskedPath2,
6687
7229
  exists: fs14.existsSync(absolute),
@@ -7319,6 +7861,24 @@ function displayWidth(line) {
7319
7861
  return width;
7320
7862
  }
7321
7863
  var GUTTER_PATTERN = /^[ \t]*(?:[❯◆◻◼▏✖][ \t]+)*/;
7864
+ function sliceByDisplayWidth(text2, maxCells) {
7865
+ let cells = 0;
7866
+ let lastSpace = -1;
7867
+ let index = 0;
7868
+ for (const character of text2) {
7869
+ const width = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;
7870
+ if (cells + width > maxCells) break;
7871
+ if (character === " ") lastSpace = index;
7872
+ cells += width;
7873
+ index += character.length;
7874
+ }
7875
+ if (index >= text2.length) return { head: text2, tail: "" };
7876
+ if (lastSpace > 0) {
7877
+ return { head: text2.slice(0, lastSpace), tail: text2.slice(lastSpace + 1).trimStart() };
7878
+ }
7879
+ const safeIndex = index > 0 ? index : Array.from(text2)[0]?.length ?? 1;
7880
+ return { head: text2.slice(0, safeIndex), tail: text2.slice(safeIndex).trimStart() };
7881
+ }
7322
7882
  function wrapOneLine(line, limit) {
7323
7883
  if (displayWidth(line) <= limit) return [line];
7324
7884
  const gutter = GUTTER_PATTERN.exec(line)?.[0] ?? "";
@@ -7333,12 +7893,11 @@ function wrapOneLine(line, limit) {
7333
7893
  rows.push(prefix + rest);
7334
7894
  break;
7335
7895
  }
7336
- let cut = room;
7337
- const space = rest.slice(0, room).lastIndexOf(" ");
7338
- if (space > 0) cut = space;
7339
- const taken = rest.slice(0, cut).trimEnd();
7340
- rows.push(`${prefix}${taken.length > 0 ? taken : rest.slice(0, cut)}`);
7341
- rest = rest.slice(cut).trimStart();
7896
+ const { head, tail } = sliceByDisplayWidth(rest, room);
7897
+ rows.push(
7898
+ `${prefix}${head.length > 0 ? head : rest.slice(0, 1)}`
7899
+ );
7900
+ rest = tail;
7342
7901
  prefix = continuation;
7343
7902
  }
7344
7903
  return rows;
@@ -7409,7 +7968,7 @@ function keyReader(input) {
7409
7968
  next() {
7410
7969
  const queued = queue.shift();
7411
7970
  if (queued) return Promise.resolve(queued);
7412
- return new Promise((resolve5) => waiters.push(resolve5));
7971
+ return new Promise((resolve6) => waiters.push(resolve6));
7413
7972
  },
7414
7973
  close() {
7415
7974
  input.off("keypress", onKey);
@@ -7626,17 +8185,25 @@ var CONFIG_CANDIDATES = [
7626
8185
  ".snapshotrc.json",
7627
8186
  ".snapshotrc"
7628
8187
  ];
7629
- function chooseConfigFileName(cwd) {
7630
- if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
7631
- const pkgPath = path16.join(cwd, "package.json");
7632
- if (!fs14.existsSync(pkgPath)) return "snapshot.config.mjs";
8188
+ function parsePackageJson(raw) {
8189
+ const parsed = JSON.parse(stripJsonComments(raw));
8190
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
8191
+ throw new Error("package.json is not a JSON object");
8192
+ }
8193
+ return parsed;
8194
+ }
8195
+ function readPackageJson(cwd) {
7633
8196
  try {
7634
- const parsed = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
7635
- return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
8197
+ return parsePackageJson(fs14.readFileSync(path16.join(cwd, "package.json"), "utf8"));
7636
8198
  } catch {
7637
- return "snapshot.config.mjs";
8199
+ return null;
7638
8200
  }
7639
8201
  }
8202
+ function chooseConfigFileName(cwd) {
8203
+ if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
8204
+ const parsed = readPackageJson(cwd);
8205
+ return parsed?.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
8206
+ }
7640
8207
  var SIZE_CHOICES = [
7641
8208
  { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
7642
8209
  { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
@@ -7644,12 +8211,9 @@ var SIZE_CHOICES = [
7644
8211
  { value: "custom", label: "Custom\u2026", hint: "type a size like 150k or 1.5mb" }
7645
8212
  ];
7646
8213
  function packageJsonHasConfig(cwd) {
7647
- try {
7648
- const parsed = JSON.parse(fs14.readFileSync(path16.join(cwd, "package.json"), "utf8"));
7649
- return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
7650
- } catch {
7651
- return false;
7652
- }
8214
+ const parsed = readPackageJson(cwd);
8215
+ if (parsed === null) return false;
8216
+ return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
7653
8217
  }
7654
8218
  function artifactAnswers(options) {
7655
8219
  const artifacts = { ...DEFAULT_ANSWERS.artifacts };
@@ -7690,12 +8254,7 @@ function writeFileOrExplain(target, content) {
7690
8254
  function canWriteModuleConfig(name, cwd) {
7691
8255
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
7692
8256
  if (!name.endsWith(".js")) return false;
7693
- try {
7694
- const parsed = JSON.parse(fs14.readFileSync(path16.join(cwd, "package.json"), "utf8"));
7695
- return parsed.type === "module";
7696
- } catch {
7697
- return false;
7698
- }
8257
+ return readPackageJson(cwd)?.type === "module";
7699
8258
  }
7700
8259
  function configFormatLabel(name) {
7701
8260
  if (name.endsWith(".js")) return 'CommonJS `.js` (package.json has no "type": "module")';
@@ -7723,7 +8282,8 @@ function serializePackageJson(raw, value) {
7723
8282
  const crlf = raw.includes("\r\n");
7724
8283
  const body = JSON.stringify(value, null, spacing);
7725
8284
  const text2 = crlf ? body.replace(/\n/g, "\r\n") : body;
7726
- return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
8285
+ const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
8286
+ return `${bom}${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
7727
8287
  }
7728
8288
  function updatePackageJson(cwd, notes) {
7729
8289
  const packagePath = path16.join(cwd, "package.json");
@@ -7739,7 +8299,7 @@ function updatePackageJson(cwd, notes) {
7739
8299
  let parsed;
7740
8300
  try {
7741
8301
  raw = fs14.readFileSync(packagePath, "utf8");
7742
- parsed = JSON.parse(raw);
8302
+ parsed = parsePackageJson(raw);
7743
8303
  } catch {
7744
8304
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
7745
8305
  return unchanged;
@@ -8278,7 +8838,10 @@ const { parentPort, workerData } = require("node:worker_threads");
8278
8838
  parentPort.postMessage({ ok: false, message: loaded.errors.join("; ") });
8279
8839
  return;
8280
8840
  }
8281
- const config = mod.resolveConfig(loaded.config, workerData.cliOptions, workerData.cwd);
8841
+ // \`hooks: {}\` is a value, not an absence: the config is applied with the hook list emptied,
8842
+ // so the run is the documented MCP run rather than the CLI run with one setting removed.
8843
+ const base = loaded.config ? { ...loaded.config, hooks: {} } : null;
8844
+ const config = mod.resolveConfig(base, workerData.cliOptions, workerData.cwd);
8282
8845
  const manifest = mod.generateSnapshot(config);
8283
8846
  parentPort.postMessage({
8284
8847
  ok: true,
@@ -8301,14 +8864,15 @@ const { parentPort, workerData } = require("node:worker_threads");
8301
8864
  })();
8302
8865
  `;
8303
8866
  function generateInWorker(cwd, cliOptions) {
8304
- return new Promise((resolve5) => {
8867
+ return new Promise((resolve6) => {
8305
8868
  let settled = false;
8869
+ let worker = null;
8306
8870
  const finish = (result) => {
8307
8871
  if (settled) return;
8308
8872
  settled = true;
8309
- resolve5(result);
8873
+ if (worker !== null) void worker.terminate();
8874
+ resolve6(result);
8310
8875
  };
8311
- let worker;
8312
8876
  const isCjs = typeof __dirname !== "undefined";
8313
8877
  const directory = isCjs ? __dirname : path16.dirname(fileURLToPath(import.meta.url));
8314
8878
  try {
@@ -8341,7 +8905,100 @@ function generateInWorker(cwd, cliOptions) {
8341
8905
 
8342
8906
  // src/index.ts
8343
8907
  var SCHEMA_VERSION = "3.0.0";
8344
- var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
8908
+ var SUMMARY_MAX_SOURCE_BYTES2 = SUMMARY_MAX_SOURCE_BYTES;
8909
+ var FILE_DERIVATIONS = /* @__PURE__ */ new Map();
8910
+ var FILE_DERIVATION_LIMIT_BYTES = 64 * 1024 * 1024;
8911
+ var fileDerivationBytes = 0;
8912
+ function takeDerivation(path17, hash, settings) {
8913
+ const cached = FILE_DERIVATIONS.get(path17);
8914
+ if (cached === void 0 || cached.hash !== hash || cached.settings !== settings) return void 0;
8915
+ return cached;
8916
+ }
8917
+ function rememberDerivation(path17, hash, settings, derivation) {
8918
+ const bodyBytes = derivation.finalContent === derivation.redacted ? derivation.redacted.length : derivation.redacted.length + derivation.finalContent.length;
8919
+ const bytes = bodyBytes * 2 + 512;
8920
+ if (bytes > FILE_DERIVATION_LIMIT_BYTES / 4) return;
8921
+ while (fileDerivationBytes + bytes > FILE_DERIVATION_LIMIT_BYTES) {
8922
+ const oldest = FILE_DERIVATIONS.keys().next();
8923
+ if (oldest.done === true) break;
8924
+ const evicted = FILE_DERIVATIONS.get(oldest.value);
8925
+ FILE_DERIVATIONS.delete(oldest.value);
8926
+ if (evicted !== void 0) fileDerivationBytes -= evicted.bytes;
8927
+ }
8928
+ FILE_DERIVATIONS.set(path17, { ...derivation, hash, settings, bytes });
8929
+ fileDerivationBytes += bytes;
8930
+ }
8931
+ function derivationSettings(config) {
8932
+ const rules = config.secretRules.map(
8933
+ (rule) => `${rule.rule}\0${rule.pattern.source}\0${rule.pattern.flags}\0${rule.valueGroup ?? ""}`
8934
+ ).join("");
8935
+ return `${GENERATOR_VERSION}${configDigest(config)}${sha256Hex2(rules)}`;
8936
+ }
8937
+ function deriveFile(relativePath, rawContent, sizeBytes, config) {
8938
+ const lineCount = countLines(rawContent);
8939
+ const { redacted, findings } = scanAndRedactSecrets(rawContent, config.secretRules, relativePath);
8940
+ const isLarge = sizeBytes > config.largeFileBytes;
8941
+ let finalContent = redacted;
8942
+ let isPreviewTruncated = false;
8943
+ let includedLines = lineCount;
8944
+ let inclusionMode = "full";
8945
+ if (config.skeleton === true && isSkeletonizable(relativePath)) {
8946
+ const skeleton = skeletonizeTypeScript(redacted, relativePath);
8947
+ if (skeleton.collapsed > 0) {
8948
+ finalContent = skeleton.text;
8949
+ inclusionMode = "signatures";
8950
+ includedLines = countLines(skeleton.text);
8951
+ }
8952
+ }
8953
+ if (isLarge && inclusionMode !== "signatures") {
8954
+ const preview = truncatePreview(redacted, config.previewHeadLines, config.previewTailLines);
8955
+ finalContent = preview.content;
8956
+ isPreviewTruncated = preview.isTruncated;
8957
+ includedLines = preview.includedLines;
8958
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
8959
+ }
8960
+ let summary;
8961
+ let summaryQuality;
8962
+ let summarySkip = null;
8963
+ let summaryWarning = null;
8964
+ let dependentImports = null;
8965
+ if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES2) {
8966
+ summarySkip = "oversize";
8967
+ dependentImports = [];
8968
+ } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
8969
+ summarySkip = "preview";
8970
+ dependentImports = importsOfText(relativePath, rawContent);
8971
+ } else {
8972
+ try {
8973
+ const outcome = summarizeFile(relativePath, rawContent);
8974
+ summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
8975
+ summaryQuality = outcome?.quality;
8976
+ if (isSourceSummary(summary) && summary.parseErrors) {
8977
+ summaryWarning = `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`;
8978
+ }
8979
+ } catch (err) {
8980
+ summaryWarning = `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
8981
+ /\s+/g,
8982
+ " "
8983
+ );
8984
+ }
8985
+ }
8986
+ return {
8987
+ redacted,
8988
+ findings,
8989
+ lineCount,
8990
+ finalContent,
8991
+ inclusionMode,
8992
+ isPreviewTruncated,
8993
+ includedLines,
8994
+ isLarge,
8995
+ summary,
8996
+ summaryQuality,
8997
+ summarySkip,
8998
+ summaryWarning,
8999
+ dependentImports
9000
+ };
9001
+ }
8345
9002
  function sha256Hex2(input) {
8346
9003
  return crypto2.createHash("sha256").update(input).digest("hex");
8347
9004
  }
@@ -8885,10 +9542,7 @@ function acquireRunLock(stagingRoot) {
8885
9542
  };
8886
9543
  } catch (err) {
8887
9544
  if (err.code !== "EEXIST") throw err;
8888
- if (attempt === 0 && lockOwnerIsGone(lockPath)) {
8889
- fs14.rmSync(lockPath, { force: true });
8890
- continue;
8891
- }
9545
+ if (attempt === 0 && reclaimStaleLock(lockPath)) continue;
8892
9546
  throw new Error(
8893
9547
  `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
8894
9548
  );
@@ -8897,14 +9551,25 @@ function acquireRunLock(stagingRoot) {
8897
9551
  throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
8898
9552
  }
8899
9553
  function lockKeyFor(stagingRoot) {
8900
- let key = stagingRoot;
8901
- try {
8902
- key = fs14.realpathSync(stagingRoot);
8903
- } catch {
8904
- }
9554
+ let key = canonicalLockTarget(stagingRoot);
8905
9555
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
8906
9556
  return sha256Hex2(key).slice(0, 16);
8907
9557
  }
9558
+ function canonicalLockTarget(targetPath) {
9559
+ const resolved = path16.resolve(targetPath);
9560
+ let existing = resolved;
9561
+ const tail = [];
9562
+ for (; ; ) {
9563
+ try {
9564
+ return path16.join(fs14.realpathSync(existing), ...tail);
9565
+ } catch {
9566
+ const parent = path16.dirname(existing);
9567
+ if (parent === existing) return resolved;
9568
+ tail.unshift(path16.basename(existing));
9569
+ existing = parent;
9570
+ }
9571
+ }
9572
+ }
8908
9573
  function lockPathFor(stagingRoot) {
8909
9574
  return path16.join(os.tmpdir(), `codebase-snapshot-${lockKeyFor(stagingRoot)}.lock`);
8910
9575
  }
@@ -8929,6 +9594,24 @@ function lockOwnerIsGone(lockPath) {
8929
9594
  return err.code === "ESRCH";
8930
9595
  }
8931
9596
  }
9597
+ function reclaimStaleLock(lockPath) {
9598
+ let before;
9599
+ try {
9600
+ before = fs14.statSync(lockPath);
9601
+ } catch {
9602
+ return true;
9603
+ }
9604
+ if (!lockOwnerIsGone(lockPath)) return false;
9605
+ try {
9606
+ const current = fs14.statSync(lockPath);
9607
+ const sameFile = current.ino === before.ino && current.birthtimeMs === before.birthtimeMs && current.mtimeMs === before.mtimeMs;
9608
+ if (!sameFile) return false;
9609
+ fs14.rmSync(lockPath, { force: true });
9610
+ return true;
9611
+ } catch {
9612
+ return false;
9613
+ }
9614
+ }
8932
9615
  function lockFileIsAncient(lockPath) {
8933
9616
  try {
8934
9617
  return Date.now() - fs14.statSync(lockPath).mtimeMs > STALE_RUN_MS;
@@ -8986,7 +9669,6 @@ function swapOnce(stagingDir, outDir) {
8986
9669
  throw copyErr;
8987
9670
  }
8988
9671
  }
8989
- if (hadPrevious) fs14.rmSync(backupDir, { recursive: true, force: true });
8990
9672
  } catch (err) {
8991
9673
  if (hadPrevious && fs14.existsSync(backupDir)) {
8992
9674
  if (fs14.existsSync(outDir)) fs14.rmSync(outDir, { recursive: true, force: true });
@@ -8994,6 +9676,16 @@ function swapOnce(stagingDir, outDir) {
8994
9676
  }
8995
9677
  throw err;
8996
9678
  }
9679
+ if (hadPrevious) {
9680
+ try {
9681
+ fs14.rmSync(backupDir, { recursive: true, force: true });
9682
+ } catch (err) {
9683
+ process.stderr.write(
9684
+ `\u26A0 The previous snapshot could not be removed from ${backupDir} (${err.message}). The new snapshot is in place; delete that directory by hand when convenient.
9685
+ `
9686
+ );
9687
+ }
9688
+ }
8997
9689
  }
8998
9690
  function sleepSync(milliseconds) {
8999
9691
  const buffer = new Int32Array(new SharedArrayBuffer(4));
@@ -9016,22 +9708,18 @@ function artifactWarnings(artifacts, sarif) {
9016
9708
  }
9017
9709
  return messages;
9018
9710
  }
9711
+ function installHookStdoutRedirect() {
9712
+ return installStdoutRouter();
9713
+ }
9019
9714
  function runAfterGenerate(config, manifest) {
9020
9715
  const hook = config.hooks.afterGenerate;
9021
9716
  if (!hook) return null;
9022
- const stdoutWrite = process.stdout.write.bind(process.stdout);
9023
- const stderrWrite = process.stderr.write.bind(process.stderr);
9024
- const hookScope = new AsyncLocalStorage();
9025
- const patchedWrite = ((...args) => hookScope.getStore() === true ? stderrWrite(...args) : stdoutWrite(...args));
9026
- const restoreStdout = () => {
9027
- if (process.stdout.write === patchedWrite) process.stdout.write = stdoutWrite;
9028
- };
9717
+ const releaseRedirect = installHookStdoutRedirect();
9029
9718
  let returned;
9030
9719
  try {
9031
- process.stdout.write = patchedWrite;
9032
- returned = hookScope.run(true, () => hook(manifest));
9720
+ returned = hookStdoutScope.run(true, () => hook(manifest));
9033
9721
  } catch (err) {
9034
- restoreStdout();
9722
+ releaseRedirect();
9035
9723
  const message = maskPathText(err.message, config.secretRules);
9036
9724
  manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
9037
9725
  manifest.totals.warningCount = manifest.warnings.length;
@@ -9039,10 +9727,10 @@ function runAfterGenerate(config, manifest) {
9039
9727
  }
9040
9728
  const thenable = returned;
9041
9729
  if (!thenable || typeof thenable.then !== "function") {
9042
- restoreStdout();
9730
+ releaseRedirect();
9043
9731
  return null;
9044
9732
  }
9045
- return Promise.resolve(returned).finally(restoreStdout);
9733
+ return Promise.resolve(returned).finally(releaseRedirect);
9046
9734
  }
9047
9735
  function partitionByBudget(files, config, fingerprint) {
9048
9736
  const budgetFor = (file) => config.budgets.byLayer[file.layer] ?? config.budgets.default;
@@ -9053,23 +9741,14 @@ function partitionByBudget(files, config, fingerprint) {
9053
9741
  if (bucket) bucket.push(file);
9054
9742
  else buckets.set(budget, [file]);
9055
9743
  }
9056
- if (buckets.size <= 1) {
9057
- const only = buckets.size === 1 ? Array.from(buckets.keys())[0] : config.maxPartBytes;
9058
- return partitionFiles(files, only, fingerprint);
9744
+ if (buckets.size === 0) return [];
9745
+ if (buckets.size === 1) {
9746
+ return partitionFiles(files, Array.from(buckets.keys())[0], fingerprint);
9059
9747
  }
9060
- const parts = [];
9061
- for (const budget of Array.from(buckets.keys()).sort((a, b) => a - b)) {
9062
- parts.push(...partitionFiles(buckets.get(budget) ?? [], budget, fingerprint));
9063
- }
9064
- const merged = renumberParts(parts, fingerprint);
9065
- const byPath = new Map(files.map((file) => [file.relativePath, file]));
9066
- for (const part of merged) {
9067
- for (const included of part.metadata.includedFiles) {
9068
- const file = byPath.get(included.relativePath);
9069
- if (file) file.part = part.filename;
9070
- }
9071
- }
9072
- return merged;
9748
+ return partitionFilesForBuckets(
9749
+ Array.from(buckets.entries()).sort(([left], [right]) => left - right).map(([budgetBytes, bucketFiles]) => ({ files: bucketFiles, budgetBytes })),
9750
+ fingerprint
9751
+ );
9073
9752
  }
9074
9753
  function generateSnapshot(configInput, options) {
9075
9754
  assertUsableConfigInput(configInput);
@@ -9101,6 +9780,7 @@ function generateSnapshot(configInput, options) {
9101
9780
  return false;
9102
9781
  }) : collection.files;
9103
9782
  const files = [];
9783
+ const derivationKey = derivationSettings(config);
9104
9784
  const monolithBodies = /* @__PURE__ */ new Map();
9105
9785
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
9106
9786
  const secretFindings = [];
@@ -9113,6 +9793,7 @@ function generateSnapshot(configInput, options) {
9113
9793
  const fileWarnings = [];
9114
9794
  const oversizeSummaryPaths = [];
9115
9795
  const previewSummaryPaths = [];
9796
+ const skippedSummaryImports = /* @__PURE__ */ new Map();
9116
9797
  const layerCounts = {};
9117
9798
  const kindCounts = {};
9118
9799
  let totalLines = 0;
@@ -9177,8 +9858,6 @@ function generateSnapshot(configInput, options) {
9177
9858
  }
9178
9859
  const hashSha256 = sha256Hex2(buffer);
9179
9860
  const rawContent = decoded.text;
9180
- const lineCount = countLines(rawContent);
9181
- totalLines += lineCount;
9182
9861
  totalSizeBytes += stats.size;
9183
9862
  if (decoded.encoding !== "utf-8") {
9184
9863
  encodingNotes.push({
@@ -9192,66 +9871,25 @@ function generateSnapshot(configInput, options) {
9192
9871
  const language = detectLanguage(relativePath);
9193
9872
  bump(layerCounts, layer2);
9194
9873
  bump(kindCounts, kind);
9195
- const { redacted, findings } = scanAndRedactSecrets(
9196
- rawContent,
9197
- config.secretRules,
9198
- relativePath
9199
- );
9200
- secretFindings.push(...findings);
9201
- const isLarge = stats.size > config.largeFileBytes;
9202
- if (isLarge) largeFileCount += 1;
9203
- let finalContent = redacted;
9204
- let isPreviewTruncated = false;
9205
- let includedLines = lineCount;
9206
- let inclusionMode = "full";
9207
- if (config.skeleton === true && isSkeletonizable(relativePath)) {
9208
- const skeleton = skeletonizeTypeScript(redacted, relativePath);
9209
- if (skeleton.collapsed > 0) {
9210
- finalContent = skeleton.text;
9211
- inclusionMode = "signatures";
9212
- includedLines = countLines(skeleton.text);
9213
- }
9214
- }
9215
- if (isLarge && inclusionMode !== "signatures") {
9216
- const preview = truncatePreview(
9217
- redacted,
9218
- config.previewHeadLines,
9219
- config.previewTailLines
9220
- );
9221
- finalContent = preview.content;
9222
- isPreviewTruncated = preview.isTruncated;
9223
- includedLines = preview.includedLines;
9224
- inclusionMode = isPreviewTruncated ? "preview" : "full";
9225
- }
9226
- if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
9227
- monolithBodies.set(relativePath, redacted);
9228
- }
9229
- let summary;
9230
- let summaryQuality;
9231
- if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES) {
9232
- oversizeSummaryPaths.push(relativePath);
9233
- } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
9234
- previewSummaryPaths.push(relativePath);
9235
- } else {
9236
- try {
9237
- const outcome = summarizeFile(relativePath, rawContent);
9238
- summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
9239
- summaryQuality = outcome?.quality;
9240
- if (isSourceSummary(summary) && summary.parseErrors) {
9241
- fileWarnings.push({
9242
- path: relativePath,
9243
- message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
9244
- });
9245
- }
9246
- } catch (err) {
9247
- fileWarnings.push({
9248
- path: relativePath,
9249
- message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
9250
- /\s+/g,
9251
- " "
9252
- )
9253
- });
9254
- }
9874
+ const derivation = takeDerivation(absolutePath, hashSha256, derivationKey) ?? (() => {
9875
+ const fresh = deriveFile(relativePath, rawContent, stats.size, config);
9876
+ rememberDerivation(absolutePath, hashSha256, derivationKey, fresh);
9877
+ return fresh;
9878
+ })();
9879
+ const { lineCount } = derivation;
9880
+ totalLines += lineCount;
9881
+ secretFindings.push(...derivation.findings);
9882
+ if (derivation.isLarge) largeFileCount += 1;
9883
+ if (derivation.summarySkip === "oversize") oversizeSummaryPaths.push(relativePath);
9884
+ else if (derivation.summarySkip === "preview") previewSummaryPaths.push(relativePath);
9885
+ if (derivation.dependentImports !== null) {
9886
+ skippedSummaryImports.set(relativePath, derivation.dependentImports);
9887
+ }
9888
+ if (derivation.summaryWarning !== null) {
9889
+ fileWarnings.push({ path: relativePath, message: derivation.summaryWarning });
9890
+ }
9891
+ if (config.includeMonolith && (derivation.isPreviewTruncated || derivation.inclusionMode === "signatures")) {
9892
+ monolithBodies.set(relativePath, derivation.redacted);
9255
9893
  }
9256
9894
  files.push({
9257
9895
  relativePath,
@@ -9262,24 +9900,25 @@ function generateSnapshot(configInput, options) {
9262
9900
  layer: layer2,
9263
9901
  kind,
9264
9902
  language,
9265
- inclusionMode,
9266
- isLarge,
9267
- isPreviewTruncated,
9903
+ inclusionMode: derivation.inclusionMode,
9904
+ isLarge: derivation.isLarge,
9905
+ isPreviewTruncated: derivation.isPreviewTruncated,
9268
9906
  originalLines: lineCount,
9269
- includedLines,
9270
- includedBytes: Buffer.byteLength(finalContent, "utf8"),
9271
- summary,
9272
- summaryQuality,
9907
+ includedLines: derivation.includedLines,
9908
+ includedBytes: Buffer.byteLength(derivation.finalContent, "utf8"),
9909
+ summary: derivation.summary,
9910
+ summaryQuality: derivation.summaryQuality,
9273
9911
  ...decoded.encoding === "utf-8" ? {} : { encoding: decoded.encoding },
9274
- content: finalContent
9912
+ content: derivation.finalContent
9275
9913
  });
9276
9914
  }
9277
9915
  if (changedScope !== null && changedPaths !== null && config.withDependents === true) {
9278
9916
  const keep = new Set(changedPaths);
9279
9917
  const resolver = buildImportResolver(files);
9280
9918
  for (const file of files) {
9281
- if (!isSourceSummary(file.summary)) continue;
9282
- if (importsChangedFile(file.relativePath, file.summary.imports, changedPaths, resolver)) {
9919
+ const fileImports = isSourceSummary(file.summary) ? file.summary.imports : skippedSummaryImports.get(file.relativePath) ?? [];
9920
+ if (fileImports.length === 0) continue;
9921
+ if (importsChangedFile(file.relativePath, fileImports, changedPaths, resolver)) {
9283
9922
  keep.add(file.relativePath);
9284
9923
  }
9285
9924
  }
@@ -9310,6 +9949,9 @@ function generateSnapshot(configInput, options) {
9310
9949
  for (const path17 of [...monolithBodies.keys()]) {
9311
9950
  if (!keep.has(path17)) monolithBodies.delete(path17);
9312
9951
  }
9952
+ for (const path17 of [...skippedSummaryImports.keys()]) {
9953
+ if (!keep.has(path17)) skippedSummaryImports.delete(path17);
9954
+ }
9313
9955
  }
9314
9956
  if (changedScope !== null) {
9315
9957
  for (const relativePath of unchangedPaths) {
@@ -9345,7 +9987,7 @@ function generateSnapshot(configInput, options) {
9345
9987
  const shown = oversizeSummaryPaths.slice(0, ENCODING_PATHS_SHOWN).map((entry) => maskPathText(entry, config.secretRules));
9346
9988
  const hidden = oversizeSummaryPaths.length - ENCODING_PATHS_SHOWN;
9347
9989
  warnings.push(
9348
- `${oversizeSummaryPaths.length} file(s) exceed ${Math.round(SUMMARY_MAX_SOURCE_BYTES / (1024 * 1024))} MB and were not parsed for a structural summary: ${shown.join(", ")}${hidden > 0 ? ` and ${hidden} more` : ""}. Their bodies are emitted as head/tail previews.`
9990
+ `${oversizeSummaryPaths.length} file(s) exceed ${Math.round(SUMMARY_MAX_SOURCE_BYTES2 / (1024 * 1024))} MB and were not parsed for a structural summary: ${shown.join(", ")}${hidden > 0 ? ` and ${hidden} more` : ""}. Their bodies are emitted as head/tail previews.`
9349
9991
  );
9350
9992
  }
9351
9993
  for (const [encoding, bucket] of encodingBuckets) {
@@ -9369,11 +10011,13 @@ function generateSnapshot(configInput, options) {
9369
10011
  ).join("\n")
9370
10012
  );
9371
10013
  const fullContents = /* @__PURE__ */ new Map();
9372
- for (const file of files) {
9373
- fullContents.set(
9374
- file.relativePath,
9375
- monolithBodies.get(file.relativePath) ?? file.content ?? ""
9376
- );
10014
+ if (config.includeMonolith) {
10015
+ for (const file of files) {
10016
+ fullContents.set(
10017
+ file.relativePath,
10018
+ monolithBodies.get(file.relativePath) ?? file.content ?? ""
10019
+ );
10020
+ }
9377
10021
  }
9378
10022
  const parts = partitionByBudget(files, config, fingerprint);
9379
10023
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
@@ -9634,7 +10278,7 @@ function generateSnapshot(configInput, options) {
9634
10278
  };
9635
10279
  shipHookState();
9636
10280
  if (pendingHook) {
9637
- pendingHook.then(
10281
+ const tracked = pendingHook.then(
9638
10282
  () => {
9639
10283
  try {
9640
10284
  shipHookState();
@@ -9653,8 +10297,10 @@ function generateSnapshot(configInput, options) {
9653
10297
  } catch {
9654
10298
  }
9655
10299
  }
9656
- );
9657
- pendingHooks.set(outDir, pendingHook);
10300
+ ).finally(() => {
10301
+ if (pendingHooks.get(outDir) === tracked) pendingHooks.delete(outDir);
10302
+ });
10303
+ pendingHooks.set(outDir, tracked);
9658
10304
  }
9659
10305
  return manifest;
9660
10306
  } catch (err) {
@@ -9672,15 +10318,15 @@ function generateSnapshot(configInput, options) {
9672
10318
  }
9673
10319
  var pendingHooks = /* @__PURE__ */ new Map();
9674
10320
  async function generateSnapshotAsync(configInput, options) {
9675
- const manifest = generateSnapshot(configInput, options);
10321
+ assertUsableConfigInput(configInput);
9676
10322
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput ?? null);
10323
+ const manifest = generateSnapshot(config, options);
9677
10324
  const { outDir } = resolveSafeOutputDirs(config);
9678
10325
  const pending = pendingHooks.get(outDir);
9679
10326
  if (pending !== void 0) {
9680
- pendingHooks.delete(outDir);
9681
10327
  await pending.catch(() => void 0);
9682
10328
  }
9683
10329
  return manifest;
9684
10330
  }
9685
10331
 
9686
- export { ARTIFACT_NAMES, CHECK_INTERVAL_MS, CONFIG_KEYS, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, FAILURE_INTERVAL_MS, GENERATOR_NAME, GENERATOR_VERSION, HOOK_MARKER, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, MIN_MAX_PART_TOKENS, PACKAGE_LATEST_URL, PRESETS, PromptBack, PromptCancelled, REQUEST_TIMEOUT_MS, SCHEMA_VERSION, anchorForPath, appendGitignoreEntries, assertReplaceable, assertUsableConfigInput, buildImportResolver, bytesForTokens, cacheDirectory, cacheFilePath, checkForUpdate, classifyKind, classifyLayer, collectFiles, compareManifests, configDigest, countInvalidUtf8Bytes, countLines, decodeText, defaultIO, defineConfig, describeManifestShapeProblem, detectLanguage, detectPreset, displayPath, displayWidth, enforceBudget, escapeControls, estimateTokens, explainPath, fenceForContent, fenceForText, fetchLatestVersion, findConfigFile, findSecretInPath, firstRunMessage, formatNumber, formatTokens, formatUpdateNotice, generateInWorker, generateProjectTree, generateSarifReport, generateSnapshot, generateSnapshotAsync, getGitInfo, getPreset, gitignorePath, globToRegExp, hasPriorSnapshot, importsChangedFile, importsOfFile, importsOfText, installKind, installPreCommitHook, isBinaryBuffer, isBinaryExtension, isBinaryFile, isDiscoveryMode, isEffectivelyEmpty, isInteractive, isNewer, isResolvedConfig, isSensitivePath, isSkeletonizable, isTsxFileName, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, lockPathFor, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, planFirstRun, profileForPath, renderConfig, renderDiffText, renderExplainText, renumberParts, resolveChangedScope, resolveConfig, resolveSafeOutputDirs, resolveSpecifier, runInit, scanAndRedactSecrets, shouldCheck, skeletonizeTypeScript, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, updateCommand, validateSnapshot, workerEntry, wrapLines };
10332
+ export { ARTIFACT_NAMES, CHECK_INTERVAL_MS, CONFIG_KEYS, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, FAILURE_INTERVAL_MS, GENERATOR_NAME, GENERATOR_VERSION, HOOK_MARKER, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, MIN_MAX_PART_TOKENS, PACKAGE_LATEST_URL, PRESETS, PromptBack, PromptCancelled, REQUEST_TIMEOUT_MS, SCHEMA_VERSION, anchorForPath, appendGitignoreEntries, assertReplaceable, assertUsableConfigInput, buildImportResolver, bytesForTokens, cacheDirectory, cacheFilePath, checkForUpdate, classifyKind, classifyLayer, collectFiles, compareManifests, configDigest, countInvalidUtf8Bytes, countLines, decodeText, defaultIO, defineConfig, describeManifestShapeProblem, detectLanguage, detectPreset, displayPath, displayWidth, enforceBudget, escapeControls, estimateTokens, explainPath, fenceForContent, fenceForText, fetchLatestVersion, findConfigFile, findSecretInPath, firstRunMessage, formatNumber, formatTokens, formatUpdateNotice, generateInWorker, generateProjectTree, generateSarifReport, generateSnapshot, generateSnapshotAsync, getGitInfo, getPreset, gitignorePath, globToRegExp, hasPriorSnapshot, importsChangedFile, importsOfFile, importsOfText, installKind, installPreCommitHook, isBinaryBuffer, isBinaryExtension, isBinaryFile, isDiscoveryMode, isEffectivelyEmpty, isInteractive, isNewer, isResolvedConfig, isSensitivePath, isSkeletonizable, isTsxFileName, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, lockPathFor, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, planFirstRun, profileForPath, renderConfig, renderDiffText, renderExplainText, renumberParts, resolveChangedScope, resolveConfig, resolveSafeOutputDirs, resolveSpecifier, runInit, runWithStdoutCapture, scanAndRedactSecrets, shouldCheck, skeletonizeTypeScript, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, updateCommand, validateSnapshot, workerEntry, wrapLines };