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.
package/dist/index.cjs CHANGED
@@ -9,6 +9,7 @@ var ts = require('typescript');
9
9
  var module$1 = require('module');
10
10
  var url = require('url');
11
11
  var childProcess = require('child_process');
12
+ var http = require('http');
12
13
  var https = require('https');
13
14
  var readline = require('readline');
14
15
  require('worker_threads');
@@ -39,10 +40,56 @@ var os__namespace = /*#__PURE__*/_interopNamespace(os);
39
40
  var path16__namespace = /*#__PURE__*/_interopNamespace(path16);
40
41
  var ts__default = /*#__PURE__*/_interopDefault(ts);
41
42
  var childProcess__namespace = /*#__PURE__*/_interopNamespace(childProcess);
43
+ var http__namespace = /*#__PURE__*/_interopNamespace(http);
42
44
  var https__namespace = /*#__PURE__*/_interopNamespace(https);
43
45
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
44
46
 
45
47
  // src/index.ts
48
+ var hookStdoutScope = new async_hooks.AsyncLocalStorage();
49
+ var captureScope = new async_hooks.AsyncLocalStorage();
50
+ var installs = 0;
51
+ var underlying = null;
52
+ var router = null;
53
+ var asText = (chunk) => typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
54
+ var callbackIn = (rest) => {
55
+ for (const argument of rest) {
56
+ if (typeof argument === "function") return argument;
57
+ }
58
+ return void 0;
59
+ };
60
+ function installStdoutRouter() {
61
+ if (installs === 0) {
62
+ underlying = process.stdout.write;
63
+ const realStdout = underlying.bind(process.stdout);
64
+ const realStderr = process.stderr.write.bind(process.stderr);
65
+ router = ((...args) => {
66
+ if (hookStdoutScope.getStore() === true) return realStderr(...args);
67
+ const capture = captureScope.getStore();
68
+ if (capture !== void 0) {
69
+ capture.write(asText(args[0]));
70
+ const callback = callbackIn(args.slice(1));
71
+ if (callback) callback(null);
72
+ return true;
73
+ }
74
+ return realStdout(...args);
75
+ });
76
+ process.stdout.write = router;
77
+ }
78
+ installs += 1;
79
+ let released = false;
80
+ return () => {
81
+ if (released) return;
82
+ released = true;
83
+ installs -= 1;
84
+ if (installs === 0) {
85
+ if (process.stdout.write === router && underlying !== null) {
86
+ process.stdout.write = underlying;
87
+ }
88
+ underlying = null;
89
+ router = null;
90
+ }
91
+ };
92
+ }
46
93
 
47
94
  // src/ast/profiles/shared.ts
48
95
  var MAX_SUMMARY_CHARS = 1e6;
@@ -98,7 +145,7 @@ function isCapitalizedName(name) {
98
145
  }
99
146
 
100
147
  // src/ast/profiles/cfamily.ts
101
- var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
148
+ var INCLUDE = /^#\s*include\s*([<"])([^>"]+)[>"]/;
102
149
  var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
103
150
  var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
104
151
  var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
@@ -117,7 +164,7 @@ var cfamilyProfile = {
117
164
  }
118
165
  const included = INCLUDE.exec(trimmed);
119
166
  if (included) {
120
- summary.imports.push(included[1]);
167
+ summary.imports.push(included[1] === '"' ? `./${included[2]}` : included[2]);
121
168
  return;
122
169
  }
123
170
  const used = USING.exec(trimmed);
@@ -521,7 +568,15 @@ var pythonProfile = {
521
568
  }
522
569
  const from = FROM_IMPORT.exec(trimmed);
523
570
  if (from) {
524
- summary.imports.push(from[1]);
571
+ const module = from[1];
572
+ if (/^\.+$/.test(module)) {
573
+ for (const name of from[2].split(",")) {
574
+ const imported2 = name.replace(/[()]/g, "").trim().split(/\s+as\s+/)[0].trim();
575
+ if (/^[A-Za-z_]\w*$/.test(imported2)) summary.imports.push(`${module}${imported2}`);
576
+ }
577
+ } else {
578
+ summary.imports.push(module);
579
+ }
525
580
  routePending = false;
526
581
  return;
527
582
  }
@@ -570,7 +625,8 @@ var pythonProfile = {
570
625
  };
571
626
 
572
627
  // src/ast/profiles/ruby.ts
573
- var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
628
+ var REQUIRE_RELATIVE = /^require_relative\s+["']([^"']+)["']/;
629
+ var REQUIRE = /^require\s+["']([^"']+)["']/;
574
630
  var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
575
631
  var METHOD2 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
576
632
  var rubyProfile = {
@@ -581,6 +637,11 @@ var rubyProfile = {
581
637
  eachLine(content, (line) => {
582
638
  const trimmed = line.trim();
583
639
  if (!trimmed || trimmed.startsWith("#")) return;
640
+ const relative8 = REQUIRE_RELATIVE.exec(trimmed);
641
+ if (relative8) {
642
+ summary.imports.push(`./${relative8[1]}`);
643
+ return;
644
+ }
584
645
  const required = REQUIRE.exec(trimmed);
585
646
  if (required) {
586
647
  summary.imports.push(required[1]);
@@ -777,13 +838,23 @@ function summarizeTypeScriptSource(relativePath, content) {
777
838
  if (ts__default.default.isImportDeclaration(node) && ts__default.default.isStringLiteral(node.moduleSpecifier)) {
778
839
  summary.imports.push(node.moduleSpecifier.text);
779
840
  }
841
+ if (ts__default.default.isImportEqualsDeclaration(node) && ts__default.default.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression !== void 0 && ts__default.default.isStringLiteral(node.moduleReference.expression)) {
842
+ summary.imports.push(node.moduleReference.expression.text);
843
+ }
844
+ if (ts__default.default.isCallExpression(node) && node.arguments.length >= 1) {
845
+ const firstArgument = node.arguments[0];
846
+ if (firstArgument !== void 0 && ts__default.default.isStringLiteral(firstArgument)) {
847
+ const isDynamicImport = node.expression.kind === ts__default.default.SyntaxKind.ImportKeyword;
848
+ const isRequire = ts__default.default.isIdentifier(node.expression) && node.expression.text === "require";
849
+ if (isDynamicImport || isRequire) summary.imports.push(firstArgument.text);
850
+ }
851
+ }
780
852
  if (ts__default.default.isExportDeclaration(node)) {
781
853
  if (node.moduleSpecifier && ts__default.default.isStringLiteral(node.moduleSpecifier)) {
782
854
  summary.imports.push(node.moduleSpecifier.text);
855
+ summary.reExports.push(node.moduleSpecifier.text);
783
856
  if (node.exportClause && ts__default.default.isNamespaceExport(node.exportClause)) {
784
857
  summary.exports.push(node.exportClause.name.text);
785
- } else if (!node.exportClause) {
786
- summary.reExports.push(node.moduleSpecifier.text);
787
858
  }
788
859
  }
789
860
  if (node.exportClause && ts__default.default.isNamedExports(node.exportClause)) {
@@ -875,7 +946,8 @@ function summarizeTypeScriptSource(relativePath, content) {
875
946
  }
876
947
 
877
948
  // src/ast/profiles/sfc.ts
878
- var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
949
+ var SCRIPT_BLOCK = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
950
+ var JSX_LANG = /\blang\s*=\s*["']?[tj]sx\b/i;
879
951
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
880
952
  function componentName(path17) {
881
953
  const base = path17.split("/").pop() ?? path17;
@@ -886,13 +958,15 @@ var sfcProfile = {
886
958
  extensions: [".vue", ".svelte", ".astro"],
887
959
  summarize(path17, content) {
888
960
  let script = "";
961
+ let usesJsx = path17.toLowerCase().endsWith(".astro");
889
962
  if (path17.toLowerCase().endsWith(".astro")) {
890
963
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
891
964
  if (frontmatter) script += `${frontmatter[1]}
892
965
  `;
893
966
  }
894
967
  for (const match of content.matchAll(SCRIPT_BLOCK)) {
895
- script += `${match[1]}
968
+ if (JSX_LANG.test(match[1] ?? "")) usesJsx = true;
969
+ script += `${match[2]}
896
970
  `;
897
971
  }
898
972
  const name = componentName(path17);
@@ -904,7 +978,7 @@ var sfcProfile = {
904
978
  }
905
979
  return { summary: finalizeSummary(summary2), quality: "heuristic" };
906
980
  }
907
- const summary = summarizeTypeScriptSource(`${name}.ts`, script);
981
+ const summary = summarizeTypeScriptSource(`${name}.${usesJsx ? "tsx" : "ts"}`, script);
908
982
  summary.reactComponents.push(name);
909
983
  summary.exports.push(name);
910
984
  return { summary: finalizeSummary(summary), quality: "ast" };
@@ -1096,124 +1170,421 @@ function summarizeWithProfile(path17, content) {
1096
1170
  const profile = profileForPath(path17);
1097
1171
  return profile ? profile.summarize(path17, content) : null;
1098
1172
  }
1099
-
1100
- // src/ast/structured.ts
1101
- function stripJsonComments(content) {
1102
- const text2 = content.replace(/^\uFEFF/, "");
1103
- let out = "";
1104
- let inString = false;
1105
- let inLineComment = false;
1106
- let inBlockComment = false;
1107
- for (let i = 0; i < text2.length; i++) {
1108
- const char = text2[i];
1109
- const next = text2[i + 1];
1110
- if (inLineComment) {
1111
- if (char === "\n") {
1112
- inLineComment = false;
1113
- out += char;
1114
- }
1173
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
1174
+ ".png",
1175
+ ".jpg",
1176
+ ".jpeg",
1177
+ ".gif",
1178
+ ".webp",
1179
+ ".avif",
1180
+ ".ico",
1181
+ ".bmp",
1182
+ ".tiff",
1183
+ ".pdf",
1184
+ ".zip",
1185
+ ".tar",
1186
+ ".gz",
1187
+ ".tgz",
1188
+ ".br",
1189
+ ".bz2",
1190
+ ".xz",
1191
+ ".zst",
1192
+ ".lz4",
1193
+ ".lzma",
1194
+ ".7z",
1195
+ ".rar",
1196
+ ".mp3",
1197
+ ".mp4",
1198
+ ".wav",
1199
+ ".ogg",
1200
+ ".webm",
1201
+ ".mov",
1202
+ ".avi",
1203
+ ".woff",
1204
+ ".woff2",
1205
+ ".ttf",
1206
+ ".otf",
1207
+ ".eot",
1208
+ ".wasm",
1209
+ ".node",
1210
+ ".exe",
1211
+ ".dll",
1212
+ ".dylib",
1213
+ ".so"
1214
+ ]);
1215
+ function isBinaryExtension(filePath) {
1216
+ const ext = path16__namespace.extname(filePath).toLowerCase();
1217
+ return BINARY_EXTENSIONS.has(ext);
1218
+ }
1219
+ function isBinaryBuffer(buffer) {
1220
+ return buffer.includes(0);
1221
+ }
1222
+ function countControlBytes(buffer) {
1223
+ let count = 0;
1224
+ for (let index = 0; index < buffer.length; index++) {
1225
+ const byte = buffer[index];
1226
+ if (byte < 32) {
1227
+ if (byte !== 9 && byte !== 10 && byte !== 13) count += 1;
1115
1228
  continue;
1116
1229
  }
1117
- if (inBlockComment) {
1118
- if (char === "*" && next === "/") {
1119
- inBlockComment = false;
1120
- i += 1;
1121
- }
1230
+ if (byte === 127) {
1231
+ count += 1;
1122
1232
  continue;
1123
1233
  }
1124
- if (inString) {
1125
- out += char;
1126
- if (char === "\\") {
1127
- if (next !== void 0) {
1128
- out += next;
1129
- i += 1;
1130
- }
1131
- } else if (char === '"') {
1132
- inString = false;
1234
+ if (byte === 194 && index + 1 < buffer.length) {
1235
+ const next = buffer[index + 1];
1236
+ if (next >= 128 && next <= 159) {
1237
+ count += 1;
1238
+ index += 1;
1133
1239
  }
1240
+ }
1241
+ }
1242
+ return count;
1243
+ }
1244
+ var CONTROL_BYTES_FLOOR = 4;
1245
+ function countInvalidUtf8Bytes(buffer) {
1246
+ let invalid = 0;
1247
+ let index = 0;
1248
+ const length = buffer.length;
1249
+ while (index < length) {
1250
+ const lead = buffer[index];
1251
+ if (lead < 128) {
1252
+ index += 1;
1134
1253
  continue;
1135
1254
  }
1136
- if (char === '"') {
1137
- inString = true;
1138
- out += char;
1255
+ let continuation = 0;
1256
+ let codePoint = 0;
1257
+ let minimum = 0;
1258
+ if ((lead & 224) === 192) {
1259
+ continuation = 1;
1260
+ codePoint = lead & 31;
1261
+ minimum = 128;
1262
+ } else if ((lead & 240) === 224) {
1263
+ continuation = 2;
1264
+ codePoint = lead & 15;
1265
+ minimum = 2048;
1266
+ } else if ((lead & 248) === 240) {
1267
+ continuation = 3;
1268
+ codePoint = lead & 7;
1269
+ minimum = 65536;
1270
+ } else {
1271
+ invalid += 1;
1272
+ index += 1;
1139
1273
  continue;
1140
1274
  }
1141
- if (char === "/" && next === "/") {
1142
- inLineComment = true;
1143
- i += 1;
1275
+ if (index + continuation >= length) {
1276
+ invalid += 1;
1277
+ index += 1;
1144
1278
  continue;
1145
1279
  }
1146
- if (char === "/" && next === "*") {
1147
- inBlockComment = true;
1148
- i += 1;
1280
+ let wellFormed = true;
1281
+ for (let offset = 1; offset <= continuation; offset++) {
1282
+ const next = buffer[index + offset];
1283
+ if ((next & 192) !== 128) {
1284
+ wellFormed = false;
1285
+ break;
1286
+ }
1287
+ codePoint = codePoint << 6 | next & 63;
1288
+ }
1289
+ const isSurrogate = codePoint >= 55296 && codePoint <= 57343;
1290
+ if (!wellFormed || codePoint < minimum || codePoint > 1114111 || isSurrogate) {
1291
+ invalid += 1;
1292
+ index += 1;
1149
1293
  continue;
1150
1294
  }
1151
- out += char;
1295
+ index += continuation + 1;
1152
1296
  }
1153
- return out;
1297
+ return invalid;
1154
1298
  }
1155
- function summarizeJson(content) {
1156
- try {
1157
- const parsed = JSON.parse(stripJsonComments(content));
1158
- if (Array.isArray(parsed)) {
1159
- const sample = parsed[0];
1160
- const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
1161
- return {
1162
- shape: "array",
1163
- itemCount: parsed.length,
1164
- keys: [],
1165
- sampleKeys
1166
- };
1299
+ function readBomEncoding(buffer) {
1300
+ if (buffer.length >= 2) {
1301
+ if (buffer[0] === 255 && buffer[1] === 254) return "utf-16le";
1302
+ if (buffer[0] === 254 && buffer[1] === 255) return "utf-16be";
1303
+ }
1304
+ return null;
1305
+ }
1306
+ function looksLikeUtf16(buffer) {
1307
+ if (buffer.length < 8 || buffer.length % 2 !== 0) return null;
1308
+ const sample = Math.min(buffer.length, 4096);
1309
+ let nulInLowByte = 0;
1310
+ let nulInHighByte = 0;
1311
+ const units = sample / 2;
1312
+ for (let index = 0; index < sample; index += 2) {
1313
+ if (buffer[index] === 0) nulInLowByte += 1;
1314
+ if (buffer[index + 1] === 0) nulInHighByte += 1;
1315
+ }
1316
+ if (nulInHighByte > units * 0.3 && nulInLowByte < units * 0.05) return "utf-16le";
1317
+ if (nulInLowByte > units * 0.3 && nulInHighByte < units * 0.05) return "utf-16be";
1318
+ return null;
1319
+ }
1320
+ function decodeUtf16(buffer, encoding) {
1321
+ const littleEndian = encoding === "utf-16le" ? buffer : Buffer.from(buffer.subarray(0, buffer.length - buffer.length % 2)).swap16();
1322
+ return new TextDecoder("utf-16le").decode(littleEndian);
1323
+ }
1324
+ function decodeText(buffer) {
1325
+ const bomEncoding = readBomEncoding(buffer);
1326
+ if (bomEncoding) {
1327
+ const text2 = decodeUtf16(buffer.subarray(2), bomEncoding);
1328
+ return { text: text2, encoding: bomEncoding, invalidBytes: 0, binary: false };
1329
+ }
1330
+ if (buffer.includes(0)) {
1331
+ const shape = looksLikeUtf16(buffer);
1332
+ if (shape) {
1333
+ return { text: decodeUtf16(buffer, shape), encoding: shape, invalidBytes: 0, binary: false };
1167
1334
  }
1168
- if (parsed && typeof parsed === "object") {
1169
- const keys = Object.keys(parsed).sort();
1335
+ const offset = buffer.indexOf(0);
1336
+ return {
1337
+ text: "",
1338
+ encoding: "utf-8",
1339
+ invalidBytes: 0,
1340
+ binary: true,
1341
+ binaryReason: `NUL byte at offset ${offset}`
1342
+ };
1343
+ }
1344
+ const invalidBytes = countInvalidUtf8Bytes(buffer);
1345
+ if (invalidBytes === 0) {
1346
+ const controlBytes = countControlBytes(buffer);
1347
+ if (controlBytes > CONTROL_BYTES_FLOOR && controlBytes * 100 > buffer.length) {
1170
1348
  return {
1171
- shape: "object",
1172
- itemCount: keys.length,
1173
- keys: keys.slice(0, 30),
1174
- sampleKeys: []
1349
+ text: "",
1350
+ encoding: "utf-8",
1351
+ invalidBytes: 0,
1352
+ binary: true,
1353
+ binaryReason: `${controlBytes} control byte(s) in ${buffer.length} B; valid UTF-8 but not text`
1175
1354
  };
1176
1355
  }
1177
1356
  return {
1178
- shape: typeof parsed,
1179
- itemCount: null,
1180
- keys: [],
1181
- sampleKeys: []
1357
+ text: new TextDecoder("utf-8").decode(buffer),
1358
+ encoding: "utf-8",
1359
+ invalidBytes: 0,
1360
+ binary: false
1182
1361
  };
1183
- } catch {
1362
+ }
1363
+ if (invalidBytes >= 32 && invalidBytes * 3 >= buffer.length) {
1184
1364
  return {
1185
- shape: "invalid-json",
1186
- itemCount: null,
1187
- keys: [],
1188
- sampleKeys: []
1365
+ text: "",
1366
+ encoding: "utf-8",
1367
+ invalidBytes,
1368
+ binary: true,
1369
+ binaryReason: `${invalidBytes} of ${buffer.length} bytes are not UTF-8; treated as binary`
1189
1370
  };
1190
1371
  }
1372
+ return {
1373
+ text: buffer.toString("utf8"),
1374
+ encoding: "utf-8-lossy",
1375
+ invalidBytes,
1376
+ binary: false
1377
+ };
1191
1378
  }
1192
- function summarizeMarkdown(content) {
1193
- const headings = [];
1194
- const frontmatterKeys = [];
1195
- const lines = content.replace(/^\uFEFF/, "").split("\n");
1196
- let inFrontmatter = false;
1197
- let fence = null;
1198
- let closesFrontmatter = false;
1199
- for (let index = 1; index < lines.length; index++) {
1200
- if (lines[index].trim() === "---") {
1201
- closesFrontmatter = true;
1202
- break;
1203
- }
1204
- }
1205
- for (let index = 0; index < lines.length; index++) {
1206
- const line = lines[index];
1207
- if (index === 0 && line.trim() === "---" && closesFrontmatter) {
1208
- inFrontmatter = true;
1209
- continue;
1210
- }
1379
+ var BINARY_SCAN_BYTES = 8 * 1024 * 1024;
1380
+ var CHUNK = 64 * 1024;
1381
+ var SHAPE_SAMPLE_BYTES = 4096;
1382
+ function isBinaryFile(absolutePath) {
1383
+ if (isBinaryExtension(absolutePath)) return true;
1384
+ let fd;
1385
+ try {
1386
+ fd = fs14__namespace.openSync(absolutePath, "r");
1387
+ const buffer = Buffer.alloc(CHUNK);
1388
+ let sample;
1389
+ let position = 0;
1390
+ while (position < BINARY_SCAN_BYTES) {
1391
+ const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
1392
+ const bytesRead = fs14__namespace.readSync(fd, buffer, 0, wanted, position);
1393
+ if (bytesRead === 0) return false;
1394
+ const chunk = buffer.subarray(0, bytesRead);
1395
+ if (sample === void 0) {
1396
+ sample = Buffer.from(chunk.subarray(0, SHAPE_SAMPLE_BYTES));
1397
+ }
1398
+ if (chunk.includes(0)) {
1399
+ const shape = readBomEncoding(sample) ?? looksLikeUtf16(sample);
1400
+ return shape === null;
1401
+ }
1402
+ position += bytesRead;
1403
+ }
1404
+ return false;
1405
+ } catch {
1406
+ return false;
1407
+ } finally {
1408
+ if (fd !== void 0) {
1409
+ try {
1410
+ fs14__namespace.closeSync(fd);
1411
+ } catch {
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1416
+
1417
+ // src/ast/imports.ts
1418
+ var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
1419
+ var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1420
+ var isTsSource = (relativePath) => {
1421
+ const lower = relativePath.toLowerCase();
1422
+ return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
1423
+ };
1424
+ function importsOfText(relativePath, text2) {
1425
+ if (Buffer.byteLength(text2, "utf8") > SUMMARY_MAX_SOURCE_BYTES) return [];
1426
+ if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
1427
+ return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
1428
+ }
1429
+ function importsOfFile(relativePath, absolutePath) {
1430
+ let buffer;
1431
+ try {
1432
+ const stats = fs14__namespace.statSync(absolutePath);
1433
+ if (!stats.isFile() || stats.size > SUMMARY_MAX_SOURCE_BYTES) return [];
1434
+ buffer = fs14__namespace.readFileSync(absolutePath);
1435
+ } catch {
1436
+ return [];
1437
+ }
1438
+ const decoded = decodeText(buffer);
1439
+ if (decoded.binary) return [];
1440
+ return importsOfText(relativePath, decoded.text);
1441
+ }
1442
+
1443
+ // src/ast/structured.ts
1444
+ function stripJsonComments(content) {
1445
+ const text2 = content.replace(/^\uFEFF/, "");
1446
+ const withoutComments = stripComments(text2);
1447
+ return stripTrailingCommas(withoutComments);
1448
+ }
1449
+ function stripComments(text2) {
1450
+ let out = "";
1451
+ let inString = false;
1452
+ let escaped = false;
1453
+ for (let index = 0; index < text2.length; index += 1) {
1454
+ const char = text2[index];
1455
+ if (inString) {
1456
+ out += char;
1457
+ if (escaped) escaped = false;
1458
+ else if (char === "\\") escaped = true;
1459
+ else if (char === '"') inString = false;
1460
+ continue;
1461
+ }
1462
+ if (char === '"') {
1463
+ inString = true;
1464
+ out += char;
1465
+ continue;
1466
+ }
1467
+ if (char === "/" && text2[index + 1] === "/") {
1468
+ while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
1469
+ index -= 1;
1470
+ continue;
1471
+ }
1472
+ if (char === "/" && text2[index + 1] === "*") {
1473
+ index += 2;
1474
+ while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
1475
+ if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
1476
+ index += 1;
1477
+ }
1478
+ index += 1;
1479
+ continue;
1480
+ }
1481
+ out += char;
1482
+ }
1483
+ return out;
1484
+ }
1485
+ function stripTrailingCommas(text2) {
1486
+ let out = "";
1487
+ let inString = false;
1488
+ let escaped = false;
1489
+ for (let index = 0; index < text2.length; index += 1) {
1490
+ const char = text2[index];
1491
+ if (inString) {
1492
+ out += char;
1493
+ if (escaped) escaped = false;
1494
+ else if (char === "\\") escaped = true;
1495
+ else if (char === '"') inString = false;
1496
+ continue;
1497
+ }
1498
+ if (char === '"') {
1499
+ inString = true;
1500
+ out += char;
1501
+ continue;
1502
+ }
1503
+ if (char === ",") {
1504
+ let next = index + 1;
1505
+ while (next < text2.length && /\s/.test(text2[next])) next += 1;
1506
+ if (text2[next] === "}" || text2[next] === "]") continue;
1507
+ }
1508
+ out += char;
1509
+ }
1510
+ return out;
1511
+ }
1512
+ function summarizeJson(content) {
1513
+ try {
1514
+ const parsed = JSON.parse(stripJsonComments(content));
1515
+ if (Array.isArray(parsed)) {
1516
+ const sample = parsed[0];
1517
+ const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
1518
+ return {
1519
+ shape: "array",
1520
+ itemCount: parsed.length,
1521
+ keys: [],
1522
+ sampleKeys
1523
+ };
1524
+ }
1525
+ if (parsed && typeof parsed === "object") {
1526
+ const keys = Object.keys(parsed).sort();
1527
+ return {
1528
+ shape: "object",
1529
+ itemCount: keys.length,
1530
+ keys: keys.slice(0, 30),
1531
+ sampleKeys: []
1532
+ };
1533
+ }
1534
+ return {
1535
+ shape: typeof parsed,
1536
+ itemCount: null,
1537
+ keys: [],
1538
+ sampleKeys: []
1539
+ };
1540
+ } catch {
1541
+ return {
1542
+ shape: "invalid-json",
1543
+ itemCount: null,
1544
+ keys: [],
1545
+ sampleKeys: []
1546
+ };
1547
+ }
1548
+ }
1549
+ function frontMatterEnd(lines) {
1550
+ if (lines.length < 3 || lines[0].trim() !== "---") return 0;
1551
+ for (let index = 1; index < lines.length; index += 1) {
1552
+ const line = lines[index];
1553
+ const candidate = line.trim();
1554
+ if (candidate === "---" || candidate === "...") {
1555
+ return index > 1 ? index : 0;
1556
+ }
1557
+ if (candidate === "") continue;
1558
+ if (/^#(\s|$)/.test(candidate)) continue;
1559
+ if (/^(`{3,}|~{3,})/.test(candidate)) return 0;
1560
+ if (/^#{1,6}\s+\S/.test(candidate)) return 0;
1561
+ const isYamlMapping = /^[A-Za-z0-9_.-]+\s*:(\s|$)/.test(candidate);
1562
+ const isListItem = /^-(?:(\s|$))/.test(candidate);
1563
+ const isIndented = /^[ \t]/.test(line);
1564
+ if (!isYamlMapping && !isListItem && !isIndented) return 0;
1565
+ }
1566
+ return 0;
1567
+ }
1568
+ function summarizeMarkdown(content) {
1569
+ const headings = [];
1570
+ const frontmatterKeys = [];
1571
+ const lines = content.replace(/^\uFEFF/, "").split(/\r\n|\n|\r/);
1572
+ let inFrontmatter = false;
1573
+ let fence = null;
1574
+ const frontMatterEndLine = frontMatterEnd(lines);
1575
+ for (let index = 0; index < lines.length; index++) {
1576
+ const line = lines[index];
1577
+ if (index === 0 && frontMatterEndLine > 0) {
1578
+ inFrontmatter = true;
1579
+ continue;
1580
+ }
1211
1581
  if (inFrontmatter) {
1212
- if (line.trim() === "---") {
1582
+ const trimmedFrontmatter = line.trim();
1583
+ if (trimmedFrontmatter === "---" || trimmedFrontmatter === "...") {
1213
1584
  inFrontmatter = false;
1214
1585
  continue;
1215
1586
  }
1216
- const match = /^([a-zA-Z0-9_-]+):/.exec(line);
1587
+ const match = /^([A-Za-z0-9_.-]+)\s*:/.exec(line);
1217
1588
  if (match) frontmatterKeys.push(match[1]);
1218
1589
  continue;
1219
1590
  }
@@ -1246,24 +1617,39 @@ function hasCollapsibleBody(node) {
1246
1617
  return body !== void 0 && ts__default.default.isBlock(body);
1247
1618
  }
1248
1619
  function isTsxFileName(fileName) {
1249
- return fileName.toLowerCase().endsWith(".tsx");
1620
+ const lower = fileName.toLowerCase();
1621
+ return lower.endsWith(".tsx") || lower.endsWith(".jsx");
1622
+ }
1623
+ function scriptKindFor(fileName) {
1624
+ const lower = fileName.toLowerCase();
1625
+ if (lower.endsWith(".tsx")) return ts__default.default.ScriptKind.TSX;
1626
+ if (lower.endsWith(".jsx")) return ts__default.default.ScriptKind.JSX;
1627
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs")) {
1628
+ return ts__default.default.ScriptKind.JS;
1629
+ }
1630
+ return ts__default.default.ScriptKind.TS;
1250
1631
  }
1251
1632
  function skeletonizeTypeScript(source, fileName = "file.ts") {
1252
- const scriptKind = isTsxFileName(fileName) ? ts__default.default.ScriptKind.TSX : ts__default.default.ScriptKind.TS;
1253
1633
  const sourceFile = ts__default.default.createSourceFile(
1254
1634
  fileName,
1255
1635
  source,
1256
1636
  ts__default.default.ScriptTarget.Latest,
1257
- true,
1258
- scriptKind
1637
+ // `setParentNodes: false`. Nothing here reads `.parent` — the one `getStart` call is given the
1638
+ // source file explicitly, which is the reason that argument exists. Asking for parent pointers
1639
+ // makes the parser run a second full pass over every node in the file to set them, on top of
1640
+ // the summary parse this same file already pays for.
1641
+ false,
1642
+ scriptKindFor(fileName)
1259
1643
  );
1260
1644
  const spans = [];
1261
1645
  const visit = (node) => {
1262
1646
  if ((ts__default.default.isFunctionDeclaration(node) || ts__default.default.isMethodDeclaration(node) || ts__default.default.isConstructorDeclaration(node) || ts__default.default.isGetAccessorDeclaration(node) || ts__default.default.isSetAccessorDeclaration(node) || ts__default.default.isFunctionExpression(node) || ts__default.default.isArrowFunction(node)) && hasCollapsibleBody(node)) {
1263
1647
  const body = node.body;
1264
- const start = body.getStart(sourceFile);
1265
- const end = body.getEnd();
1266
- if (end > start) spans.push({ start, end });
1648
+ if (body.statements.length > 0) {
1649
+ const start = body.getStart(sourceFile);
1650
+ const end = body.getEnd();
1651
+ if (end > start) spans.push({ start, end });
1652
+ }
1267
1653
  }
1268
1654
  ts__default.default.forEachChild(node, visit);
1269
1655
  };
@@ -1275,16 +1661,21 @@ function skeletonizeTypeScript(source, fileName = "file.ts") {
1275
1661
  if (last && span.start < last.end) continue;
1276
1662
  outermost.push(span);
1277
1663
  }
1278
- let text2 = source;
1279
- for (let index = outermost.length - 1; index >= 0; index -= 1) {
1280
- const span = outermost[index];
1281
- text2 = `${text2.slice(0, span.start)}{ /* ... */ }${text2.slice(span.end)}`;
1664
+ const chunks = [];
1665
+ let cursor = 0;
1666
+ for (const span of outermost) {
1667
+ chunks.push(source.slice(cursor, span.start), "{ /* ... */ }");
1668
+ cursor = span.end;
1282
1669
  }
1283
- return { text: text2, collapsed: outermost.length };
1670
+ chunks.push(source.slice(cursor));
1671
+ return { text: chunks.join(""), collapsed: outermost.length };
1284
1672
  }
1285
1673
  function isSkeletonizable(filePath) {
1286
1674
  const lower = filePath.toLowerCase();
1287
- return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts");
1675
+ if (lower.endsWith(".d.ts") || lower.endsWith(".d.mts") || lower.endsWith(".d.cts")) {
1676
+ return false;
1677
+ }
1678
+ 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");
1288
1679
  }
1289
1680
  var MANIFEST_FILES = [
1290
1681
  "package.json",
@@ -2522,9 +2913,18 @@ var DEFAULT_SECRET_PATTERNS = [
2522
2913
  // The unquoted twin for YAML, docker-compose and `.env`: those formats never
2523
2914
  // quote their values, so the rules above cannot see them. `valueGroup` keeps
2524
2915
  // `POSTGRES_PASSWORD:` in the output.
2916
+ //
2917
+ // This is the one unquoted rule without `NOT_AFTER_QUOTE`, and the lookbehind is a
2918
+ // string-literal guard rather than a precision rule: a credential assignment *inside* a
2919
+ // literal — `const dsn = "password=hunter2;host=db"`, `curl -d "password=hunter2"` — was
2920
+ // rejected before it could be considered, and a DSN is a credential in a string. What made the
2921
+ // lookbehind redundant is the shape of `UNQUOTED_LITERAL`: a value has to start with a word
2922
+ // character, so `["awsSecret:", "apiKey:"]` — the list of key names the lookbehind was added
2923
+ // for — cannot match here anyway. `redact.ts` still refuses a match whose separator is inside
2924
+ // the literal, which is the case that list actually is.
2525
2925
  rule: "credential-assignment-unquoted",
2526
2926
  pattern: new RegExp(
2527
- `${NOT_AFTER_QUOTE}${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2927
+ `${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
2528
2928
  "i"
2529
2929
  ),
2530
2930
  valueGroup: 1
@@ -2559,6 +2959,27 @@ var DEFAULT_SECRET_PATTERNS = [
2559
2959
  pattern: /(?<!["'])(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*(?<![=!<>])[:=](?!=)\s*["']([^"'\s]{8,256})["']/i,
2560
2960
  valueGroup: 1
2561
2961
  },
2962
+ {
2963
+ // `${VAR:-fallback}` — a default value in a shell script, a `.env`, docker-compose or a
2964
+ // GitHub Actions workflow.
2965
+ //
2966
+ // This shape defeated every rule above it, and each refusal was individually reasonable:
2967
+ // the quoted rules need a quote after the separator, and the unquoted ones need the value to
2968
+ // begin with a word character — a fallback begins with `$`. So
2969
+ // `POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-MyProdPassword123!}` was emitted whole, `.env` files
2970
+ // and compose files included, which is where that idiom is the norm.
2971
+ //
2972
+ // The **fallback** is what gets masked, not the whole expansion: the variable name is the
2973
+ // readable context, exactly as `POSTGRES_PASSWORD:` is kept by the rule above. A single
2974
+ // interpolation with no operator (`${DB_PASSWORD}`) still does not match — there is nothing
2975
+ // between the braces to mask, and `isTemplateInterpolation` keeps such a value readable.
2976
+ rule: "interpolation-fallback-secret",
2977
+ pattern: new RegExp(
2978
+ `(?:["']${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})\\}["'\`]?`,
2979
+ "i"
2980
+ ),
2981
+ valueGroup: 1
2982
+ },
2562
2983
  {
2563
2984
  // Covers `.env` / shell assignment syntax, which never quotes its values and
2564
2985
  // was therefore invisible to the rule above. Anchored to the line start and
@@ -2586,8 +3007,8 @@ var DEFAULT_SECRET_PATTERNS = [
2586
3007
  valueGroup: 1
2587
3008
  }
2588
3009
  ];
2589
- var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
2590
- var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
3010
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:(?!-----BEGIN)[\s\S])*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
3011
+ 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;
2591
3012
 
2592
3013
  // src/security/redact.ts
2593
3014
  var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
@@ -2681,6 +3102,20 @@ function toGlobal(rule) {
2681
3102
  return new RegExp(rule.pattern.source, [...flags].join(""));
2682
3103
  }
2683
3104
  var LINE_BREAK = /(\r\n|\n|\r)/;
3105
+ var LINE_TERMINATOR = /\r\n|\n|\r/g;
3106
+ var COMPILED_RULES = /* @__PURE__ */ new WeakMap();
3107
+ function compiledRuleFor(rule) {
3108
+ let compiled = COMPILED_RULES.get(rule);
3109
+ if (compiled === void 0) {
3110
+ compiled = {
3111
+ rule: rule.rule,
3112
+ pattern: toGlobal(rule),
3113
+ ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
3114
+ };
3115
+ COMPILED_RULES.set(rule, compiled);
3116
+ }
3117
+ return compiled;
3118
+ }
2684
3119
  function scanAndRedactSecrets(content, rules, filePath) {
2685
3120
  const findings = [];
2686
3121
  let redacted = content;
@@ -2692,18 +3127,15 @@ function scanAndRedactSecrets(content, rules, filePath) {
2692
3127
  blockPattern.lastIndex += 1;
2693
3128
  continue;
2694
3129
  }
2695
- const line = redacted.slice(0, match.index).split("\n").length;
2696
- const newlines = (match[0].match(/\n/g) ?? []).length;
3130
+ const before = redacted.slice(0, match.index);
3131
+ const line = (before.match(LINE_TERMINATOR) ?? []).length + 1;
3132
+ const newlines = (match[0].match(LINE_TERMINATOR) ?? []).length;
2697
3133
  findings.push({ rules: ["private-key-block"], line, path: filePath });
2698
- redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
3134
+ redacted = before + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
2699
3135
  blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
2700
3136
  }
2701
3137
  }
2702
- const compiled = rules.map((rule) => ({
2703
- rule: rule.rule,
2704
- pattern: toGlobal(rule),
2705
- ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
2706
- }));
3138
+ const compiled = rules.map(compiledRuleFor);
2707
3139
  const segments = redacted.split(LINE_BREAK);
2708
3140
  let lineNumber = 1;
2709
3141
  for (let index = 0; index < segments.length; index += 2) {
@@ -2721,8 +3153,10 @@ function scanAndRedactSecrets(content, rules, filePath) {
2721
3153
  }
2722
3154
  const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
2723
3155
  if (group && isSelfNamingConstant(line, group[0], group[1])) continue;
2724
- if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index)) continue;
2725
- if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index)) {
3156
+ if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index, group?.[0])) {
3157
+ continue;
3158
+ }
3159
+ if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index, filePath)) {
2726
3160
  continue;
2727
3161
  }
2728
3162
  if (group && isTemplateInterpolation(line, group[0], group[1])) continue;
@@ -2755,14 +3189,20 @@ function expandOverToken(line, start, end) {
2755
3189
  while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
2756
3190
  return { start: left, end: right };
2757
3191
  }
2758
- function startsInsideStringLiteral(line, matchIndex) {
3192
+ function startsInsideStringLiteral(line, matchIndex, valueStart) {
2759
3193
  let runStart = matchIndex;
2760
3194
  while (runStart > 0 && /[A-Za-z0-9_-]/.test(line[runStart - 1])) runStart -= 1;
2761
3195
  const before = runStart > 0 ? line[runStart - 1] : "";
2762
- return before === '"' || before === "'" || before === "`";
3196
+ if (before !== '"' && before !== "'" && before !== "`") return false;
3197
+ if (valueStart === void 0) return true;
3198
+ return /["'`]/.test(line.slice(matchIndex, valueStart));
2763
3199
  }
2764
3200
  function isTemplateInterpolation(line, start, end) {
2765
- return line[start] === "$" && line[start + 1] === "{" && line[end - 1] === "}";
3201
+ if (line[start] !== "$" || line[start + 1] !== "{" || line[end - 1] !== "}") {
3202
+ return false;
3203
+ }
3204
+ const inner = line.slice(start + 2, end - 1).trim();
3205
+ return /^[A-Za-z_$][A-Za-z0-9_$.()[\]]*$/.test(inner);
2766
3206
  }
2767
3207
  function isSelfNamingConstant(line, start, end) {
2768
3208
  const value = line.slice(start, end);
@@ -2787,9 +3227,19 @@ function isUnquotedValue(line, start) {
2787
3227
  const before = start > 0 ? line[start - 1] : "";
2788
3228
  return before !== '"' && before !== "'" && before !== "`";
2789
3229
  }
2790
- var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|declare|export|using)\s+$/;
3230
+ var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|using)\s+$/;
3231
+ var ASSIGNMENT_FILE = /\.(?:sh|bash|zsh|ksh|fish|env|ya?ml|toml|ini|conf|cfg|properties|tf|tfvars|service)$/i;
3232
+ var ASSIGNMENT_BASENAME = /^(?:\.env(?:\.[^/]*)?|\.envrc|\.profile|\.bashrc|\.zshrc|\.bash_profile)$/i;
3233
+ function isAssignmentFile(filePath) {
3234
+ if (filePath === void 0) return false;
3235
+ const normalised = filePath.replace(/\\/g, "/");
3236
+ if (ASSIGNMENT_FILE.test(normalised)) return true;
3237
+ const base = normalised.slice(normalised.lastIndexOf("/") + 1);
3238
+ return ASSIGNMENT_BASENAME.test(base);
3239
+ }
2791
3240
  var DECLARATION_WINDOW = 40;
2792
- function isDeclaredBinding(line, matchIndex) {
3241
+ function isDeclaredBinding(line, matchIndex, filePath) {
3242
+ if (isAssignmentFile(filePath)) return false;
2793
3243
  let runStart = matchIndex;
2794
3244
  while (runStart > 0 && /[A-Za-z0-9_$]/.test(line[runStart - 1])) runStart -= 1;
2795
3245
  const from = Math.max(0, runStart - DECLARATION_WINDOW);
@@ -2831,6 +3281,15 @@ function readLinkTarget(linkPath, fallback) {
2831
3281
  return fallback;
2832
3282
  }
2833
3283
  }
3284
+ function brokenLinkRecord(linkPath, relativePath, rootReal) {
3285
+ const rawTarget = readLinkTarget(linkPath, linkPath);
3286
+ const resolvedFallback = path16__namespace.resolve(path16__namespace.dirname(linkPath), rawTarget);
3287
+ return isInside(rootReal, resolvedFallback) ? { path: relativePath, reason: "read-error", detail: "broken symlink" } : {
3288
+ path: relativePath,
3289
+ reason: "outside-root",
3290
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
3291
+ };
3292
+ }
2834
3293
  function isInside(root, candidate) {
2835
3294
  const relative8 = path16__namespace.relative(root, candidate);
2836
3295
  return relative8 === "" || !relative8.startsWith(`..${path16__namespace.sep}`) && relative8 !== ".." && !path16__namespace.isAbsolute(relative8);
@@ -2920,6 +3379,13 @@ function isSensitivePath(relativePath) {
2920
3379
  return matchesSensitiveList(posixPath, base);
2921
3380
  }
2922
3381
  var GLOBAL_PATTERNS = /* @__PURE__ */ new WeakMap();
3382
+ var GLOB_REGEX_CACHE = /* @__PURE__ */ new Map();
3383
+ var GITIGNORE_REGEX_CACHE = /* @__PURE__ */ new Map();
3384
+ var REGEX_CACHE_LIMIT = 4096;
3385
+ function rememberCompiled(cache, key, value) {
3386
+ if (cache.size >= REGEX_CACHE_LIMIT) cache.clear();
3387
+ cache.set(key, value);
3388
+ }
2923
3389
  function globalPattern(pattern) {
2924
3390
  let compiled = GLOBAL_PATTERNS.get(pattern);
2925
3391
  if (compiled === void 0) {
@@ -2930,8 +3396,8 @@ function globalPattern(pattern) {
2930
3396
  }
2931
3397
  var DEFAULT_PATH_RULES = [...DEFAULT_SECRET_PATTERNS];
2932
3398
  function pathScanRules(extraRules) {
2933
- if (extraRules === void 0 || extraRules.length === 0) return DEFAULT_PATH_RULES;
2934
- return [...DEFAULT_SECRET_PATTERNS, ...extraRules];
3399
+ if (extraRules === void 0) return DEFAULT_PATH_RULES;
3400
+ return extraRules;
2935
3401
  }
2936
3402
  function findSecretInPath(relativePath, extraRules) {
2937
3403
  const posixPath = relativePath.replace(/\\/g, "/");
@@ -2980,11 +3446,22 @@ function maskSkipPaths(records, extraRules) {
2980
3446
  });
2981
3447
  }
2982
3448
  function globToRegExp(glob) {
3449
+ const cached = GLOB_REGEX_CACHE.get(glob);
3450
+ if (cached !== void 0) return cached;
2983
3451
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
2984
3452
  const anchored = normalized.startsWith("/");
2985
3453
  const body = anchored ? normalized.slice(1) : normalized;
2986
3454
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
2987
- return new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
3455
+ let compiled;
3456
+ try {
3457
+ compiled = new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
3458
+ } catch (error) {
3459
+ throw new Error(
3460
+ `"${glob}" is not a valid glob: ${error.message}. A character class must be ascending, as in "[a-z]".`
3461
+ );
3462
+ }
3463
+ rememberCompiled(GLOB_REGEX_CACHE, glob, compiled);
3464
+ return compiled;
2988
3465
  }
2989
3466
  function globBodyToRegExp(body, allowBraces) {
2990
3467
  let out = "";
@@ -3096,6 +3573,14 @@ function escapeRegExpChar(character) {
3096
3573
  return /[\\^$.*+?()[\]{}|/]/.test(character) ? `\\${character}` : character;
3097
3574
  }
3098
3575
  function matchGitignorePattern(target, pattern) {
3576
+ let compiled = GITIGNORE_REGEX_CACHE.get(pattern);
3577
+ if (compiled === void 0) {
3578
+ compiled = compileGitignorePattern(pattern);
3579
+ rememberCompiled(GITIGNORE_REGEX_CACHE, pattern, compiled);
3580
+ }
3581
+ return compiled.test(target);
3582
+ }
3583
+ function compileGitignorePattern(pattern) {
3099
3584
  const anchored = pattern.startsWith("/");
3100
3585
  const body = anchored ? pattern.slice(1) : pattern;
3101
3586
  let source = "";
@@ -3144,7 +3629,11 @@ function matchGitignorePattern(target, pattern) {
3144
3629
  source += escapeRegExpChar(character);
3145
3630
  }
3146
3631
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
3147
- return new RegExp(`${prefix}${source}(?:/.*)?$`).test(target);
3632
+ try {
3633
+ return new RegExp(`${prefix}${source}(?:/.*)?$`);
3634
+ } catch {
3635
+ return /$^/;
3636
+ }
3148
3637
  }
3149
3638
  function matchesGitignoreRule(rule, scoped, isDirectory) {
3150
3639
  if (!rule.dirOnly) return matchGitignorePattern(scoped, rule.pattern);
@@ -3174,14 +3663,31 @@ function gitignoreVerdict(layers, relativePath, isDirectory) {
3174
3663
  }
3175
3664
  return verdict;
3176
3665
  }
3177
- function defaultIgnoreReason(relativePath) {
3666
+ var DEFAULT_TOOL_DIR_NAMES = ["snapshot"];
3667
+ function toolDirNamesFor(toolOwnedPaths) {
3668
+ const names = new Set(DEFAULT_TOOL_DIR_NAMES);
3669
+ for (const entry of toolOwnedPaths) {
3670
+ const base = entry.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop();
3671
+ if (base !== void 0 && base.length > 0) names.add(base);
3672
+ }
3673
+ return [...names];
3674
+ }
3675
+ function isToolLeftoverName(name, dirNames) {
3676
+ for (const dirName of dirNames) {
3677
+ if (name.startsWith(`${dirName}.backup-`) || name.startsWith(`${dirName}.partial-`)) {
3678
+ return true;
3679
+ }
3680
+ }
3681
+ return false;
3682
+ }
3683
+ function defaultIgnoreReason(relativePath, toolDirNames = DEFAULT_TOOL_DIR_NAMES) {
3178
3684
  const posixPath = relativePath.replace(/\\/g, "/");
3179
3685
  const parts = posixPath.split("/");
3180
3686
  for (let i = 0; i < parts.length - 1; i++) {
3181
3687
  if (DEFAULT_IGNORED_DIRS.includes(parts[i])) {
3182
3688
  return `built-in ignore: directory "${parts[i]}"`;
3183
3689
  }
3184
- if (parts[i].startsWith("snapshot.backup-")) {
3690
+ if (isToolLeftoverName(parts[i], toolDirNames)) {
3185
3691
  return `built-in ignore: leftover snapshot backup "${parts[i]}"`;
3186
3692
  }
3187
3693
  }
@@ -3189,6 +3695,9 @@ function defaultIgnoreReason(relativePath) {
3189
3695
  if (DEFAULT_IGNORED_FILES.includes(base)) {
3190
3696
  return `built-in ignore: filename "${base}"`;
3191
3697
  }
3698
+ if (isToolLeftoverName(base, toolDirNames)) {
3699
+ return `built-in ignore: leftover snapshot backup "${base}"`;
3700
+ }
3192
3701
  return null;
3193
3702
  }
3194
3703
  function listIgnoredPaths(cwd) {
@@ -3257,41 +3766,72 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
3257
3766
  }
3258
3767
  const reported = /* @__PURE__ */ new Set();
3259
3768
  const kept = [];
3769
+ const report = (linkedPath, record) => {
3770
+ if (reported.has(linkedPath)) return;
3771
+ reported.add(linkedPath);
3772
+ skipped.push(record);
3773
+ };
3260
3774
  for (const candidate of candidates) {
3261
3775
  const parts = candidate.split("/");
3262
- let linkedDir = null;
3776
+ let linkedPath = null;
3263
3777
  for (let depth = 1; depth <= parts.length; depth += 1) {
3264
3778
  const prefix = parts.slice(0, depth).join("/");
3265
3779
  if (isLink(path16__namespace.join(cwd, prefix))) {
3266
- linkedDir = prefix;
3780
+ linkedPath = prefix;
3267
3781
  break;
3268
3782
  }
3269
3783
  }
3270
- if (linkedDir === null) {
3784
+ if (linkedPath === null) {
3271
3785
  kept.push(candidate);
3272
3786
  continue;
3273
3787
  }
3274
- if (reported.has(linkedDir)) continue;
3275
- reported.add(linkedDir);
3276
- const absolute = path16__namespace.join(cwd, linkedDir);
3277
- let targetReal = absolute;
3788
+ const absolute = path16__namespace.join(cwd, linkedPath);
3789
+ const rawTarget = readLinkTarget(absolute, absolute);
3790
+ let targetReal = null;
3278
3791
  try {
3279
3792
  targetReal = fs14__namespace.realpathSync(absolute);
3280
3793
  } catch {
3794
+ targetReal = null;
3281
3795
  }
3282
- const target = readLinkTarget(absolute, targetReal);
3283
- const what = linkedDir === candidate ? "symbolic link" : "directory link";
3284
- skipped.push(
3285
- isInside(rootReal, targetReal) ? {
3286
- path: linkedDir,
3287
- reason: "symlink",
3288
- detail: `${what} not followed (target: ${target})`
3289
- } : {
3290
- path: linkedDir,
3796
+ if (targetReal === null) {
3797
+ report(linkedPath, brokenLinkRecord(absolute, linkedPath, rootReal));
3798
+ continue;
3799
+ }
3800
+ if (!isInside(rootReal, targetReal)) {
3801
+ report(linkedPath, {
3802
+ path: linkedPath,
3291
3803
  reason: "outside-root",
3292
- detail: `link target resolves outside the scanned root (target: ${target}); not followed`
3293
- }
3294
- );
3804
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
3805
+ });
3806
+ continue;
3807
+ }
3808
+ let stats = null;
3809
+ try {
3810
+ stats = fs14__namespace.statSync(absolute);
3811
+ } catch {
3812
+ stats = null;
3813
+ }
3814
+ if (stats === null) {
3815
+ report(linkedPath, { path: linkedPath, reason: "read-error", detail: "unreadable symlink" });
3816
+ continue;
3817
+ }
3818
+ if (linkedPath !== candidate || stats.isDirectory()) {
3819
+ report(linkedPath, {
3820
+ path: linkedPath,
3821
+ reason: "symlink",
3822
+ detail: `directory link not followed (target: ${rawTarget})`
3823
+ });
3824
+ continue;
3825
+ }
3826
+ if (!stats.isFile()) {
3827
+ report(linkedPath, {
3828
+ path: linkedPath,
3829
+ reason: "symlink",
3830
+ detail: `link to neither a file nor a directory (target: ${rawTarget})`
3831
+ });
3832
+ continue;
3833
+ }
3834
+ kept.push(candidate);
3295
3835
  }
3296
3836
  return kept;
3297
3837
  }
@@ -3338,6 +3878,7 @@ function collectFiles(cwd, options = {}) {
3338
3878
  (options.toolOwnedPaths ?? []).map((entry) => entry.replace(/\\/g, "/"))
3339
3879
  );
3340
3880
  const isToolOwned = makeToolOwnedDecider(cwd, [...toolOwnedPaths]);
3881
+ const toolDirNames = toolDirNamesFor(toolOwnedPaths);
3341
3882
  let candidates;
3342
3883
  const skipped = [];
3343
3884
  const discovery = options.discovery ?? "auto";
@@ -3366,7 +3907,7 @@ function collectFiles(cwd, options = {}) {
3366
3907
  }
3367
3908
  const files = candidates.filter((file) => {
3368
3909
  if (isToolOwned(file)) return false;
3369
- const ignored = defaultIgnoreReason(file);
3910
+ const ignored = defaultIgnoreReason(file, toolDirNames);
3370
3911
  if (ignored) {
3371
3912
  skipped.push({ path: file, reason: "default-ignore", detail: ignored });
3372
3913
  return false;
@@ -3421,6 +3962,7 @@ function rootRealFor(root) {
3421
3962
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
3422
3963
  const results = { files: [], skipped: [] };
3423
3964
  const stack = [];
3965
+ const toolDirNames = toolDirNamesFor(reservedPaths);
3424
3966
  const relativeToBase = (target) => path16__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
3425
3967
  const enter = (current, layers) => {
3426
3968
  if (visited.has(current)) return false;
@@ -3465,11 +4007,11 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3465
4007
  });
3466
4008
  continue;
3467
4009
  }
3468
- if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
4010
+ if (DEFAULT_IGNORED_FILES.includes(entry.name) || isToolLeftoverName(entry.name, toolDirNames)) {
3469
4011
  results.skipped.push({
3470
4012
  path: relPath,
3471
4013
  reason: "default-ignore",
3472
- detail: "built-in ignore: filename"
4014
+ detail: isToolLeftoverName(entry.name, toolDirNames) ? `built-in ignore: leftover snapshot backup "${entry.name}" (not descended)` : "built-in ignore: filename"
3473
4015
  });
3474
4016
  continue;
3475
4017
  }
@@ -3490,7 +4032,7 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
3490
4032
  try {
3491
4033
  targetReal = fs14__namespace.realpathSync(fullPath);
3492
4034
  } catch {
3493
- results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
4035
+ results.skipped.push(brokenLinkRecord(fullPath, relPath, rootReal));
3494
4036
  continue;
3495
4037
  }
3496
4038
  let stats;
@@ -3615,15 +4157,19 @@ function normalizeBudgets(budgets, maxPartBytes) {
3615
4157
  }
3616
4158
  function normalizeResolvedConfig(config) {
3617
4159
  const discovery = isDiscoveryMode(config.discovery) ? config.discovery : "auto";
4160
+ const monolithWanted = config.includeMonolith === true || config.artifacts?.monolith === true;
4161
+ const artifacts = resolveArtifacts(
4162
+ config.artifacts,
4163
+ void 0,
4164
+ monolithWanted,
4165
+ monolithWanted
4166
+ );
3618
4167
  return markResolvedConfig({
3619
4168
  ...config,
3620
4169
  discovery,
4170
+ includeMonolith: artifacts.monolith,
3621
4171
  secretRules: compileSecretRules(config.secretRules),
3622
- artifacts: resolveArtifacts(
3623
- config.artifacts,
3624
- void 0,
3625
- config.includeMonolith === true
3626
- ),
4172
+ artifacts,
3627
4173
  include: resolveInclude(config.include),
3628
4174
  preset: typeof config.preset === "string" ? config.preset : "generic",
3629
4175
  presetExclude: [
@@ -3702,9 +4248,9 @@ function stripJsonComments2(raw) {
3702
4248
  }
3703
4249
  out += char;
3704
4250
  }
3705
- return stripTrailingCommas(out);
4251
+ return stripTrailingCommas2(out);
3706
4252
  }
3707
- function stripTrailingCommas(text2) {
4253
+ function stripTrailingCommas2(text2) {
3708
4254
  let out = "";
3709
4255
  let inString = false;
3710
4256
  let escaped = false;
@@ -3832,6 +4378,16 @@ async function importConfigModule(fullPath) {
3832
4378
  url$1.searchParams.set("codebaseSnapshotLoad", String(moduleConfigLoadToken));
3833
4379
  return await import(url$1.href);
3834
4380
  }
4381
+ function attachConfigDir(config, filePath) {
4382
+ if (typeof config === "object" && config !== null) {
4383
+ Object.defineProperty(config, CONFIG_DIR_SYMBOL, {
4384
+ value: path16__namespace.dirname(filePath),
4385
+ enumerable: false,
4386
+ configurable: true
4387
+ });
4388
+ }
4389
+ return config;
4390
+ }
3835
4391
  async function loadConfigFileDetailed(cwd) {
3836
4392
  const warnings = [];
3837
4393
  const errors = [];
@@ -3847,7 +4403,7 @@ async function loadConfigFileDetailed(cwd) {
3847
4403
  );
3848
4404
  }
3849
4405
  assertKnownConfigKeys(config, fullPath);
3850
- return { config, file: fullPath, warnings, errors };
4406
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
3851
4407
  } catch (err) {
3852
4408
  const raw = err.message;
3853
4409
  const message = raw.includes(fullPath) ? raw : `${fullPath}: ${raw}`;
@@ -3866,7 +4422,7 @@ async function loadConfigFileDetailed(cwd) {
3866
4422
  const imported = await importConfigModule(fullPath);
3867
4423
  const hasDefault = Object.prototype.hasOwnProperty.call(imported, "default");
3868
4424
  const config = readConfigExport(hasDefault ? imported.default : imported, fullPath);
3869
- return { config, file: fullPath, warnings, errors };
4425
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
3870
4426
  } catch (err) {
3871
4427
  const message = `Could not load ${fullPath}: ${err.message}`;
3872
4428
  warnings.push(message);
@@ -3884,7 +4440,12 @@ async function loadConfigFileDetailed(cwd) {
3884
4440
  snapshotKey,
3885
4441
  `${pkgJsonPath} (the "snapshot" key)`
3886
4442
  );
3887
- return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
4443
+ return {
4444
+ config: attachConfigDir(snapshotKey, pkgJsonPath),
4445
+ file: pkgJsonPath,
4446
+ warnings,
4447
+ errors
4448
+ };
3888
4449
  }
3889
4450
  if (snapshotKey !== void 0) {
3890
4451
  const kind = Array.isArray(snapshotKey) ? "array" : typeof snapshotKey;
@@ -3902,13 +4463,6 @@ async function loadConfigFileDetailed(cwd) {
3902
4463
  }
3903
4464
  async function loadConfigFile(cwd) {
3904
4465
  const loaded = await loadConfigFileDetailed(cwd);
3905
- if (loaded.config && loaded.file && typeof loaded.config === "object") {
3906
- Object.defineProperty(loaded.config, CONFIG_DIR_SYMBOL, {
3907
- value: path16__namespace.dirname(loaded.file),
3908
- enumerable: false,
3909
- configurable: true
3910
- });
3911
- }
3912
4466
  if (loaded.errors.length > 0) {
3913
4467
  throw new Error(`The config file could not be read: ${loaded.errors.join("; ")}`);
3914
4468
  }
@@ -3937,7 +4491,7 @@ var DEFAULT_ARTIFACTS = {
3937
4491
  function isArtifactName(value) {
3938
4492
  return ARTIFACT_NAMES.includes(value);
3939
4493
  }
3940
- function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
4494
+ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride, only) {
3941
4495
  const artifacts = {
3942
4496
  ...DEFAULT_ARTIFACTS,
3943
4497
  // `includeMonolith` predates this selection and stays the switch for it.
@@ -3962,6 +4516,21 @@ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride)
3962
4516
  if (monolithOverride !== void 0) {
3963
4517
  artifacts.monolith = monolithOverride;
3964
4518
  }
4519
+ if (only !== void 0 && only.length > 0) {
4520
+ const keep = /* @__PURE__ */ new Set();
4521
+ for (const name of only) {
4522
+ if (!isArtifactName(name)) {
4523
+ throw new Error(
4524
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
4525
+ );
4526
+ }
4527
+ keep.add(name);
4528
+ }
4529
+ for (const name of ARTIFACT_NAMES) {
4530
+ artifacts[name] = keep.has(name);
4531
+ }
4532
+ return artifacts;
4533
+ }
3965
4534
  for (const name of skipped ?? []) {
3966
4535
  if (!isArtifactName(name)) {
3967
4536
  throw new Error(
@@ -4262,7 +4831,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
4262
4831
  userConfig?.artifacts,
4263
4832
  cliOptions?.skipArtifacts,
4264
4833
  includeMonolith,
4265
- cliMonolith
4834
+ cliMonolith,
4835
+ cliOptions?.onlyArtifacts
4266
4836
  );
4267
4837
  const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
4268
4838
  const maxPartBytes = boundedBytes(
@@ -4436,7 +5006,18 @@ function configDigest(config) {
4436
5006
  excludePatterns: config.excludePatterns,
4437
5007
  preset: config.preset,
4438
5008
  layersMode: config.layersMode,
4439
- budgets: config.budgets,
5009
+ // `budgets.byLayer` is a plain object, and `JSON.stringify` preserves insertion order — so
5010
+ // `{ docs, api }` and `{ api, docs }` hashed differently despite being the same settings, and
5011
+ // `validate` reported configuration drift for a snapshot nobody had changed. The keys are
5012
+ // canonicalised here; nothing else about the object is order-dependent.
5013
+ budgets: {
5014
+ default: config.budgets.default,
5015
+ byLayer: Object.fromEntries(
5016
+ Object.entries(config.budgets.byLayer).sort(
5017
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
5018
+ )
5019
+ )
5020
+ },
4440
5021
  // The matcher is what makes a layer a layer. Hashing only the names meant that
4441
5022
  // moving `{ name: "api", match: "src/api/**" }` to `"src/v2/**"` produced the same
4442
5023
  // digest, so `validate` reported "valid and up to date" for a snapshot whose files
@@ -4720,9 +5301,21 @@ function generateSarifReport(manifest, findings, rules, toolVersion) {
4720
5301
  }
4721
5302
 
4722
5303
  // src/partitioner/preview.ts
5304
+ var LINE_ENDINGS = /\r\n|\r/g;
5305
+ var normaliseLineEndings = (content) => content.replace(LINE_ENDINGS, "\n");
4723
5306
  function countLines(content) {
4724
5307
  if (content.length === 0) return 0;
4725
- return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
5308
+ let breaks = 0;
5309
+ for (let index = 0; index < content.length; index += 1) {
5310
+ const code = content.charCodeAt(index);
5311
+ if (code === 10) {
5312
+ breaks += 1;
5313
+ continue;
5314
+ }
5315
+ if (code === 13 && content.charCodeAt(index + 1) !== 10) breaks += 1;
5316
+ }
5317
+ const last = content.charCodeAt(content.length - 1);
5318
+ return last === 10 || last === 13 ? breaks : breaks + 1;
4726
5319
  }
4727
5320
  function truncateToBytes(content, maxBytes) {
4728
5321
  if (maxBytes <= 0) return { content: "", isTruncated: content.length > 0 };
@@ -4733,17 +5326,18 @@ function truncateToBytes(content, maxBytes) {
4733
5326
  return { content: slice, isTruncated: true };
4734
5327
  }
4735
5328
  function truncatePreview(content, headLines, tailLines) {
4736
- const lines = content.split("\n");
4737
- const total = countLines(content);
5329
+ const normalised = normaliseLineEndings(content);
5330
+ const lines = normalised.split("\n");
5331
+ const total = countLines(normalised);
4738
5332
  if (total <= headLines + tailLines) {
4739
5333
  return {
4740
- content,
5334
+ content: normalised,
4741
5335
  isTruncated: false,
4742
5336
  originalLines: total,
4743
5337
  includedLines: total
4744
5338
  };
4745
5339
  }
4746
- const window = content.endsWith("\n") ? lines.slice(0, -1) : lines;
5340
+ const window = normalised.endsWith("\n") ? lines.slice(0, -1) : lines;
4747
5341
  const head = window.slice(0, headLines);
4748
5342
  const tail = tailLines > 0 ? window.slice(window.length - tailLines) : [];
4749
5343
  const omitted = total - head.length - tail.length;
@@ -4797,23 +5391,55 @@ function truncateWithBudgetMarker(content, maxBytes, budget, originalLines) {
4797
5391
  };
4798
5392
  }
4799
5393
  function partitionFiles(files, maxPartBytes, contentFingerprint) {
4800
- if (files.length === 0) return [];
4801
- const budget = Math.max(MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES, maxPartBytes);
4802
- const sectionBudget = budget - HEADER_RESERVE_BYTES;
4803
- const prepared = files.map((file) => prepareSection(file, sectionBudget));
4804
- let groups = greedyGroups(prepared, budget);
5394
+ return partitionFilesForBuckets([{ files, budgetBytes: maxPartBytes }], contentFingerprint);
5395
+ }
5396
+ function partitionFilesForBuckets(buckets, contentFingerprint) {
5397
+ const groups = [];
5398
+ for (const bucket of buckets) {
5399
+ if (bucket.files.length === 0) continue;
5400
+ const budget = Math.max(
5401
+ MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES,
5402
+ bucket.budgetBytes
5403
+ );
5404
+ const sectionBudget = budget - HEADER_RESERVE_BYTES;
5405
+ const prepared = bucket.files.map((file) => prepareSection(file, sectionBudget));
5406
+ for (const sections of greedyGroups(prepared, budget)) groups.push({ sections, budget });
5407
+ }
5408
+ if (groups.length === 0) return [];
5409
+ const measurements = /* @__PURE__ */ new Map();
5410
+ const widthOf = (total) => Math.max(3, String(total).length);
5411
+ const measure = (group, index, total) => {
5412
+ const cached = measurements.get(group);
5413
+ const width = widthOf(total);
5414
+ if (cached !== void 0 && cached.index === index && cached.width === width) {
5415
+ return cached.size;
5416
+ }
5417
+ const { content } = assembleGroup(group, index, total, contentFingerprint);
5418
+ const size2 = Buffer.byteLength(content, "utf8");
5419
+ measurements.set(group, { index, width, size: size2 });
5420
+ return size2;
5421
+ };
4805
5422
  for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
4806
- const rendered = renderGroups(groups, budget, contentFingerprint);
4807
- const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
4808
- if (oversized === -1) return rendered;
5423
+ const total = groups.length;
5424
+ let oversized = -1;
5425
+ for (let index = 0; index < total; index += 1) {
5426
+ const group2 = groups[index];
5427
+ if (measure(group2, index, total) > group2.budget) {
5428
+ oversized = index;
5429
+ break;
5430
+ }
5431
+ }
5432
+ if (oversized === -1) return renderGroups(groups, contentFingerprint);
4809
5433
  const group = groups[oversized];
4810
- if (group.length > 1) {
4811
- const moved = group.pop();
4812
- groups.splice(oversized + 1, 0, [moved]);
5434
+ if (group.sections.length > 1) {
5435
+ const moved = group.sections.pop();
5436
+ groups.splice(oversized + 1, 0, { sections: [moved], budget: group.budget });
5437
+ measurements.delete(group);
4813
5438
  continue;
4814
5439
  }
4815
- const [section] = group;
4816
- const overflow = rendered[oversized].metadata.byteSize - budget;
5440
+ const [section] = group.sections;
5441
+ if (section === void 0) return renderGroups(groups, contentFingerprint);
5442
+ const overflow = measure(group, oversized, total) - group.budget;
4817
5443
  const currentContent = section.file.content ?? "";
4818
5444
  const shrinkTo = Math.max(
4819
5445
  0,
@@ -4822,26 +5448,33 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
4822
5448
  const shrunk = truncateWithBudgetMarker(
4823
5449
  currentContent,
4824
5450
  shrinkTo,
4825
- budget,
5451
+ group.budget,
4826
5452
  section.file.originalLines
4827
5453
  );
4828
5454
  if (shrunk.isTruncated) {
4829
5455
  applyTruncatedContent(section.file, shrunk.content);
4830
5456
  section.text = renderFileSection(section.file);
4831
5457
  section.byteSize = Buffer.byteLength(section.text, "utf8");
5458
+ measurements.delete(group);
4832
5459
  continue;
4833
5460
  }
5461
+ const rendered = renderGroups(groups, contentFingerprint);
4834
5462
  const marker2 = TRUNCATION_MARKER;
4835
- assertBudgetHoldsMarker(budget);
5463
+ assertBudgetHoldsMarker(group.budget);
4836
5464
  const target = Math.max(
4837
5465
  0,
4838
- budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
5466
+ group.budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
4839
5467
  );
4840
5468
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
4841
- rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
5469
+ rendered[oversized] = rehashPart(
5470
+ rendered[oversized],
5471
+ clipped.content + marker2
5472
+ );
4842
5473
  return rendered;
4843
5474
  }
4844
- return enforceBudget(renderGroups(groups, budget, contentFingerprint), budget);
5475
+ return renderGroups(groups, contentFingerprint).flatMap(
5476
+ (part) => enforceBudget([part], part.metadata.budgetBytes)
5477
+ );
4845
5478
  }
4846
5479
  var TRUNCATION_MARKER = "\n... [part truncated: cannot fit budget] ...\n";
4847
5480
  function assertBudgetHoldsMarker(budget) {
@@ -4926,7 +5559,7 @@ function renumberParts(parts, contentFingerprint) {
4926
5559
  (part) => enforceBudget([part], part.metadata.budgetBytes)[0] ?? part
4927
5560
  );
4928
5561
  }
4929
- var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
5562
+ var SECTION_TRUNCATION_NOTICE = "... [section truncated: the fixed metadata and a code fence did not fit the part budget] ...";
4930
5563
  var METADATA_VALUE_MAX = 200;
4931
5564
  var DISPLAY_PATH_MAX = 300;
4932
5565
  function capValue(value, max) {
@@ -4968,13 +5601,57 @@ function prepareSection(file, sectionBudget) {
4968
5601
  shrinkBody();
4969
5602
  }
4970
5603
  if (byteSize > sectionBudget) {
4971
- const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
4972
- const truncated = truncateToBytes(text2, Math.max(0, target));
4973
- text2 = truncated.content + SECTION_TRUNCATION_NOTICE;
5604
+ applyTruncatedContent(file, SECTION_TRUNCATION_NOTICE);
5605
+ file.summary = void 0;
5606
+ file.summaryOmitted = true;
5607
+ text2 = renderFileSection(file);
4974
5608
  byteSize = Buffer.byteLength(text2, "utf8");
5609
+ if (byteSize > sectionBudget) {
5610
+ text2 = truncateRenderedSection(text2, sectionBudget, fenceForContent(file.content ?? ""));
5611
+ byteSize = Buffer.byteLength(text2, "utf8");
5612
+ }
4975
5613
  }
4976
5614
  return { file, text: text2, byteSize };
4977
5615
  }
5616
+ function truncateRenderedSection(text2, sectionBudget, fence) {
5617
+ const closing = `
5618
+ ${SECTION_TRUNCATION_NOTICE}
5619
+ ${fence}
5620
+ `;
5621
+ const bodyRoom = sectionBudget - Buffer.byteLength(closing, "utf8");
5622
+ if (bodyRoom > 0) {
5623
+ const cut2 = truncateToBytes(text2, bodyRoom).content;
5624
+ const lastBreak2 = cut2.lastIndexOf("\n");
5625
+ const kept = lastBreak2 >= 0 ? cut2.slice(0, lastBreak2 + 1) : "";
5626
+ if (fenceIsOpen(kept, fence)) return `${kept}${closing}`;
5627
+ }
5628
+ const probe = text2.slice(0, 512);
5629
+ const emptyFence = fenceForText(probe.slice(0, Math.max(0, probe.lastIndexOf("\n") + 1)), 4);
5630
+ const block = `
5631
+ ${emptyFence}
5632
+ ${SECTION_TRUNCATION_NOTICE}
5633
+ ${emptyFence}
5634
+ `;
5635
+ const prefixRoom = sectionBudget - Buffer.byteLength(block, "utf8");
5636
+ const cut = prefixRoom > 0 ? truncateToBytes(text2, prefixRoom).content : "";
5637
+ const lastBreak = cut.lastIndexOf("\n");
5638
+ const prefix = lastBreak >= 0 ? cut.slice(0, lastBreak + 1) : "";
5639
+ return `${dropTrailingOpenFence(prefix, fence)}${block}`;
5640
+ }
5641
+ function dropTrailingOpenFence(text2, fence) {
5642
+ if (!fenceIsOpen(text2, fence)) return text2;
5643
+ const lines = text2.split("\n");
5644
+ const opening = lines.findIndex((line) => line.startsWith(fence));
5645
+ if (opening < 0) return text2;
5646
+ return lines.slice(0, opening).join("\n") + (opening > 0 ? "\n" : "");
5647
+ }
5648
+ function fenceIsOpen(text2, fence) {
5649
+ let open = false;
5650
+ for (const line of text2.split("\n")) {
5651
+ if (line.startsWith(fence)) open = !open;
5652
+ }
5653
+ return open;
5654
+ }
4978
5655
  function applyTruncatedContent(file, content) {
4979
5656
  file.content = content;
4980
5657
  file.isPreviewTruncated = true;
@@ -5022,52 +5699,64 @@ function greedyGroups(sections, budget) {
5022
5699
  if (current.length > 0) groups.push(current);
5023
5700
  return groups;
5024
5701
  }
5025
- function renderGroups(groups, budget, contentFingerprint) {
5702
+ function assembleGroup(group, index, total, contentFingerprint) {
5703
+ const width = Math.max(3, String(total).length);
5704
+ const zeroPad = (value) => String(value).padStart(width, "0");
5705
+ const filename = `part-${zeroPad(index + 1)}.md`;
5706
+ const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
5707
+ const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
5708
+ for (const section of group.sections) {
5709
+ section.file.part = filename;
5710
+ }
5711
+ const layerCounts = {};
5712
+ const kindCounts = {};
5713
+ for (const section of group.sections) {
5714
+ layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
5715
+ kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
5716
+ }
5717
+ const pathLines = group.sections.map((s) => displayPath(s.file.relativePath));
5718
+ const listFence = fenceForText(pathLines.join("\n"), 3);
5719
+ const header = [
5720
+ `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
5721
+ "",
5722
+ `- Previous: ${prev}`,
5723
+ `- Next: ${next}`,
5724
+ `- Files: ${group.sections.length}`,
5725
+ `- Content Fingerprint: ${contentFingerprint || "none"}`,
5726
+ `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
5727
+ `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
5728
+ "",
5729
+ UNTRUSTED_CONTENT_NOTICE,
5730
+ "",
5731
+ "## Files",
5732
+ "",
5733
+ `${listFence}text`,
5734
+ ...pathLines,
5735
+ listFence,
5736
+ "",
5737
+ "## Contents",
5738
+ ""
5739
+ ].join("\n");
5740
+ const assembled = (header + group.sections.map((s) => s.text).join("")).replace(
5741
+ /\r\n|\r/g,
5742
+ "\n"
5743
+ );
5744
+ const estimatedTokens = estimateTokens(assembled);
5745
+ return { filename, content: insertTokenLine(assembled, estimatedTokens), estimatedTokens };
5746
+ }
5747
+ function renderGroups(groups, contentFingerprint) {
5026
5748
  const total = groups.length;
5027
- const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
5028
5749
  return groups.map((group, index) => {
5029
- const filename = `part-${zeroPad(index + 1)}.md`;
5030
- const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
5031
- const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
5032
- for (const section of group) {
5033
- section.file.part = filename;
5034
- }
5035
- const layersCovered = Array.from(new Set(group.map((s) => s.file.layer))).sort();
5036
- const layerCounts = {};
5037
- const kindCounts = {};
5038
- for (const section of group) {
5039
- layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
5040
- kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
5041
- }
5042
- const pathLines = group.map((s) => displayPath(s.file.relativePath));
5043
- const listFence = fenceForText(pathLines.join("\n"), 3);
5044
- const header = [
5045
- `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
5046
- "",
5047
- `- Previous: ${prev}`,
5048
- `- Next: ${next}`,
5049
- `- Files: ${group.length}`,
5050
- `- Content Fingerprint: ${contentFingerprint || "none"}`,
5051
- `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
5052
- `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
5053
- "",
5054
- UNTRUSTED_CONTENT_NOTICE,
5055
- "",
5056
- "## Files",
5057
- "",
5058
- `${listFence}text`,
5059
- ...pathLines,
5060
- listFence,
5061
- "",
5062
- "## Contents",
5063
- ""
5064
- ].join("\n");
5065
- const assembled = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
5066
- const estimatedTokens = estimateTokens(assembled);
5067
- const content = insertTokenLine(assembled, estimatedTokens);
5750
+ const { filename, content, estimatedTokens } = assembleGroup(
5751
+ group,
5752
+ index,
5753
+ total,
5754
+ contentFingerprint
5755
+ );
5068
5756
  const byteSize = Buffer.byteLength(content, "utf8");
5069
5757
  const sha256 = crypto2__namespace.createHash("sha256").update(content).digest("hex");
5070
- const includedFiles = group.map(({ file }) => ({
5758
+ const layersCovered = Array.from(new Set(group.sections.map((s) => s.file.layer))).sort();
5759
+ const includedFiles = group.sections.map(({ file }) => ({
5071
5760
  relativePath: file.relativePath,
5072
5761
  sourceBytes: file.sizeBytes,
5073
5762
  includedBytes: file.includedBytes,
@@ -5080,10 +5769,10 @@ function renderGroups(groups, budget, contentFingerprint) {
5080
5769
  filename,
5081
5770
  byteSize,
5082
5771
  estimatedTokens,
5083
- budgetBytes: budget,
5772
+ budgetBytes: group.budget,
5084
5773
  sha256,
5085
5774
  lines: countLines(content),
5086
- fileCount: group.length,
5775
+ fileCount: group.sections.length,
5087
5776
  layersCovered,
5088
5777
  includedFiles
5089
5778
  };
@@ -5091,7 +5780,7 @@ function renderGroups(groups, budget, contentFingerprint) {
5091
5780
  });
5092
5781
  }
5093
5782
  function emittedBody(content) {
5094
- const normalised = content.replace(/\r\n/g, "\n");
5783
+ const normalised = content.replace(/\r\n|\r/g, "\n");
5095
5784
  return normalised.endsWith("\n") ? normalised.slice(0, -1) : normalised;
5096
5785
  }
5097
5786
  function compactMetadata(file) {
@@ -5159,7 +5848,7 @@ function generateMonolithJson(manifest, filesWithContent) {
5159
5848
  }
5160
5849
  function generateMonolithMarkdown(manifest, filesWithContent) {
5161
5850
  const sections = filesWithContent.map((file) => {
5162
- const content = (file.content ?? "").replace(/\r\n/g, "\n");
5851
+ const content = (file.content ?? "").replace(/\r\n|\r/g, "\n");
5163
5852
  const fence = fenceForContent(content);
5164
5853
  return [
5165
5854
  `## File: ${escapeControls(file.relativePath)}`,
@@ -5324,322 +6013,79 @@ function generateReadme(manifest, artifacts) {
5324
6013
  `${readOrder.length + 1}. Inspect \`manifest.json\` for precise dependency and AST symbol maps.`
5325
6014
  );
5326
6015
  }
5327
- readOrder.push(
5328
- `${readOrder.length + 1}. Check \`manifest.validation.status\` before trusting the snapshot; \`snapshot validate\` re-checks it against disk.`
5329
- );
5330
- lines.push("", "## AI Agent Ingestion Guide", "", ...readOrder, "");
5331
- return lines.join("\n");
5332
- }
5333
-
5334
- // src/generator/tree.ts
5335
- var MAX_TREE_LINES = 2e4;
5336
- var MAX_TREE_DEPTH = 512;
5337
- function childKeys(node) {
5338
- return Array.from(node.children.keys()).sort((a, b) => {
5339
- const aIsDir = node.children.get(a).children.size > 0;
5340
- const bIsDir = node.children.get(b).children.size > 0;
5341
- if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
5342
- return a < b ? -1 : a > b ? 1 : 0;
5343
- });
5344
- }
5345
- function generateProjectTree(filePaths) {
5346
- const root = { children: /* @__PURE__ */ new Map() };
5347
- for (const filePath of filePaths) {
5348
- const parts = filePath.split("/").filter((part) => part.length > 0);
5349
- let current = root;
5350
- for (const part of parts) {
5351
- let child = current.children.get(part);
5352
- if (!child) {
5353
- child = { children: /* @__PURE__ */ new Map() };
5354
- current.children.set(part, child);
5355
- }
5356
- current = child;
5357
- }
5358
- }
5359
- const lines = ["."];
5360
- let truncatedAt = -1;
5361
- let depthTruncated = false;
5362
- const stack = [{ node: root, prefix: "", depth: 0, keys: childKeys(root), index: 0 }];
5363
- while (stack.length > 0) {
5364
- const frame = stack[stack.length - 1];
5365
- if (frame.index >= frame.keys.length) {
5366
- stack.pop();
5367
- continue;
5368
- }
5369
- const key = frame.keys[frame.index];
5370
- frame.index += 1;
5371
- const isLast = frame.index === frame.keys.length;
5372
- const child = frame.node.children.get(key);
5373
- const isDirectory = child.children.size > 0;
5374
- const label = escapeControls(key);
5375
- lines.push(`${frame.prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
5376
- if (lines.length >= MAX_TREE_LINES) {
5377
- truncatedAt = lines.length;
5378
- break;
5379
- }
5380
- if (!isDirectory) continue;
5381
- if (frame.depth + 1 > MAX_TREE_DEPTH) {
5382
- depthTruncated = true;
5383
- continue;
5384
- }
5385
- stack.push({
5386
- node: child,
5387
- prefix: frame.prefix + (isLast ? " " : "\u2502 "),
5388
- depth: frame.depth + 1,
5389
- keys: childKeys(child),
5390
- index: 0
5391
- });
5392
- }
5393
- if (truncatedAt !== -1) {
5394
- lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
5395
- } else if (depthTruncated) {
5396
- lines.push(`... [tree truncated at ${MAX_TREE_DEPTH} levels] ...`);
5397
- }
5398
- const fence = fenceForText(lines.join("\n"), 3);
5399
- return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
5400
- }
5401
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
5402
- ".png",
5403
- ".jpg",
5404
- ".jpeg",
5405
- ".gif",
5406
- ".webp",
5407
- ".avif",
5408
- ".ico",
5409
- ".bmp",
5410
- ".tiff",
5411
- ".pdf",
5412
- ".zip",
5413
- ".tar",
5414
- ".gz",
5415
- ".tgz",
5416
- ".br",
5417
- ".bz2",
5418
- ".xz",
5419
- ".zst",
5420
- ".lz4",
5421
- ".lzma",
5422
- ".7z",
5423
- ".rar",
5424
- ".mp3",
5425
- ".mp4",
5426
- ".wav",
5427
- ".ogg",
5428
- ".webm",
5429
- ".mov",
5430
- ".avi",
5431
- ".woff",
5432
- ".woff2",
5433
- ".ttf",
5434
- ".otf",
5435
- ".eot",
5436
- ".wasm",
5437
- ".node",
5438
- ".exe",
5439
- ".dll",
5440
- ".dylib",
5441
- ".so"
5442
- ]);
5443
- function isBinaryExtension(filePath) {
5444
- const ext = path16__namespace.extname(filePath).toLowerCase();
5445
- return BINARY_EXTENSIONS.has(ext);
5446
- }
5447
- function isBinaryBuffer(buffer) {
5448
- return buffer.includes(0);
5449
- }
5450
- function countControlBytes(buffer) {
5451
- let count = 0;
5452
- for (let index = 0; index < buffer.length; index++) {
5453
- const byte = buffer[index];
5454
- if (byte < 32) {
5455
- if (byte !== 9 && byte !== 10 && byte !== 13) count += 1;
5456
- continue;
5457
- }
5458
- if (byte === 127) {
5459
- count += 1;
5460
- continue;
5461
- }
5462
- if (byte === 194 && index + 1 < buffer.length) {
5463
- const next = buffer[index + 1];
5464
- if (next >= 128 && next <= 159) {
5465
- count += 1;
5466
- index += 1;
5467
- }
5468
- }
5469
- }
5470
- return count;
5471
- }
5472
- var CONTROL_BYTES_FLOOR = 4;
5473
- function countInvalidUtf8Bytes(buffer) {
5474
- let invalid = 0;
5475
- let index = 0;
5476
- const length = buffer.length;
5477
- while (index < length) {
5478
- const lead = buffer[index];
5479
- if (lead < 128) {
5480
- index += 1;
5481
- continue;
5482
- }
5483
- let continuation = 0;
5484
- let codePoint = 0;
5485
- let minimum = 0;
5486
- if ((lead & 224) === 192) {
5487
- continuation = 1;
5488
- codePoint = lead & 31;
5489
- minimum = 128;
5490
- } else if ((lead & 240) === 224) {
5491
- continuation = 2;
5492
- codePoint = lead & 15;
5493
- minimum = 2048;
5494
- } else if ((lead & 248) === 240) {
5495
- continuation = 3;
5496
- codePoint = lead & 7;
5497
- minimum = 65536;
5498
- } else {
5499
- invalid += 1;
5500
- index += 1;
5501
- continue;
5502
- }
5503
- if (index + continuation >= length) {
5504
- invalid += 1;
5505
- index += 1;
5506
- continue;
5507
- }
5508
- let wellFormed = true;
5509
- for (let offset = 1; offset <= continuation; offset++) {
5510
- const next = buffer[index + offset];
5511
- if ((next & 192) !== 128) {
5512
- wellFormed = false;
5513
- break;
5514
- }
5515
- codePoint = codePoint << 6 | next & 63;
5516
- }
5517
- const isSurrogate = codePoint >= 55296 && codePoint <= 57343;
5518
- if (!wellFormed || codePoint < minimum || codePoint > 1114111 || isSurrogate) {
5519
- invalid += 1;
5520
- index += 1;
5521
- continue;
5522
- }
5523
- index += continuation + 1;
5524
- }
5525
- return invalid;
5526
- }
5527
- function readBomEncoding(buffer) {
5528
- if (buffer.length >= 2) {
5529
- if (buffer[0] === 255 && buffer[1] === 254) return "utf-16le";
5530
- if (buffer[0] === 254 && buffer[1] === 255) return "utf-16be";
5531
- }
5532
- return null;
5533
- }
5534
- function looksLikeUtf16(buffer) {
5535
- if (buffer.length < 8 || buffer.length % 2 !== 0) return null;
5536
- const sample = Math.min(buffer.length, 4096);
5537
- let nulInLowByte = 0;
5538
- let nulInHighByte = 0;
5539
- const units = sample / 2;
5540
- for (let index = 0; index < sample; index += 2) {
5541
- if (buffer[index] === 0) nulInLowByte += 1;
5542
- if (buffer[index + 1] === 0) nulInHighByte += 1;
5543
- }
5544
- if (nulInHighByte > units * 0.3 && nulInLowByte < units * 0.05) return "utf-16le";
5545
- if (nulInLowByte > units * 0.3 && nulInHighByte < units * 0.05) return "utf-16be";
5546
- return null;
5547
- }
5548
- function decodeUtf16(buffer, encoding) {
5549
- const littleEndian = encoding === "utf-16le" ? buffer : Buffer.from(buffer.subarray(0, buffer.length - buffer.length % 2)).swap16();
5550
- return new TextDecoder("utf-16le").decode(littleEndian);
5551
- }
5552
- function decodeText(buffer) {
5553
- const bomEncoding = readBomEncoding(buffer);
5554
- if (bomEncoding) {
5555
- const text2 = decodeUtf16(buffer.subarray(2), bomEncoding);
5556
- return { text: text2, encoding: bomEncoding, invalidBytes: 0, binary: false };
5557
- }
5558
- if (buffer.includes(0)) {
5559
- const shape = looksLikeUtf16(buffer);
5560
- if (shape) {
5561
- return { text: decodeUtf16(buffer, shape), encoding: shape, invalidBytes: 0, binary: false };
5562
- }
5563
- const offset = buffer.indexOf(0);
5564
- return {
5565
- text: "",
5566
- encoding: "utf-8",
5567
- invalidBytes: 0,
5568
- binary: true,
5569
- binaryReason: `NUL byte at offset ${offset}`
5570
- };
5571
- }
5572
- const invalidBytes = countInvalidUtf8Bytes(buffer);
5573
- if (invalidBytes === 0) {
5574
- const controlBytes = countControlBytes(buffer);
5575
- if (controlBytes > CONTROL_BYTES_FLOOR && controlBytes * 100 > buffer.length) {
5576
- return {
5577
- text: "",
5578
- encoding: "utf-8",
5579
- invalidBytes: 0,
5580
- binary: true,
5581
- binaryReason: `${controlBytes} control byte(s) in ${buffer.length} B; valid UTF-8 but not text`
5582
- };
5583
- }
5584
- return {
5585
- text: new TextDecoder("utf-8").decode(buffer),
5586
- encoding: "utf-8",
5587
- invalidBytes: 0,
5588
- binary: false
5589
- };
5590
- }
5591
- if (invalidBytes >= 32 && invalidBytes * 3 >= buffer.length) {
5592
- return {
5593
- text: "",
5594
- encoding: "utf-8",
5595
- invalidBytes,
5596
- binary: true,
5597
- binaryReason: `${invalidBytes} of ${buffer.length} bytes are not UTF-8; treated as binary`
5598
- };
5599
- }
5600
- return {
5601
- text: buffer.toString("utf8"),
5602
- encoding: "utf-8-lossy",
5603
- invalidBytes,
5604
- binary: false
5605
- };
6016
+ readOrder.push(
6017
+ `${readOrder.length + 1}. Check \`manifest.validation.status\` before trusting the snapshot; \`snapshot validate\` re-checks it against disk.`
6018
+ );
6019
+ lines.push("", "## AI Agent Ingestion Guide", "", ...readOrder, "");
6020
+ return lines.join("\n");
5606
6021
  }
5607
- var BINARY_SCAN_BYTES = 8 * 1024 * 1024;
5608
- var CHUNK = 64 * 1024;
5609
- var SHAPE_SAMPLE_BYTES = 4096;
5610
- function isBinaryFile(absolutePath) {
5611
- if (isBinaryExtension(absolutePath)) return true;
5612
- let fd;
5613
- try {
5614
- fd = fs14__namespace.openSync(absolutePath, "r");
5615
- const buffer = Buffer.alloc(CHUNK);
5616
- let sample;
5617
- let position = 0;
5618
- while (position < BINARY_SCAN_BYTES) {
5619
- const wanted = Math.min(CHUNK, BINARY_SCAN_BYTES - position);
5620
- const bytesRead = fs14__namespace.readSync(fd, buffer, 0, wanted, position);
5621
- if (bytesRead === 0) return false;
5622
- const chunk = buffer.subarray(0, bytesRead);
5623
- if (sample === void 0) {
5624
- sample = Buffer.from(chunk.subarray(0, SHAPE_SAMPLE_BYTES));
5625
- }
5626
- if (chunk.includes(0)) {
5627
- const shape = readBomEncoding(sample) ?? looksLikeUtf16(sample);
5628
- return shape === null;
6022
+
6023
+ // src/generator/tree.ts
6024
+ var MAX_TREE_LINES = 2e4;
6025
+ var MAX_TREE_DEPTH = 512;
6026
+ function childKeys(node) {
6027
+ return Array.from(node.children.keys()).sort((a, b) => {
6028
+ const aIsDir = node.children.get(a).children.size > 0;
6029
+ const bIsDir = node.children.get(b).children.size > 0;
6030
+ if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
6031
+ return a < b ? -1 : a > b ? 1 : 0;
6032
+ });
6033
+ }
6034
+ function generateProjectTree(filePaths) {
6035
+ const root = { children: /* @__PURE__ */ new Map() };
6036
+ for (const filePath of filePaths) {
6037
+ const parts = filePath.split("/").filter((part) => part.length > 0);
6038
+ let current = root;
6039
+ for (const part of parts) {
6040
+ let child = current.children.get(part);
6041
+ if (!child) {
6042
+ child = { children: /* @__PURE__ */ new Map() };
6043
+ current.children.set(part, child);
5629
6044
  }
5630
- position += bytesRead;
6045
+ current = child;
5631
6046
  }
5632
- return false;
5633
- } catch {
5634
- return false;
5635
- } finally {
5636
- if (fd !== void 0) {
5637
- try {
5638
- fs14__namespace.closeSync(fd);
5639
- } catch {
5640
- }
6047
+ }
6048
+ const lines = ["."];
6049
+ let truncatedAt = -1;
6050
+ let depthTruncated = false;
6051
+ const stack = [{ node: root, prefix: "", depth: 0, keys: childKeys(root), index: 0 }];
6052
+ while (stack.length > 0) {
6053
+ const frame = stack[stack.length - 1];
6054
+ if (frame.index >= frame.keys.length) {
6055
+ stack.pop();
6056
+ continue;
6057
+ }
6058
+ const key = frame.keys[frame.index];
6059
+ frame.index += 1;
6060
+ const isLast = frame.index === frame.keys.length;
6061
+ const child = frame.node.children.get(key);
6062
+ const isDirectory = child.children.size > 0;
6063
+ const label = escapeControls(key);
6064
+ lines.push(`${frame.prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
6065
+ if (lines.length >= MAX_TREE_LINES) {
6066
+ truncatedAt = lines.length;
6067
+ break;
6068
+ }
6069
+ if (!isDirectory) continue;
6070
+ if (frame.depth + 1 > MAX_TREE_DEPTH) {
6071
+ depthTruncated = true;
6072
+ continue;
5641
6073
  }
6074
+ stack.push({
6075
+ node: child,
6076
+ prefix: frame.prefix + (isLast ? " " : "\u2502 "),
6077
+ depth: frame.depth + 1,
6078
+ keys: childKeys(child),
6079
+ index: 0
6080
+ });
6081
+ }
6082
+ if (truncatedAt !== -1) {
6083
+ lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
6084
+ } else if (depthTruncated) {
6085
+ lines.push(`... [tree truncated at ${MAX_TREE_DEPTH} levels] ...`);
5642
6086
  }
6087
+ const fence = fenceForText(lines.join("\n"), 3);
6088
+ return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
5643
6089
  }
5644
6090
  function gitPaths(cwd, args) {
5645
6091
  let output;
@@ -5726,6 +6172,10 @@ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5726
6172
  ".mjs",
5727
6173
  ".cjs"
5728
6174
  ]);
6175
+ var PYTHON_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".pyi"]);
6176
+ var RUBY_EXTENSIONS = /* @__PURE__ */ new Set([".rb", ".rake", ".gemspec"]);
6177
+ var C_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"]);
6178
+ var PYTHON_SUFFIXES = [".py", ".pyi", "/__init__.py", "/__init__.pyi"];
5729
6179
  function buildImportResolver(files) {
5730
6180
  const byPath = /* @__PURE__ */ new Map();
5731
6181
  const byPathFolded = /* @__PURE__ */ new Map();
@@ -5752,11 +6202,45 @@ function buildImportResolver(files) {
5752
6202
  return { byPath, byPathFolded, byStem, byStemFolded };
5753
6203
  }
5754
6204
  function resolveSpecifier(fromFile, specifier, resolver) {
6205
+ const extension = path16__namespace.posix.extname(fromFile).toLowerCase();
6206
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
6207
+ const firstOf = (candidates) => {
6208
+ for (const candidate of candidates) {
6209
+ const hit = exact(candidate);
6210
+ if (hit) return hit;
6211
+ }
6212
+ return null;
6213
+ };
6214
+ if (PYTHON_EXTENSIONS.has(extension) && specifier.startsWith(".")) {
6215
+ const dots = /^\.+/.exec(specifier)?.[0].length ?? 0;
6216
+ const rest = specifier.slice(dots).replace(/\./g, "/");
6217
+ let base = path16__namespace.posix.dirname(fromFile);
6218
+ for (let up = 1; up < dots; up += 1) base = path16__namespace.posix.dirname(base);
6219
+ const joined2 = rest ? path16__namespace.posix.normalize(path16__namespace.posix.join(base, rest)) : base;
6220
+ return firstOf([joined2, ...PYTHON_SUFFIXES.map((suffix) => `${joined2}${suffix}`)]);
6221
+ }
6222
+ if (RUBY_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
6223
+ const joined2 = path16__namespace.posix.normalize(
6224
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
6225
+ );
6226
+ return firstOf([`${joined2}.rb`, joined2]);
6227
+ }
6228
+ if (C_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
6229
+ const joined2 = path16__namespace.posix.normalize(
6230
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
6231
+ );
6232
+ return exact(joined2);
6233
+ }
6234
+ if (extension === ".dart" && !specifier.includes(":")) {
6235
+ const joined2 = path16__namespace.posix.normalize(
6236
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
6237
+ );
6238
+ return firstOf([joined2, `${joined2}.dart`]);
6239
+ }
5755
6240
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5756
6241
  const joined = path16__namespace.posix.normalize(
5757
6242
  path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
5758
6243
  );
5759
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5760
6244
  const direct = exact(joined);
5761
6245
  if (direct) return direct;
5762
6246
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
@@ -5837,28 +6321,6 @@ function detectLanguage(filePath) {
5837
6321
  const ext = path16__namespace.posix.extname(base).toLowerCase();
5838
6322
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
5839
6323
  }
5840
- var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5841
- var isTsSource = (relativePath) => {
5842
- const lower = relativePath.toLowerCase();
5843
- return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
5844
- };
5845
- function importsOfText(relativePath, text2) {
5846
- if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
5847
- return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
5848
- }
5849
- function importsOfFile(relativePath, absolutePath) {
5850
- let buffer;
5851
- try {
5852
- buffer = fs14__namespace.readFileSync(absolutePath);
5853
- } catch {
5854
- return [];
5855
- }
5856
- const decoded = decodeText(buffer);
5857
- if (decoded.binary) return [];
5858
- return importsOfText(relativePath, decoded.text);
5859
- }
5860
-
5861
- // src/validator/validate.ts
5862
6324
  function maskedPath(value, secretRules) {
5863
6325
  return findSecretInPath(value, secretRules)?.masked ?? value;
5864
6326
  }
@@ -5979,9 +6441,13 @@ function describeManifestShapeProblem(value) {
5979
6441
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5980
6442
  return 'a "parts" entry is not an object';
5981
6443
  }
5982
- if (typeof entry.filename !== "string") {
6444
+ const filename = entry.filename;
6445
+ if (typeof filename !== "string" || filename.trim() === "") {
5983
6446
  return 'a "parts" entry has no "filename"';
5984
6447
  }
6448
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("\0") || filename === "." || filename === ".." || path16__namespace.isAbsolute(filename)) {
6449
+ return 'a "parts" entry "filename" must be a bare filename inside parts/';
6450
+ }
5985
6451
  }
5986
6452
  if (!candidate.layers || typeof candidate.layers !== "object" || Array.isArray(candidate.layers)) {
5987
6453
  return 'missing "layers" object';
@@ -6092,7 +6558,7 @@ function validateSnapshot(configInput) {
6092
6558
  });
6093
6559
  for (const diskFile of collection.files) {
6094
6560
  const snapshotHash = manifestFileMap.get(diskFile);
6095
- if (!snapshotHash) {
6561
+ if (snapshotHash === void 0) {
6096
6562
  const skipReason = skippedReasonOnDisk(diskFile);
6097
6563
  if (skipReason === "unchanged") {
6098
6564
  if (unchangedScope !== null) {
@@ -6106,6 +6572,11 @@ function validateSnapshot(configInput) {
6106
6572
  drift.missingInSnapshot.push(diskFile);
6107
6573
  continue;
6108
6574
  }
6575
+ if (unchangedScope !== null && !unchangedScope.has(diskFile) && !becameDependent(diskFile)) {
6576
+ drift.configMismatches.push(
6577
+ `scope: ${maskedPath(diskFile, config.secretRules)} is in the snapshot but is no longer in the changed-files scope`
6578
+ );
6579
+ }
6109
6580
  let diskHash;
6110
6581
  try {
6111
6582
  diskHash = sha256Hex(fs14__namespace.readFileSync(path16__namespace.join(config.cwd, diskFile)));
@@ -6246,6 +6717,41 @@ function validateSnapshot(configInput) {
6246
6717
  `includeSensitiveFiles: manifest ${recorded.includeSensitiveFiles} vs current ${config.includeSensitiveFiles}`
6247
6718
  );
6248
6719
  }
6720
+ if ((recorded.maxPartTokens ?? void 0) !== (config.maxPartTokens ?? void 0)) {
6721
+ drift.configMismatches.push(
6722
+ `maxPartTokens: manifest ${String(recorded.maxPartTokens ?? "unset")} vs current ${String(config.maxPartTokens ?? "unset")}`
6723
+ );
6724
+ }
6725
+ if ((recorded.maxMonolithBytes ?? null) !== (config.maxMonolithBytes ?? null)) {
6726
+ drift.configMismatches.push(
6727
+ `maxMonolithBytes: manifest ${String(recorded.maxMonolithBytes ?? null)} vs current ${String(config.maxMonolithBytes ?? null)}`
6728
+ );
6729
+ }
6730
+ if (Boolean(recorded.skeleton) !== Boolean(config.skeleton)) {
6731
+ drift.configMismatches.push(
6732
+ `skeleton: manifest ${Boolean(recorded.skeleton)} vs current ${Boolean(config.skeleton)}`
6733
+ );
6734
+ }
6735
+ if (Boolean(recorded.sarif) !== Boolean(config.sarif)) {
6736
+ drift.configMismatches.push(
6737
+ `sarif: manifest ${Boolean(recorded.sarif)} vs current ${Boolean(config.sarif)}`
6738
+ );
6739
+ }
6740
+ if ((recorded.changedSince ?? void 0) !== (config.changedSince ?? void 0)) {
6741
+ drift.configMismatches.push(
6742
+ `changedSince: manifest ${String(recorded.changedSince ?? "unset")} vs current ${String(config.changedSince ?? "unset")}`
6743
+ );
6744
+ }
6745
+ if (Boolean(recorded.staged) !== Boolean(config.staged)) {
6746
+ drift.configMismatches.push(
6747
+ `staged: manifest ${Boolean(recorded.staged)} vs current ${Boolean(config.staged)}`
6748
+ );
6749
+ }
6750
+ if (Boolean(recorded.withDependents) !== Boolean(config.withDependents)) {
6751
+ drift.configMismatches.push(
6752
+ `withDependents: manifest ${Boolean(recorded.withDependents)} vs current ${Boolean(config.withDependents)}`
6753
+ );
6754
+ }
6249
6755
  const effectiveDiscovery = config.discovery ?? "auto";
6250
6756
  const recordedDiscovery = recorded.discovery;
6251
6757
  if (recordedDiscovery !== void 0 && recordedDiscovery !== effectiveDiscovery) {
@@ -6466,7 +6972,7 @@ function validateSnapshot(configInput) {
6466
6972
 
6467
6973
  // src/version.ts
6468
6974
  var GENERATOR_NAME = "codebase-snapshot";
6469
- var GENERATOR_VERSION = "0.9.12";
6975
+ var GENERATOR_VERSION = "0.9.14";
6470
6976
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
6471
6977
  function isOurGeneratorName(name) {
6472
6978
  if (typeof name !== "string") return false;
@@ -6493,22 +6999,28 @@ function updateCommand(kind) {
6493
6999
  if (kind === "local") return "npm install codebase-snapshot@latest";
6494
7000
  return "npm install -g codebase-snapshot";
6495
7001
  }
6496
- function versionParts(value) {
6497
- const core = value.trim().replace(/^v/i, "").split("-")[0];
6498
- return core.split(".").map((part) => {
7002
+ function parseSemver(value) {
7003
+ const cleaned = value.trim().replace(/^v/i, "").split("+")[0] ?? "";
7004
+ const dash = cleaned.indexOf("-");
7005
+ const corePart = dash === -1 ? cleaned : cleaned.slice(0, dash);
7006
+ const prerelease = dash === -1 ? null : cleaned.slice(dash + 1);
7007
+ const core = corePart.split(".").map((part) => {
6499
7008
  const parsed = Number(part);
6500
7009
  return Number.isFinite(parsed) ? parsed : 0;
6501
7010
  });
7011
+ return { core, prerelease };
6502
7012
  }
6503
7013
  function isNewer(candidate, current) {
6504
- const left = versionParts(candidate);
6505
- const right = versionParts(current);
6506
- const length = Math.max(left.length, right.length);
7014
+ const left = parseSemver(candidate);
7015
+ const right = parseSemver(current);
7016
+ if (left.prerelease !== null && right.prerelease === null) return false;
7017
+ const length = Math.max(left.core.length, right.core.length);
6507
7018
  for (let index = 0; index < length; index += 1) {
6508
- const a = left[index] ?? 0;
6509
- const b = right[index] ?? 0;
7019
+ const a = left.core[index] ?? 0;
7020
+ const b = right.core[index] ?? 0;
6510
7021
  if (a !== b) return a > b;
6511
7022
  }
7023
+ if (left.prerelease === null && right.prerelease !== null) return true;
6512
7024
  return false;
6513
7025
  }
6514
7026
  function cacheDirectory(env = process.env, platform = process.platform) {
@@ -6576,14 +7088,22 @@ function writeCache(file, entry) {
6576
7088
  }
6577
7089
  }
6578
7090
  function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOUT_MS) {
6579
- return new Promise((resolve5) => {
7091
+ return new Promise((resolve6) => {
6580
7092
  let settled = false;
7093
+ let request = null;
6581
7094
  const finish = (value) => {
6582
7095
  if (settled) return;
6583
7096
  settled = true;
6584
- resolve5(value);
7097
+ clearTimeout(deadline);
7098
+ resolve6(value);
6585
7099
  };
6586
- const request = https__namespace.get(
7100
+ const deadline = setTimeout(() => {
7101
+ request?.destroy();
7102
+ finish(null);
7103
+ }, timeoutMs);
7104
+ deadline.unref();
7105
+ const transport = url.startsWith("http:") ? http__namespace : https__namespace;
7106
+ request = transport.get(
6587
7107
  url,
6588
7108
  {
6589
7109
  headers: {
@@ -6602,7 +7122,7 @@ function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOU
6602
7122
  response.on("data", (chunk) => {
6603
7123
  body += chunk;
6604
7124
  if (body.length > 64 * 1024) {
6605
- request.destroy();
7125
+ request?.destroy();
6606
7126
  finish(null);
6607
7127
  }
6608
7128
  });
@@ -6617,10 +7137,6 @@ function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOU
6617
7137
  }
6618
7138
  );
6619
7139
  request.on("error", () => finish(null));
6620
- request.setTimeout(timeoutMs, () => {
6621
- request.destroy();
6622
- finish(null);
6623
- });
6624
7140
  });
6625
7141
  }
6626
7142
  async function checkForUpdate(options) {
@@ -6664,10 +7180,13 @@ function describeMatcher(matcher) {
6664
7180
  return String(matcher);
6665
7181
  }
6666
7182
  function normalizeTarget(cwd, target) {
6667
- const absolute = path16__namespace.resolve(cwd, target);
6668
- const relative8 = path16__namespace.relative(cwd, absolute);
6669
- if (relative8.startsWith("..") || path16__namespace.isAbsolute(relative8)) return target;
6670
- return relative8.split(path16__namespace.sep).join("/");
7183
+ const resolvedCwd = path16__namespace.resolve(cwd);
7184
+ const absolute = path16__namespace.resolve(resolvedCwd, target);
7185
+ const relative8 = path16__namespace.relative(resolvedCwd, absolute);
7186
+ if (relative8 === "" || relative8 === ".." || relative8.startsWith(`..${path16__namespace.sep}`) || path16__namespace.isAbsolute(relative8)) {
7187
+ return { relative: target.replace(/\\/g, "/"), outsideRoot: true };
7188
+ }
7189
+ return { relative: relative8.split(path16__namespace.sep).join("/"), outsideRoot: false };
6671
7190
  }
6672
7191
  function findRuleMatch(rules, relativePath) {
6673
7192
  return rules.find(
@@ -6694,9 +7213,19 @@ function readManifest2(config) {
6694
7213
  }
6695
7214
  }
6696
7215
  function explainPath(config, target) {
6697
- const relative8 = normalizeTarget(config.cwd, target);
6698
- const absolute = path16__namespace.join(config.cwd, relative8);
7216
+ const normalized = normalizeTarget(config.cwd, target);
7217
+ const relative8 = normalized.relative;
6699
7218
  const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
7219
+ if (normalized.outsideRoot) {
7220
+ return {
7221
+ path: maskedPath2,
7222
+ exists: false,
7223
+ included: false,
7224
+ reason: "outside-root",
7225
+ detail: "path resolves outside the configured repository root and was not inspected"
7226
+ };
7227
+ }
7228
+ const absolute = path16__namespace.join(config.cwd, relative8);
6700
7229
  const result = {
6701
7230
  path: maskedPath2,
6702
7231
  exists: fs14__namespace.existsSync(absolute),
@@ -7331,6 +7860,24 @@ function displayWidth(line) {
7331
7860
  return width;
7332
7861
  }
7333
7862
  var GUTTER_PATTERN = /^[ \t]*(?:[❯◆◻◼▏✖][ \t]+)*/;
7863
+ function sliceByDisplayWidth(text2, maxCells) {
7864
+ let cells = 0;
7865
+ let lastSpace = -1;
7866
+ let index = 0;
7867
+ for (const character of text2) {
7868
+ const width = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;
7869
+ if (cells + width > maxCells) break;
7870
+ if (character === " ") lastSpace = index;
7871
+ cells += width;
7872
+ index += character.length;
7873
+ }
7874
+ if (index >= text2.length) return { head: text2, tail: "" };
7875
+ if (lastSpace > 0) {
7876
+ return { head: text2.slice(0, lastSpace), tail: text2.slice(lastSpace + 1).trimStart() };
7877
+ }
7878
+ const safeIndex = index > 0 ? index : Array.from(text2)[0]?.length ?? 1;
7879
+ return { head: text2.slice(0, safeIndex), tail: text2.slice(safeIndex).trimStart() };
7880
+ }
7334
7881
  function wrapOneLine(line, limit) {
7335
7882
  if (displayWidth(line) <= limit) return [line];
7336
7883
  const gutter = GUTTER_PATTERN.exec(line)?.[0] ?? "";
@@ -7345,12 +7892,11 @@ function wrapOneLine(line, limit) {
7345
7892
  rows.push(prefix + rest);
7346
7893
  break;
7347
7894
  }
7348
- let cut = room;
7349
- const space = rest.slice(0, room).lastIndexOf(" ");
7350
- if (space > 0) cut = space;
7351
- const taken = rest.slice(0, cut).trimEnd();
7352
- rows.push(`${prefix}${taken.length > 0 ? taken : rest.slice(0, cut)}`);
7353
- rest = rest.slice(cut).trimStart();
7895
+ const { head, tail } = sliceByDisplayWidth(rest, room);
7896
+ rows.push(
7897
+ `${prefix}${head.length > 0 ? head : rest.slice(0, 1)}`
7898
+ );
7899
+ rest = tail;
7354
7900
  prefix = continuation;
7355
7901
  }
7356
7902
  return rows;
@@ -7421,7 +7967,7 @@ function keyReader(input) {
7421
7967
  next() {
7422
7968
  const queued = queue.shift();
7423
7969
  if (queued) return Promise.resolve(queued);
7424
- return new Promise((resolve5) => waiters.push(resolve5));
7970
+ return new Promise((resolve6) => waiters.push(resolve6));
7425
7971
  },
7426
7972
  close() {
7427
7973
  input.off("keypress", onKey);
@@ -7638,17 +8184,25 @@ var CONFIG_CANDIDATES = [
7638
8184
  ".snapshotrc.json",
7639
8185
  ".snapshotrc"
7640
8186
  ];
7641
- function chooseConfigFileName(cwd) {
7642
- if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
7643
- const pkgPath = path16__namespace.join(cwd, "package.json");
7644
- if (!fs14__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
8187
+ function parsePackageJson(raw) {
8188
+ const parsed = JSON.parse(stripJsonComments(raw));
8189
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
8190
+ throw new Error("package.json is not a JSON object");
8191
+ }
8192
+ return parsed;
8193
+ }
8194
+ function readPackageJson(cwd) {
7645
8195
  try {
7646
- const parsed = JSON.parse(fs14__namespace.readFileSync(pkgPath, "utf8"));
7647
- return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
8196
+ return parsePackageJson(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
7648
8197
  } catch {
7649
- return "snapshot.config.mjs";
8198
+ return null;
7650
8199
  }
7651
8200
  }
8201
+ function chooseConfigFileName(cwd) {
8202
+ if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
8203
+ const parsed = readPackageJson(cwd);
8204
+ return parsed?.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
8205
+ }
7652
8206
  var SIZE_CHOICES = [
7653
8207
  { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
7654
8208
  { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
@@ -7656,12 +8210,9 @@ var SIZE_CHOICES = [
7656
8210
  { value: "custom", label: "Custom\u2026", hint: "type a size like 150k or 1.5mb" }
7657
8211
  ];
7658
8212
  function packageJsonHasConfig(cwd) {
7659
- try {
7660
- const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
7661
- return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
7662
- } catch {
7663
- return false;
7664
- }
8213
+ const parsed = readPackageJson(cwd);
8214
+ if (parsed === null) return false;
8215
+ return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
7665
8216
  }
7666
8217
  function artifactAnswers(options) {
7667
8218
  const artifacts = { ...DEFAULT_ANSWERS.artifacts };
@@ -7702,12 +8253,7 @@ function writeFileOrExplain(target, content) {
7702
8253
  function canWriteModuleConfig(name, cwd) {
7703
8254
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
7704
8255
  if (!name.endsWith(".js")) return false;
7705
- try {
7706
- const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
7707
- return parsed.type === "module";
7708
- } catch {
7709
- return false;
7710
- }
8256
+ return readPackageJson(cwd)?.type === "module";
7711
8257
  }
7712
8258
  function configFormatLabel(name) {
7713
8259
  if (name.endsWith(".js")) return 'CommonJS `.js` (package.json has no "type": "module")';
@@ -7735,7 +8281,8 @@ function serializePackageJson(raw, value) {
7735
8281
  const crlf = raw.includes("\r\n");
7736
8282
  const body = JSON.stringify(value, null, spacing);
7737
8283
  const text2 = crlf ? body.replace(/\n/g, "\r\n") : body;
7738
- return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
8284
+ const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
8285
+ return `${bom}${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
7739
8286
  }
7740
8287
  function updatePackageJson(cwd, notes) {
7741
8288
  const packagePath = path16__namespace.join(cwd, "package.json");
@@ -7751,7 +8298,7 @@ function updatePackageJson(cwd, notes) {
7751
8298
  let parsed;
7752
8299
  try {
7753
8300
  raw = fs14__namespace.readFileSync(packagePath, "utf8");
7754
- parsed = JSON.parse(raw);
8301
+ parsed = parsePackageJson(raw);
7755
8302
  } catch {
7756
8303
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
7757
8304
  return unchanged;
@@ -8282,7 +8829,100 @@ function workerEntry(directory, isCjs) {
8282
8829
 
8283
8830
  // src/index.ts
8284
8831
  var SCHEMA_VERSION = "3.0.0";
8285
- var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
8832
+ var SUMMARY_MAX_SOURCE_BYTES2 = SUMMARY_MAX_SOURCE_BYTES;
8833
+ var FILE_DERIVATIONS = /* @__PURE__ */ new Map();
8834
+ var FILE_DERIVATION_LIMIT_BYTES = 64 * 1024 * 1024;
8835
+ var fileDerivationBytes = 0;
8836
+ function takeDerivation(path17, hash, settings) {
8837
+ const cached = FILE_DERIVATIONS.get(path17);
8838
+ if (cached === void 0 || cached.hash !== hash || cached.settings !== settings) return void 0;
8839
+ return cached;
8840
+ }
8841
+ function rememberDerivation(path17, hash, settings, derivation) {
8842
+ const bodyBytes = derivation.finalContent === derivation.redacted ? derivation.redacted.length : derivation.redacted.length + derivation.finalContent.length;
8843
+ const bytes = bodyBytes * 2 + 512;
8844
+ if (bytes > FILE_DERIVATION_LIMIT_BYTES / 4) return;
8845
+ while (fileDerivationBytes + bytes > FILE_DERIVATION_LIMIT_BYTES) {
8846
+ const oldest = FILE_DERIVATIONS.keys().next();
8847
+ if (oldest.done === true) break;
8848
+ const evicted = FILE_DERIVATIONS.get(oldest.value);
8849
+ FILE_DERIVATIONS.delete(oldest.value);
8850
+ if (evicted !== void 0) fileDerivationBytes -= evicted.bytes;
8851
+ }
8852
+ FILE_DERIVATIONS.set(path17, { ...derivation, hash, settings, bytes });
8853
+ fileDerivationBytes += bytes;
8854
+ }
8855
+ function derivationSettings(config) {
8856
+ const rules = config.secretRules.map(
8857
+ (rule) => `${rule.rule}\0${rule.pattern.source}\0${rule.pattern.flags}\0${rule.valueGroup ?? ""}`
8858
+ ).join("");
8859
+ return `${GENERATOR_VERSION}${configDigest(config)}${sha256Hex2(rules)}`;
8860
+ }
8861
+ function deriveFile(relativePath, rawContent, sizeBytes, config) {
8862
+ const lineCount = countLines(rawContent);
8863
+ const { redacted, findings } = scanAndRedactSecrets(rawContent, config.secretRules, relativePath);
8864
+ const isLarge = sizeBytes > config.largeFileBytes;
8865
+ let finalContent = redacted;
8866
+ let isPreviewTruncated = false;
8867
+ let includedLines = lineCount;
8868
+ let inclusionMode = "full";
8869
+ if (config.skeleton === true && isSkeletonizable(relativePath)) {
8870
+ const skeleton = skeletonizeTypeScript(redacted, relativePath);
8871
+ if (skeleton.collapsed > 0) {
8872
+ finalContent = skeleton.text;
8873
+ inclusionMode = "signatures";
8874
+ includedLines = countLines(skeleton.text);
8875
+ }
8876
+ }
8877
+ if (isLarge && inclusionMode !== "signatures") {
8878
+ const preview = truncatePreview(redacted, config.previewHeadLines, config.previewTailLines);
8879
+ finalContent = preview.content;
8880
+ isPreviewTruncated = preview.isTruncated;
8881
+ includedLines = preview.includedLines;
8882
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
8883
+ }
8884
+ let summary;
8885
+ let summaryQuality;
8886
+ let summarySkip = null;
8887
+ let summaryWarning = null;
8888
+ let dependentImports = null;
8889
+ if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES2) {
8890
+ summarySkip = "oversize";
8891
+ dependentImports = [];
8892
+ } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
8893
+ summarySkip = "preview";
8894
+ dependentImports = importsOfText(relativePath, rawContent);
8895
+ } else {
8896
+ try {
8897
+ const outcome = summarizeFile(relativePath, rawContent);
8898
+ summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
8899
+ summaryQuality = outcome?.quality;
8900
+ if (isSourceSummary(summary) && summary.parseErrors) {
8901
+ summaryWarning = `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`;
8902
+ }
8903
+ } catch (err) {
8904
+ summaryWarning = `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
8905
+ /\s+/g,
8906
+ " "
8907
+ );
8908
+ }
8909
+ }
8910
+ return {
8911
+ redacted,
8912
+ findings,
8913
+ lineCount,
8914
+ finalContent,
8915
+ inclusionMode,
8916
+ isPreviewTruncated,
8917
+ includedLines,
8918
+ isLarge,
8919
+ summary,
8920
+ summaryQuality,
8921
+ summarySkip,
8922
+ summaryWarning,
8923
+ dependentImports
8924
+ };
8925
+ }
8286
8926
  function sha256Hex2(input) {
8287
8927
  return crypto2__namespace.createHash("sha256").update(input).digest("hex");
8288
8928
  }
@@ -8826,10 +9466,7 @@ function acquireRunLock(stagingRoot) {
8826
9466
  };
8827
9467
  } catch (err) {
8828
9468
  if (err.code !== "EEXIST") throw err;
8829
- if (attempt === 0 && lockOwnerIsGone(lockPath)) {
8830
- fs14__namespace.rmSync(lockPath, { force: true });
8831
- continue;
8832
- }
9469
+ if (attempt === 0 && reclaimStaleLock(lockPath)) continue;
8833
9470
  throw new Error(
8834
9471
  `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
8835
9472
  );
@@ -8838,14 +9475,25 @@ function acquireRunLock(stagingRoot) {
8838
9475
  throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
8839
9476
  }
8840
9477
  function lockKeyFor(stagingRoot) {
8841
- let key = stagingRoot;
8842
- try {
8843
- key = fs14__namespace.realpathSync(stagingRoot);
8844
- } catch {
8845
- }
9478
+ let key = canonicalLockTarget(stagingRoot);
8846
9479
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
8847
9480
  return sha256Hex2(key).slice(0, 16);
8848
9481
  }
9482
+ function canonicalLockTarget(targetPath) {
9483
+ const resolved = path16__namespace.resolve(targetPath);
9484
+ let existing = resolved;
9485
+ const tail = [];
9486
+ for (; ; ) {
9487
+ try {
9488
+ return path16__namespace.join(fs14__namespace.realpathSync(existing), ...tail);
9489
+ } catch {
9490
+ const parent = path16__namespace.dirname(existing);
9491
+ if (parent === existing) return resolved;
9492
+ tail.unshift(path16__namespace.basename(existing));
9493
+ existing = parent;
9494
+ }
9495
+ }
9496
+ }
8849
9497
  function lockPathFor(stagingRoot) {
8850
9498
  return path16__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${lockKeyFor(stagingRoot)}.lock`);
8851
9499
  }
@@ -8870,6 +9518,24 @@ function lockOwnerIsGone(lockPath) {
8870
9518
  return err.code === "ESRCH";
8871
9519
  }
8872
9520
  }
9521
+ function reclaimStaleLock(lockPath) {
9522
+ let before;
9523
+ try {
9524
+ before = fs14__namespace.statSync(lockPath);
9525
+ } catch {
9526
+ return true;
9527
+ }
9528
+ if (!lockOwnerIsGone(lockPath)) return false;
9529
+ try {
9530
+ const current = fs14__namespace.statSync(lockPath);
9531
+ const sameFile = current.ino === before.ino && current.birthtimeMs === before.birthtimeMs && current.mtimeMs === before.mtimeMs;
9532
+ if (!sameFile) return false;
9533
+ fs14__namespace.rmSync(lockPath, { force: true });
9534
+ return true;
9535
+ } catch {
9536
+ return false;
9537
+ }
9538
+ }
8873
9539
  function lockFileIsAncient(lockPath) {
8874
9540
  try {
8875
9541
  return Date.now() - fs14__namespace.statSync(lockPath).mtimeMs > STALE_RUN_MS;
@@ -8927,7 +9593,6 @@ function swapOnce(stagingDir, outDir) {
8927
9593
  throw copyErr;
8928
9594
  }
8929
9595
  }
8930
- if (hadPrevious) fs14__namespace.rmSync(backupDir, { recursive: true, force: true });
8931
9596
  } catch (err) {
8932
9597
  if (hadPrevious && fs14__namespace.existsSync(backupDir)) {
8933
9598
  if (fs14__namespace.existsSync(outDir)) fs14__namespace.rmSync(outDir, { recursive: true, force: true });
@@ -8935,6 +9600,16 @@ function swapOnce(stagingDir, outDir) {
8935
9600
  }
8936
9601
  throw err;
8937
9602
  }
9603
+ if (hadPrevious) {
9604
+ try {
9605
+ fs14__namespace.rmSync(backupDir, { recursive: true, force: true });
9606
+ } catch (err) {
9607
+ process.stderr.write(
9608
+ `\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.
9609
+ `
9610
+ );
9611
+ }
9612
+ }
8938
9613
  }
8939
9614
  function sleepSync(milliseconds) {
8940
9615
  const buffer = new Int32Array(new SharedArrayBuffer(4));
@@ -8957,22 +9632,18 @@ function artifactWarnings(artifacts, sarif) {
8957
9632
  }
8958
9633
  return messages;
8959
9634
  }
9635
+ function installHookStdoutRedirect() {
9636
+ return installStdoutRouter();
9637
+ }
8960
9638
  function runAfterGenerate(config, manifest) {
8961
9639
  const hook = config.hooks.afterGenerate;
8962
9640
  if (!hook) return null;
8963
- const stdoutWrite = process.stdout.write.bind(process.stdout);
8964
- const stderrWrite = process.stderr.write.bind(process.stderr);
8965
- const hookScope = new async_hooks.AsyncLocalStorage();
8966
- const patchedWrite = ((...args) => hookScope.getStore() === true ? stderrWrite(...args) : stdoutWrite(...args));
8967
- const restoreStdout = () => {
8968
- if (process.stdout.write === patchedWrite) process.stdout.write = stdoutWrite;
8969
- };
9641
+ const releaseRedirect = installHookStdoutRedirect();
8970
9642
  let returned;
8971
9643
  try {
8972
- process.stdout.write = patchedWrite;
8973
- returned = hookScope.run(true, () => hook(manifest));
9644
+ returned = hookStdoutScope.run(true, () => hook(manifest));
8974
9645
  } catch (err) {
8975
- restoreStdout();
9646
+ releaseRedirect();
8976
9647
  const message = maskPathText(err.message, config.secretRules);
8977
9648
  manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
8978
9649
  manifest.totals.warningCount = manifest.warnings.length;
@@ -8980,10 +9651,10 @@ function runAfterGenerate(config, manifest) {
8980
9651
  }
8981
9652
  const thenable = returned;
8982
9653
  if (!thenable || typeof thenable.then !== "function") {
8983
- restoreStdout();
9654
+ releaseRedirect();
8984
9655
  return null;
8985
9656
  }
8986
- return Promise.resolve(returned).finally(restoreStdout);
9657
+ return Promise.resolve(returned).finally(releaseRedirect);
8987
9658
  }
8988
9659
  function partitionByBudget(files, config, fingerprint) {
8989
9660
  const budgetFor = (file) => config.budgets.byLayer[file.layer] ?? config.budgets.default;
@@ -8994,23 +9665,14 @@ function partitionByBudget(files, config, fingerprint) {
8994
9665
  if (bucket) bucket.push(file);
8995
9666
  else buckets.set(budget, [file]);
8996
9667
  }
8997
- if (buckets.size <= 1) {
8998
- const only = buckets.size === 1 ? Array.from(buckets.keys())[0] : config.maxPartBytes;
8999
- return partitionFiles(files, only, fingerprint);
9668
+ if (buckets.size === 0) return [];
9669
+ if (buckets.size === 1) {
9670
+ return partitionFiles(files, Array.from(buckets.keys())[0], fingerprint);
9000
9671
  }
9001
- const parts = [];
9002
- for (const budget of Array.from(buckets.keys()).sort((a, b) => a - b)) {
9003
- parts.push(...partitionFiles(buckets.get(budget) ?? [], budget, fingerprint));
9004
- }
9005
- const merged = renumberParts(parts, fingerprint);
9006
- const byPath = new Map(files.map((file) => [file.relativePath, file]));
9007
- for (const part of merged) {
9008
- for (const included of part.metadata.includedFiles) {
9009
- const file = byPath.get(included.relativePath);
9010
- if (file) file.part = part.filename;
9011
- }
9012
- }
9013
- return merged;
9672
+ return partitionFilesForBuckets(
9673
+ Array.from(buckets.entries()).sort(([left], [right]) => left - right).map(([budgetBytes, bucketFiles]) => ({ files: bucketFiles, budgetBytes })),
9674
+ fingerprint
9675
+ );
9014
9676
  }
9015
9677
  function generateSnapshot(configInput, options) {
9016
9678
  assertUsableConfigInput(configInput);
@@ -9042,6 +9704,7 @@ function generateSnapshot(configInput, options) {
9042
9704
  return false;
9043
9705
  }) : collection.files;
9044
9706
  const files = [];
9707
+ const derivationKey = derivationSettings(config);
9045
9708
  const monolithBodies = /* @__PURE__ */ new Map();
9046
9709
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
9047
9710
  const secretFindings = [];
@@ -9054,6 +9717,7 @@ function generateSnapshot(configInput, options) {
9054
9717
  const fileWarnings = [];
9055
9718
  const oversizeSummaryPaths = [];
9056
9719
  const previewSummaryPaths = [];
9720
+ const skippedSummaryImports = /* @__PURE__ */ new Map();
9057
9721
  const layerCounts = {};
9058
9722
  const kindCounts = {};
9059
9723
  let totalLines = 0;
@@ -9118,8 +9782,6 @@ function generateSnapshot(configInput, options) {
9118
9782
  }
9119
9783
  const hashSha256 = sha256Hex2(buffer);
9120
9784
  const rawContent = decoded.text;
9121
- const lineCount = countLines(rawContent);
9122
- totalLines += lineCount;
9123
9785
  totalSizeBytes += stats.size;
9124
9786
  if (decoded.encoding !== "utf-8") {
9125
9787
  encodingNotes.push({
@@ -9133,66 +9795,25 @@ function generateSnapshot(configInput, options) {
9133
9795
  const language = detectLanguage(relativePath);
9134
9796
  bump(layerCounts, layer2);
9135
9797
  bump(kindCounts, kind);
9136
- const { redacted, findings } = scanAndRedactSecrets(
9137
- rawContent,
9138
- config.secretRules,
9139
- relativePath
9140
- );
9141
- secretFindings.push(...findings);
9142
- const isLarge = stats.size > config.largeFileBytes;
9143
- if (isLarge) largeFileCount += 1;
9144
- let finalContent = redacted;
9145
- let isPreviewTruncated = false;
9146
- let includedLines = lineCount;
9147
- let inclusionMode = "full";
9148
- if (config.skeleton === true && isSkeletonizable(relativePath)) {
9149
- const skeleton = skeletonizeTypeScript(redacted, relativePath);
9150
- if (skeleton.collapsed > 0) {
9151
- finalContent = skeleton.text;
9152
- inclusionMode = "signatures";
9153
- includedLines = countLines(skeleton.text);
9154
- }
9155
- }
9156
- if (isLarge && inclusionMode !== "signatures") {
9157
- const preview = truncatePreview(
9158
- redacted,
9159
- config.previewHeadLines,
9160
- config.previewTailLines
9161
- );
9162
- finalContent = preview.content;
9163
- isPreviewTruncated = preview.isTruncated;
9164
- includedLines = preview.includedLines;
9165
- inclusionMode = isPreviewTruncated ? "preview" : "full";
9166
- }
9167
- if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
9168
- monolithBodies.set(relativePath, redacted);
9169
- }
9170
- let summary;
9171
- let summaryQuality;
9172
- if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES) {
9173
- oversizeSummaryPaths.push(relativePath);
9174
- } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
9175
- previewSummaryPaths.push(relativePath);
9176
- } else {
9177
- try {
9178
- const outcome = summarizeFile(relativePath, rawContent);
9179
- summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
9180
- summaryQuality = outcome?.quality;
9181
- if (isSourceSummary(summary) && summary.parseErrors) {
9182
- fileWarnings.push({
9183
- path: relativePath,
9184
- message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
9185
- });
9186
- }
9187
- } catch (err) {
9188
- fileWarnings.push({
9189
- path: relativePath,
9190
- message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
9191
- /\s+/g,
9192
- " "
9193
- )
9194
- });
9195
- }
9798
+ const derivation = takeDerivation(absolutePath, hashSha256, derivationKey) ?? (() => {
9799
+ const fresh = deriveFile(relativePath, rawContent, stats.size, config);
9800
+ rememberDerivation(absolutePath, hashSha256, derivationKey, fresh);
9801
+ return fresh;
9802
+ })();
9803
+ const { lineCount } = derivation;
9804
+ totalLines += lineCount;
9805
+ secretFindings.push(...derivation.findings);
9806
+ if (derivation.isLarge) largeFileCount += 1;
9807
+ if (derivation.summarySkip === "oversize") oversizeSummaryPaths.push(relativePath);
9808
+ else if (derivation.summarySkip === "preview") previewSummaryPaths.push(relativePath);
9809
+ if (derivation.dependentImports !== null) {
9810
+ skippedSummaryImports.set(relativePath, derivation.dependentImports);
9811
+ }
9812
+ if (derivation.summaryWarning !== null) {
9813
+ fileWarnings.push({ path: relativePath, message: derivation.summaryWarning });
9814
+ }
9815
+ if (config.includeMonolith && (derivation.isPreviewTruncated || derivation.inclusionMode === "signatures")) {
9816
+ monolithBodies.set(relativePath, derivation.redacted);
9196
9817
  }
9197
9818
  files.push({
9198
9819
  relativePath,
@@ -9203,24 +9824,25 @@ function generateSnapshot(configInput, options) {
9203
9824
  layer: layer2,
9204
9825
  kind,
9205
9826
  language,
9206
- inclusionMode,
9207
- isLarge,
9208
- isPreviewTruncated,
9827
+ inclusionMode: derivation.inclusionMode,
9828
+ isLarge: derivation.isLarge,
9829
+ isPreviewTruncated: derivation.isPreviewTruncated,
9209
9830
  originalLines: lineCount,
9210
- includedLines,
9211
- includedBytes: Buffer.byteLength(finalContent, "utf8"),
9212
- summary,
9213
- summaryQuality,
9831
+ includedLines: derivation.includedLines,
9832
+ includedBytes: Buffer.byteLength(derivation.finalContent, "utf8"),
9833
+ summary: derivation.summary,
9834
+ summaryQuality: derivation.summaryQuality,
9214
9835
  ...decoded.encoding === "utf-8" ? {} : { encoding: decoded.encoding },
9215
- content: finalContent
9836
+ content: derivation.finalContent
9216
9837
  });
9217
9838
  }
9218
9839
  if (changedScope !== null && changedPaths !== null && config.withDependents === true) {
9219
9840
  const keep = new Set(changedPaths);
9220
9841
  const resolver = buildImportResolver(files);
9221
9842
  for (const file of files) {
9222
- if (!isSourceSummary(file.summary)) continue;
9223
- if (importsChangedFile(file.relativePath, file.summary.imports, changedPaths, resolver)) {
9843
+ const fileImports = isSourceSummary(file.summary) ? file.summary.imports : skippedSummaryImports.get(file.relativePath) ?? [];
9844
+ if (fileImports.length === 0) continue;
9845
+ if (importsChangedFile(file.relativePath, fileImports, changedPaths, resolver)) {
9224
9846
  keep.add(file.relativePath);
9225
9847
  }
9226
9848
  }
@@ -9251,6 +9873,9 @@ function generateSnapshot(configInput, options) {
9251
9873
  for (const path17 of [...monolithBodies.keys()]) {
9252
9874
  if (!keep.has(path17)) monolithBodies.delete(path17);
9253
9875
  }
9876
+ for (const path17 of [...skippedSummaryImports.keys()]) {
9877
+ if (!keep.has(path17)) skippedSummaryImports.delete(path17);
9878
+ }
9254
9879
  }
9255
9880
  if (changedScope !== null) {
9256
9881
  for (const relativePath of unchangedPaths) {
@@ -9286,7 +9911,7 @@ function generateSnapshot(configInput, options) {
9286
9911
  const shown = oversizeSummaryPaths.slice(0, ENCODING_PATHS_SHOWN).map((entry) => maskPathText(entry, config.secretRules));
9287
9912
  const hidden = oversizeSummaryPaths.length - ENCODING_PATHS_SHOWN;
9288
9913
  warnings.push(
9289
- `${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.`
9914
+ `${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.`
9290
9915
  );
9291
9916
  }
9292
9917
  for (const [encoding, bucket] of encodingBuckets) {
@@ -9310,11 +9935,13 @@ function generateSnapshot(configInput, options) {
9310
9935
  ).join("\n")
9311
9936
  );
9312
9937
  const fullContents = /* @__PURE__ */ new Map();
9313
- for (const file of files) {
9314
- fullContents.set(
9315
- file.relativePath,
9316
- monolithBodies.get(file.relativePath) ?? file.content ?? ""
9317
- );
9938
+ if (config.includeMonolith) {
9939
+ for (const file of files) {
9940
+ fullContents.set(
9941
+ file.relativePath,
9942
+ monolithBodies.get(file.relativePath) ?? file.content ?? ""
9943
+ );
9944
+ }
9318
9945
  }
9319
9946
  const parts = partitionByBudget(files, config, fingerprint);
9320
9947
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
@@ -9575,7 +10202,7 @@ function generateSnapshot(configInput, options) {
9575
10202
  };
9576
10203
  shipHookState();
9577
10204
  if (pendingHook) {
9578
- pendingHook.then(
10205
+ const tracked = pendingHook.then(
9579
10206
  () => {
9580
10207
  try {
9581
10208
  shipHookState();
@@ -9594,8 +10221,10 @@ function generateSnapshot(configInput, options) {
9594
10221
  } catch {
9595
10222
  }
9596
10223
  }
9597
- );
9598
- pendingHooks.set(outDir, pendingHook);
10224
+ ).finally(() => {
10225
+ if (pendingHooks.get(outDir) === tracked) pendingHooks.delete(outDir);
10226
+ });
10227
+ pendingHooks.set(outDir, tracked);
9599
10228
  }
9600
10229
  return manifest;
9601
10230
  } catch (err) {
@@ -9613,12 +10242,12 @@ function generateSnapshot(configInput, options) {
9613
10242
  }
9614
10243
  var pendingHooks = /* @__PURE__ */ new Map();
9615
10244
  async function generateSnapshotAsync(configInput, options) {
9616
- const manifest = generateSnapshot(configInput, options);
10245
+ assertUsableConfigInput(configInput);
9617
10246
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput ?? null);
10247
+ const manifest = generateSnapshot(config, options);
9618
10248
  const { outDir } = resolveSafeOutputDirs(config);
9619
10249
  const pending = pendingHooks.get(outDir);
9620
10250
  if (pending !== void 0) {
9621
- pendingHooks.delete(outDir);
9622
10251
  await pending.catch(() => void 0);
9623
10252
  }
9624
10253
  return manifest;