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/mcp.cjs CHANGED
@@ -7,7 +7,7 @@ var crypto2 = require('crypto');
7
7
  var module$1 = require('module');
8
8
  var url = require('url');
9
9
  var childProcess = require('child_process');
10
- require('async_hooks');
10
+ var async_hooks = require('async_hooks');
11
11
  require('os');
12
12
  var worker_threads = require('worker_threads');
13
13
 
@@ -660,13 +660,23 @@ function summarizeTypeScriptSource(relativePath, content) {
660
660
  if (ts__default.default.isImportDeclaration(node) && ts__default.default.isStringLiteral(node.moduleSpecifier)) {
661
661
  summary.imports.push(node.moduleSpecifier.text);
662
662
  }
663
+ if (ts__default.default.isImportEqualsDeclaration(node) && ts__default.default.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression !== void 0 && ts__default.default.isStringLiteral(node.moduleReference.expression)) {
664
+ summary.imports.push(node.moduleReference.expression.text);
665
+ }
666
+ if (ts__default.default.isCallExpression(node) && node.arguments.length >= 1) {
667
+ const firstArgument = node.arguments[0];
668
+ if (firstArgument !== void 0 && ts__default.default.isStringLiteral(firstArgument)) {
669
+ const isDynamicImport = node.expression.kind === ts__default.default.SyntaxKind.ImportKeyword;
670
+ const isRequire = ts__default.default.isIdentifier(node.expression) && node.expression.text === "require";
671
+ if (isDynamicImport || isRequire) summary.imports.push(firstArgument.text);
672
+ }
673
+ }
663
674
  if (ts__default.default.isExportDeclaration(node)) {
664
675
  if (node.moduleSpecifier && ts__default.default.isStringLiteral(node.moduleSpecifier)) {
665
676
  summary.imports.push(node.moduleSpecifier.text);
677
+ summary.reExports.push(node.moduleSpecifier.text);
666
678
  if (node.exportClause && ts__default.default.isNamespaceExport(node.exportClause)) {
667
679
  summary.exports.push(node.exportClause.name.text);
668
- } else if (!node.exportClause) {
669
- summary.reExports.push(node.moduleSpecifier.text);
670
680
  }
671
681
  }
672
682
  if (node.exportClause && ts__default.default.isNamedExports(node.exportClause)) {
@@ -811,7 +821,7 @@ function isCapitalizedName(name) {
811
821
  }
812
822
 
813
823
  // src/ast/profiles/cfamily.ts
814
- var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
824
+ var INCLUDE = /^#\s*include\s*([<"])([^>"]+)[>"]/;
815
825
  var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
816
826
  var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
817
827
  var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
@@ -830,7 +840,7 @@ var cfamilyProfile = {
830
840
  }
831
841
  const included = INCLUDE.exec(trimmed);
832
842
  if (included) {
833
- summary.imports.push(included[1]);
843
+ summary.imports.push(included[1] === '"' ? `./${included[2]}` : included[2]);
834
844
  return;
835
845
  }
836
846
  const used = USING.exec(trimmed);
@@ -1234,7 +1244,15 @@ var pythonProfile = {
1234
1244
  }
1235
1245
  const from = FROM_IMPORT.exec(trimmed);
1236
1246
  if (from) {
1237
- summary.imports.push(from[1]);
1247
+ const module = from[1];
1248
+ if (/^\.+$/.test(module)) {
1249
+ for (const name of from[2].split(",")) {
1250
+ const imported2 = name.replace(/[()]/g, "").trim().split(/\s+as\s+/)[0].trim();
1251
+ if (/^[A-Za-z_]\w*$/.test(imported2)) summary.imports.push(`${module}${imported2}`);
1252
+ }
1253
+ } else {
1254
+ summary.imports.push(module);
1255
+ }
1238
1256
  routePending = false;
1239
1257
  return;
1240
1258
  }
@@ -1283,7 +1301,8 @@ var pythonProfile = {
1283
1301
  };
1284
1302
 
1285
1303
  // src/ast/profiles/ruby.ts
1286
- var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
1304
+ var REQUIRE_RELATIVE = /^require_relative\s+["']([^"']+)["']/;
1305
+ var REQUIRE = /^require\s+["']([^"']+)["']/;
1287
1306
  var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
1288
1307
  var METHOD3 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
1289
1308
  var rubyProfile = {
@@ -1294,6 +1313,11 @@ var rubyProfile = {
1294
1313
  eachLine(content, (line) => {
1295
1314
  const trimmed = line.trim();
1296
1315
  if (!trimmed || trimmed.startsWith("#")) return;
1316
+ const relative8 = REQUIRE_RELATIVE.exec(trimmed);
1317
+ if (relative8) {
1318
+ summary.imports.push(`./${relative8[1]}`);
1319
+ return;
1320
+ }
1297
1321
  const required = REQUIRE.exec(trimmed);
1298
1322
  if (required) {
1299
1323
  summary.imports.push(required[1]);
@@ -1384,7 +1408,8 @@ var rustProfile = {
1384
1408
  };
1385
1409
 
1386
1410
  // src/ast/profiles/sfc.ts
1387
- var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1411
+ var SCRIPT_BLOCK = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
1412
+ var JSX_LANG = /\blang\s*=\s*["']?[tj]sx\b/i;
1388
1413
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
1389
1414
  function componentName(path14) {
1390
1415
  const base = path14.split("/").pop() ?? path14;
@@ -1395,13 +1420,15 @@ var sfcProfile = {
1395
1420
  extensions: [".vue", ".svelte", ".astro"],
1396
1421
  summarize(path14, content) {
1397
1422
  let script = "";
1423
+ let usesJsx = path14.toLowerCase().endsWith(".astro");
1398
1424
  if (path14.toLowerCase().endsWith(".astro")) {
1399
1425
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
1400
1426
  if (frontmatter) script += `${frontmatter[1]}
1401
1427
  `;
1402
1428
  }
1403
1429
  for (const match of content.matchAll(SCRIPT_BLOCK)) {
1404
- script += `${match[1]}
1430
+ if (JSX_LANG.test(match[1] ?? "")) usesJsx = true;
1431
+ script += `${match[2]}
1405
1432
  `;
1406
1433
  }
1407
1434
  const name = componentName(path14);
@@ -1413,7 +1440,7 @@ var sfcProfile = {
1413
1440
  }
1414
1441
  return { summary: finalizeSummary(summary2), quality: "heuristic" };
1415
1442
  }
1416
- const summary = summarizeTypeScriptSource(`${name}.ts`, script);
1443
+ const summary = summarizeTypeScriptSource(`${name}.${usesJsx ? "tsx" : "ts"}`, script);
1417
1444
  summary.reactComponents.push(name);
1418
1445
  summary.exports.push(name);
1419
1446
  return { summary: finalizeSummary(summary), quality: "ast" };
@@ -1847,18 +1874,22 @@ function isBinaryFile(absolutePath) {
1847
1874
  }
1848
1875
 
1849
1876
  // src/ast/imports.ts
1877
+ var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
1850
1878
  var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1851
1879
  var isTsSource = (relativePath) => {
1852
1880
  const lower = relativePath.toLowerCase();
1853
1881
  return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
1854
1882
  };
1855
1883
  function importsOfText(relativePath, text) {
1884
+ if (Buffer.byteLength(text, "utf8") > SUMMARY_MAX_SOURCE_BYTES) return [];
1856
1885
  if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text).imports;
1857
1886
  return summarizeWithProfile(relativePath, text)?.summary.imports ?? [];
1858
1887
  }
1859
1888
  function importsOfFile(relativePath, absolutePath) {
1860
1889
  let buffer;
1861
1890
  try {
1891
+ const stats = fs8__namespace.statSync(absolutePath);
1892
+ if (!stats.isFile() || stats.size > SUMMARY_MAX_SOURCE_BYTES) return [];
1862
1893
  buffer = fs8__namespace.readFileSync(absolutePath);
1863
1894
  } catch {
1864
1895
  return [];
@@ -3103,9 +3134,18 @@ var DEFAULT_SECRET_PATTERNS = [
3103
3134
  // The unquoted twin for YAML, docker-compose and `.env`: those formats never
3104
3135
  // quote their values, so the rules above cannot see them. `valueGroup` keeps
3105
3136
  // `POSTGRES_PASSWORD:` in the output.
3137
+ //
3138
+ // This is the one unquoted rule without `NOT_AFTER_QUOTE`, and the lookbehind is a
3139
+ // string-literal guard rather than a precision rule: a credential assignment *inside* a
3140
+ // literal — `const dsn = "password=hunter2;host=db"`, `curl -d "password=hunter2"` — was
3141
+ // rejected before it could be considered, and a DSN is a credential in a string. What made the
3142
+ // lookbehind redundant is the shape of `UNQUOTED_LITERAL`: a value has to start with a word
3143
+ // character, so `["awsSecret:", "apiKey:"]` — the list of key names the lookbehind was added
3144
+ // for — cannot match here anyway. `redact.ts` still refuses a match whose separator is inside
3145
+ // the literal, which is the case that list actually is.
3106
3146
  rule: "credential-assignment-unquoted",
3107
3147
  pattern: new RegExp(
3108
- `${NOT_AFTER_QUOTE}${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
3148
+ `${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
3109
3149
  "i"
3110
3150
  ),
3111
3151
  valueGroup: 1
@@ -3140,6 +3180,27 @@ var DEFAULT_SECRET_PATTERNS = [
3140
3180
  pattern: /(?<!["'])(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*(?<![=!<>])[:=](?!=)\s*["']([^"'\s]{8,256})["']/i,
3141
3181
  valueGroup: 1
3142
3182
  },
3183
+ {
3184
+ // `${VAR:-fallback}` — a default value in a shell script, a `.env`, docker-compose or a
3185
+ // GitHub Actions workflow.
3186
+ //
3187
+ // This shape defeated every rule above it, and each refusal was individually reasonable:
3188
+ // the quoted rules need a quote after the separator, and the unquoted ones need the value to
3189
+ // begin with a word character — a fallback begins with `$`. So
3190
+ // `POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-MyProdPassword123!}` was emitted whole, `.env` files
3191
+ // and compose files included, which is where that idiom is the norm.
3192
+ //
3193
+ // The **fallback** is what gets masked, not the whole expansion: the variable name is the
3194
+ // readable context, exactly as `POSTGRES_PASSWORD:` is kept by the rule above. A single
3195
+ // interpolation with no operator (`${DB_PASSWORD}`) still does not match — there is nothing
3196
+ // between the braces to mask, and `isTemplateInterpolation` keeps such a value readable.
3197
+ rule: "interpolation-fallback-secret",
3198
+ pattern: new RegExp(
3199
+ `(?:["']${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})\\}["'\`]?`,
3200
+ "i"
3201
+ ),
3202
+ valueGroup: 1
3203
+ },
3143
3204
  {
3144
3205
  // Covers `.env` / shell assignment syntax, which never quotes its values and
3145
3206
  // was therefore invisible to the rule above. Anchored to the line start and
@@ -3167,8 +3228,8 @@ var DEFAULT_SECRET_PATTERNS = [
3167
3228
  valueGroup: 1
3168
3229
  }
3169
3230
  ];
3170
- var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
3171
- var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
3231
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:(?!-----BEGIN)[\s\S])*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
3232
+ 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;
3172
3233
 
3173
3234
  // src/security/redact.ts
3174
3235
  var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
@@ -3262,6 +3323,20 @@ function toGlobal(rule) {
3262
3323
  return new RegExp(rule.pattern.source, [...flags].join(""));
3263
3324
  }
3264
3325
  var LINE_BREAK = /(\r\n|\n|\r)/;
3326
+ var LINE_TERMINATOR = /\r\n|\n|\r/g;
3327
+ var COMPILED_RULES = /* @__PURE__ */ new WeakMap();
3328
+ function compiledRuleFor(rule) {
3329
+ let compiled = COMPILED_RULES.get(rule);
3330
+ if (compiled === void 0) {
3331
+ compiled = {
3332
+ rule: rule.rule,
3333
+ pattern: toGlobal(rule),
3334
+ ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
3335
+ };
3336
+ COMPILED_RULES.set(rule, compiled);
3337
+ }
3338
+ return compiled;
3339
+ }
3265
3340
  function scanAndRedactSecrets(content, rules, filePath) {
3266
3341
  const findings = [];
3267
3342
  let redacted = content;
@@ -3273,18 +3348,15 @@ function scanAndRedactSecrets(content, rules, filePath) {
3273
3348
  blockPattern.lastIndex += 1;
3274
3349
  continue;
3275
3350
  }
3276
- const line = redacted.slice(0, match.index).split("\n").length;
3277
- const newlines = (match[0].match(/\n/g) ?? []).length;
3351
+ const before = redacted.slice(0, match.index);
3352
+ const line = (before.match(LINE_TERMINATOR) ?? []).length + 1;
3353
+ const newlines = (match[0].match(LINE_TERMINATOR) ?? []).length;
3278
3354
  findings.push({ rules: ["private-key-block"], line, path: filePath });
3279
- redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
3355
+ redacted = before + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
3280
3356
  blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
3281
3357
  }
3282
3358
  }
3283
- const compiled = rules.map((rule) => ({
3284
- rule: rule.rule,
3285
- pattern: toGlobal(rule),
3286
- ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
3287
- }));
3359
+ const compiled = rules.map(compiledRuleFor);
3288
3360
  const segments = redacted.split(LINE_BREAK);
3289
3361
  let lineNumber = 1;
3290
3362
  for (let index = 0; index < segments.length; index += 2) {
@@ -3302,8 +3374,10 @@ function scanAndRedactSecrets(content, rules, filePath) {
3302
3374
  }
3303
3375
  const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
3304
3376
  if (group && isSelfNamingConstant(line, group[0], group[1])) continue;
3305
- if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index)) continue;
3306
- if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index)) {
3377
+ if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index, group?.[0])) {
3378
+ continue;
3379
+ }
3380
+ if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index, filePath)) {
3307
3381
  continue;
3308
3382
  }
3309
3383
  if (group && isTemplateInterpolation(line, group[0], group[1])) continue;
@@ -3336,14 +3410,20 @@ function expandOverToken(line, start, end) {
3336
3410
  while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
3337
3411
  return { start: left, end: right };
3338
3412
  }
3339
- function startsInsideStringLiteral(line, matchIndex) {
3413
+ function startsInsideStringLiteral(line, matchIndex, valueStart) {
3340
3414
  let runStart = matchIndex;
3341
3415
  while (runStart > 0 && /[A-Za-z0-9_-]/.test(line[runStart - 1])) runStart -= 1;
3342
3416
  const before = runStart > 0 ? line[runStart - 1] : "";
3343
- return before === '"' || before === "'" || before === "`";
3417
+ if (before !== '"' && before !== "'" && before !== "`") return false;
3418
+ if (valueStart === void 0) return true;
3419
+ return /["'`]/.test(line.slice(matchIndex, valueStart));
3344
3420
  }
3345
3421
  function isTemplateInterpolation(line, start, end) {
3346
- return line[start] === "$" && line[start + 1] === "{" && line[end - 1] === "}";
3422
+ if (line[start] !== "$" || line[start + 1] !== "{" || line[end - 1] !== "}") {
3423
+ return false;
3424
+ }
3425
+ const inner = line.slice(start + 2, end - 1).trim();
3426
+ return /^[A-Za-z_$][A-Za-z0-9_$.()[\]]*$/.test(inner);
3347
3427
  }
3348
3428
  function isSelfNamingConstant(line, start, end) {
3349
3429
  const value = line.slice(start, end);
@@ -3368,9 +3448,19 @@ function isUnquotedValue(line, start) {
3368
3448
  const before = start > 0 ? line[start - 1] : "";
3369
3449
  return before !== '"' && before !== "'" && before !== "`";
3370
3450
  }
3371
- var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|declare|export|using)\s+$/;
3451
+ var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|using)\s+$/;
3452
+ var ASSIGNMENT_FILE = /\.(?:sh|bash|zsh|ksh|fish|env|ya?ml|toml|ini|conf|cfg|properties|tf|tfvars|service)$/i;
3453
+ var ASSIGNMENT_BASENAME = /^(?:\.env(?:\.[^/]*)?|\.envrc|\.profile|\.bashrc|\.zshrc|\.bash_profile)$/i;
3454
+ function isAssignmentFile(filePath) {
3455
+ if (filePath === void 0) return false;
3456
+ const normalised = filePath.replace(/\\/g, "/");
3457
+ if (ASSIGNMENT_FILE.test(normalised)) return true;
3458
+ const base = normalised.slice(normalised.lastIndexOf("/") + 1);
3459
+ return ASSIGNMENT_BASENAME.test(base);
3460
+ }
3372
3461
  var DECLARATION_WINDOW = 40;
3373
- function isDeclaredBinding(line, matchIndex) {
3462
+ function isDeclaredBinding(line, matchIndex, filePath) {
3463
+ if (isAssignmentFile(filePath)) return false;
3374
3464
  let runStart = matchIndex;
3375
3465
  while (runStart > 0 && /[A-Za-z0-9_$]/.test(line[runStart - 1])) runStart -= 1;
3376
3466
  const from = Math.max(0, runStart - DECLARATION_WINDOW);
@@ -3412,6 +3502,15 @@ function readLinkTarget(linkPath, fallback) {
3412
3502
  return fallback;
3413
3503
  }
3414
3504
  }
3505
+ function brokenLinkRecord(linkPath, relativePath, rootReal) {
3506
+ const rawTarget = readLinkTarget(linkPath, linkPath);
3507
+ const resolvedFallback = path10__namespace.resolve(path10__namespace.dirname(linkPath), rawTarget);
3508
+ return isInside(rootReal, resolvedFallback) ? { path: relativePath, reason: "read-error", detail: "broken symlink" } : {
3509
+ path: relativePath,
3510
+ reason: "outside-root",
3511
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
3512
+ };
3513
+ }
3415
3514
  function isInside(root, candidate) {
3416
3515
  const relative8 = path10__namespace.relative(root, candidate);
3417
3516
  return relative8 === "" || !relative8.startsWith(`..${path10__namespace.sep}`) && relative8 !== ".." && !path10__namespace.isAbsolute(relative8);
@@ -3501,6 +3600,13 @@ function isSensitivePath(relativePath) {
3501
3600
  return matchesSensitiveList(posixPath, base);
3502
3601
  }
3503
3602
  var GLOBAL_PATTERNS = /* @__PURE__ */ new WeakMap();
3603
+ var GLOB_REGEX_CACHE = /* @__PURE__ */ new Map();
3604
+ var GITIGNORE_REGEX_CACHE = /* @__PURE__ */ new Map();
3605
+ var REGEX_CACHE_LIMIT = 4096;
3606
+ function rememberCompiled(cache, key, value) {
3607
+ if (cache.size >= REGEX_CACHE_LIMIT) cache.clear();
3608
+ cache.set(key, value);
3609
+ }
3504
3610
  function globalPattern(pattern) {
3505
3611
  let compiled = GLOBAL_PATTERNS.get(pattern);
3506
3612
  if (compiled === void 0) {
@@ -3511,8 +3617,8 @@ function globalPattern(pattern) {
3511
3617
  }
3512
3618
  var DEFAULT_PATH_RULES = [...DEFAULT_SECRET_PATTERNS];
3513
3619
  function pathScanRules(extraRules) {
3514
- if (extraRules === void 0 || extraRules.length === 0) return DEFAULT_PATH_RULES;
3515
- return [...DEFAULT_SECRET_PATTERNS, ...extraRules];
3620
+ if (extraRules === void 0) return DEFAULT_PATH_RULES;
3621
+ return extraRules;
3516
3622
  }
3517
3623
  function findSecretInPath(relativePath, extraRules) {
3518
3624
  const posixPath = relativePath.replace(/\\/g, "/");
@@ -3561,11 +3667,22 @@ function maskSkipPaths(records, extraRules) {
3561
3667
  });
3562
3668
  }
3563
3669
  function globToRegExp(glob) {
3670
+ const cached = GLOB_REGEX_CACHE.get(glob);
3671
+ if (cached !== void 0) return cached;
3564
3672
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
3565
3673
  const anchored = normalized.startsWith("/");
3566
3674
  const body = anchored ? normalized.slice(1) : normalized;
3567
3675
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
3568
- return new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
3676
+ let compiled;
3677
+ try {
3678
+ compiled = new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
3679
+ } catch (error) {
3680
+ throw new Error(
3681
+ `"${glob}" is not a valid glob: ${error.message}. A character class must be ascending, as in "[a-z]".`
3682
+ );
3683
+ }
3684
+ rememberCompiled(GLOB_REGEX_CACHE, glob, compiled);
3685
+ return compiled;
3569
3686
  }
3570
3687
  function globBodyToRegExp(body, allowBraces) {
3571
3688
  let out = "";
@@ -3677,6 +3794,14 @@ function escapeRegExpChar(character) {
3677
3794
  return /[\\^$.*+?()[\]{}|/]/.test(character) ? `\\${character}` : character;
3678
3795
  }
3679
3796
  function matchGitignorePattern(target, pattern) {
3797
+ let compiled = GITIGNORE_REGEX_CACHE.get(pattern);
3798
+ if (compiled === void 0) {
3799
+ compiled = compileGitignorePattern(pattern);
3800
+ rememberCompiled(GITIGNORE_REGEX_CACHE, pattern, compiled);
3801
+ }
3802
+ return compiled.test(target);
3803
+ }
3804
+ function compileGitignorePattern(pattern) {
3680
3805
  const anchored = pattern.startsWith("/");
3681
3806
  const body = anchored ? pattern.slice(1) : pattern;
3682
3807
  let source = "";
@@ -3725,7 +3850,11 @@ function matchGitignorePattern(target, pattern) {
3725
3850
  source += escapeRegExpChar(character);
3726
3851
  }
3727
3852
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
3728
- return new RegExp(`${prefix}${source}(?:/.*)?$`).test(target);
3853
+ try {
3854
+ return new RegExp(`${prefix}${source}(?:/.*)?$`);
3855
+ } catch {
3856
+ return /$^/;
3857
+ }
3729
3858
  }
3730
3859
  function matchesGitignoreRule(rule, scoped, isDirectory) {
3731
3860
  if (!rule.dirOnly) return matchGitignorePattern(scoped, rule.pattern);
@@ -3755,14 +3884,31 @@ function gitignoreVerdict(layers, relativePath, isDirectory) {
3755
3884
  }
3756
3885
  return verdict;
3757
3886
  }
3758
- function defaultIgnoreReason(relativePath) {
3887
+ var DEFAULT_TOOL_DIR_NAMES = ["snapshot"];
3888
+ function toolDirNamesFor(toolOwnedPaths) {
3889
+ const names = new Set(DEFAULT_TOOL_DIR_NAMES);
3890
+ for (const entry of toolOwnedPaths) {
3891
+ const base = entry.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop();
3892
+ if (base !== void 0 && base.length > 0) names.add(base);
3893
+ }
3894
+ return [...names];
3895
+ }
3896
+ function isToolLeftoverName(name, dirNames) {
3897
+ for (const dirName of dirNames) {
3898
+ if (name.startsWith(`${dirName}.backup-`) || name.startsWith(`${dirName}.partial-`)) {
3899
+ return true;
3900
+ }
3901
+ }
3902
+ return false;
3903
+ }
3904
+ function defaultIgnoreReason(relativePath, toolDirNames = DEFAULT_TOOL_DIR_NAMES) {
3759
3905
  const posixPath = relativePath.replace(/\\/g, "/");
3760
3906
  const parts = posixPath.split("/");
3761
3907
  for (let i = 0; i < parts.length - 1; i++) {
3762
3908
  if (DEFAULT_IGNORED_DIRS.includes(parts[i])) {
3763
3909
  return `built-in ignore: directory "${parts[i]}"`;
3764
3910
  }
3765
- if (parts[i].startsWith("snapshot.backup-")) {
3911
+ if (isToolLeftoverName(parts[i], toolDirNames)) {
3766
3912
  return `built-in ignore: leftover snapshot backup "${parts[i]}"`;
3767
3913
  }
3768
3914
  }
@@ -3770,6 +3916,9 @@ function defaultIgnoreReason(relativePath) {
3770
3916
  if (DEFAULT_IGNORED_FILES.includes(base)) {
3771
3917
  return `built-in ignore: filename "${base}"`;
3772
3918
  }
3919
+ if (isToolLeftoverName(base, toolDirNames)) {
3920
+ return `built-in ignore: leftover snapshot backup "${base}"`;
3921
+ }
3773
3922
  return null;
3774
3923
  }
3775
3924
  function listIgnoredPaths(cwd) {
@@ -3838,41 +3987,72 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
3838
3987
  }
3839
3988
  const reported = /* @__PURE__ */ new Set();
3840
3989
  const kept = [];
3990
+ const report = (linkedPath, record) => {
3991
+ if (reported.has(linkedPath)) return;
3992
+ reported.add(linkedPath);
3993
+ skipped.push(record);
3994
+ };
3841
3995
  for (const candidate of candidates) {
3842
3996
  const parts = candidate.split("/");
3843
- let linkedDir = null;
3997
+ let linkedPath = null;
3844
3998
  for (let depth = 1; depth <= parts.length; depth += 1) {
3845
3999
  const prefix = parts.slice(0, depth).join("/");
3846
4000
  if (isLink(path10__namespace.join(cwd, prefix))) {
3847
- linkedDir = prefix;
4001
+ linkedPath = prefix;
3848
4002
  break;
3849
4003
  }
3850
4004
  }
3851
- if (linkedDir === null) {
4005
+ if (linkedPath === null) {
3852
4006
  kept.push(candidate);
3853
4007
  continue;
3854
4008
  }
3855
- if (reported.has(linkedDir)) continue;
3856
- reported.add(linkedDir);
3857
- const absolute = path10__namespace.join(cwd, linkedDir);
3858
- let targetReal = absolute;
4009
+ const absolute = path10__namespace.join(cwd, linkedPath);
4010
+ const rawTarget = readLinkTarget(absolute, absolute);
4011
+ let targetReal = null;
3859
4012
  try {
3860
4013
  targetReal = fs8__namespace.realpathSync(absolute);
3861
4014
  } catch {
4015
+ targetReal = null;
3862
4016
  }
3863
- const target = readLinkTarget(absolute, targetReal);
3864
- const what = linkedDir === candidate ? "symbolic link" : "directory link";
3865
- skipped.push(
3866
- isInside(rootReal, targetReal) ? {
3867
- path: linkedDir,
3868
- reason: "symlink",
3869
- detail: `${what} not followed (target: ${target})`
3870
- } : {
3871
- path: linkedDir,
4017
+ if (targetReal === null) {
4018
+ report(linkedPath, brokenLinkRecord(absolute, linkedPath, rootReal));
4019
+ continue;
4020
+ }
4021
+ if (!isInside(rootReal, targetReal)) {
4022
+ report(linkedPath, {
4023
+ path: linkedPath,
3872
4024
  reason: "outside-root",
3873
- detail: `link target resolves outside the scanned root (target: ${target}); not followed`
3874
- }
3875
- );
4025
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
4026
+ });
4027
+ continue;
4028
+ }
4029
+ let stats = null;
4030
+ try {
4031
+ stats = fs8__namespace.statSync(absolute);
4032
+ } catch {
4033
+ stats = null;
4034
+ }
4035
+ if (stats === null) {
4036
+ report(linkedPath, { path: linkedPath, reason: "read-error", detail: "unreadable symlink" });
4037
+ continue;
4038
+ }
4039
+ if (linkedPath !== candidate || stats.isDirectory()) {
4040
+ report(linkedPath, {
4041
+ path: linkedPath,
4042
+ reason: "symlink",
4043
+ detail: `directory link not followed (target: ${rawTarget})`
4044
+ });
4045
+ continue;
4046
+ }
4047
+ if (!stats.isFile()) {
4048
+ report(linkedPath, {
4049
+ path: linkedPath,
4050
+ reason: "symlink",
4051
+ detail: `link to neither a file nor a directory (target: ${rawTarget})`
4052
+ });
4053
+ continue;
4054
+ }
4055
+ kept.push(candidate);
3876
4056
  }
3877
4057
  return kept;
3878
4058
  }
@@ -3919,6 +4099,7 @@ function collectFiles(cwd, options = {}) {
3919
4099
  (options.toolOwnedPaths ?? []).map((entry) => entry.replace(/\\/g, "/"))
3920
4100
  );
3921
4101
  const isToolOwned = makeToolOwnedDecider(cwd, [...toolOwnedPaths]);
4102
+ const toolDirNames = toolDirNamesFor(toolOwnedPaths);
3922
4103
  let candidates;
3923
4104
  const skipped = [];
3924
4105
  const discovery = options.discovery ?? "auto";
@@ -3947,7 +4128,7 @@ function collectFiles(cwd, options = {}) {
3947
4128
  }
3948
4129
  const files = candidates.filter((file) => {
3949
4130
  if (isToolOwned(file)) return false;
3950
- const ignored = defaultIgnoreReason(file);
4131
+ const ignored = defaultIgnoreReason(file, toolDirNames);
3951
4132
  if (ignored) {
3952
4133
  skipped.push({ path: file, reason: "default-ignore", detail: ignored });
3953
4134
  return false;
@@ -4002,6 +4183,7 @@ function rootRealFor(root) {
4002
4183
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
4003
4184
  const results = { files: [], skipped: [] };
4004
4185
  const stack = [];
4186
+ const toolDirNames = toolDirNamesFor(reservedPaths);
4005
4187
  const relativeToBase = (target) => path10__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
4006
4188
  const enter = (current, layers) => {
4007
4189
  if (visited.has(current)) return false;
@@ -4046,11 +4228,11 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
4046
4228
  });
4047
4229
  continue;
4048
4230
  }
4049
- if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
4231
+ if (DEFAULT_IGNORED_FILES.includes(entry.name) || isToolLeftoverName(entry.name, toolDirNames)) {
4050
4232
  results.skipped.push({
4051
4233
  path: relPath,
4052
4234
  reason: "default-ignore",
4053
- detail: "built-in ignore: filename"
4235
+ detail: isToolLeftoverName(entry.name, toolDirNames) ? `built-in ignore: leftover snapshot backup "${entry.name}" (not descended)` : "built-in ignore: filename"
4054
4236
  });
4055
4237
  continue;
4056
4238
  }
@@ -4071,7 +4253,7 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
4071
4253
  try {
4072
4254
  targetReal = fs8__namespace.realpathSync(fullPath);
4073
4255
  } catch {
4074
- results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
4256
+ results.skipped.push(brokenLinkRecord(fullPath, relPath, rootReal));
4075
4257
  continue;
4076
4258
  }
4077
4259
  let stats;
@@ -4196,15 +4378,19 @@ function normalizeBudgets(budgets, maxPartBytes) {
4196
4378
  }
4197
4379
  function normalizeResolvedConfig(config) {
4198
4380
  const discovery = isDiscoveryMode(config.discovery) ? config.discovery : "auto";
4381
+ const monolithWanted = config.includeMonolith === true || config.artifacts?.monolith === true;
4382
+ const artifacts = resolveArtifacts(
4383
+ config.artifacts,
4384
+ void 0,
4385
+ monolithWanted,
4386
+ monolithWanted
4387
+ );
4199
4388
  return markResolvedConfig({
4200
4389
  ...config,
4201
4390
  discovery,
4391
+ includeMonolith: artifacts.monolith,
4202
4392
  secretRules: compileSecretRules(config.secretRules),
4203
- artifacts: resolveArtifacts(
4204
- config.artifacts,
4205
- void 0,
4206
- config.includeMonolith === true
4207
- ),
4393
+ artifacts,
4208
4394
  include: resolveInclude(config.include),
4209
4395
  preset: typeof config.preset === "string" ? config.preset : "generic",
4210
4396
  presetExclude: [
@@ -4413,6 +4599,16 @@ async function importConfigModule(fullPath) {
4413
4599
  url$1.searchParams.set("codebaseSnapshotLoad", String(moduleConfigLoadToken));
4414
4600
  return await import(url$1.href);
4415
4601
  }
4602
+ function attachConfigDir(config, filePath) {
4603
+ if (typeof config === "object" && config !== null) {
4604
+ Object.defineProperty(config, CONFIG_DIR_SYMBOL, {
4605
+ value: path10__namespace.dirname(filePath),
4606
+ enumerable: false,
4607
+ configurable: true
4608
+ });
4609
+ }
4610
+ return config;
4611
+ }
4416
4612
  async function loadConfigFileDetailed(cwd) {
4417
4613
  const warnings = [];
4418
4614
  const errors = [];
@@ -4428,7 +4624,7 @@ async function loadConfigFileDetailed(cwd) {
4428
4624
  );
4429
4625
  }
4430
4626
  assertKnownConfigKeys(config, fullPath);
4431
- return { config, file: fullPath, warnings, errors };
4627
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
4432
4628
  } catch (err) {
4433
4629
  const raw = err.message;
4434
4630
  const message = raw.includes(fullPath) ? raw : `${fullPath}: ${raw}`;
@@ -4447,7 +4643,7 @@ async function loadConfigFileDetailed(cwd) {
4447
4643
  const imported = await importConfigModule(fullPath);
4448
4644
  const hasDefault = Object.prototype.hasOwnProperty.call(imported, "default");
4449
4645
  const config = readConfigExport(hasDefault ? imported.default : imported, fullPath);
4450
- return { config, file: fullPath, warnings, errors };
4646
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
4451
4647
  } catch (err) {
4452
4648
  const message = `Could not load ${fullPath}: ${err.message}`;
4453
4649
  warnings.push(message);
@@ -4465,7 +4661,12 @@ async function loadConfigFileDetailed(cwd) {
4465
4661
  snapshotKey,
4466
4662
  `${pkgJsonPath} (the "snapshot" key)`
4467
4663
  );
4468
- return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
4664
+ return {
4665
+ config: attachConfigDir(snapshotKey, pkgJsonPath),
4666
+ file: pkgJsonPath,
4667
+ warnings,
4668
+ errors
4669
+ };
4469
4670
  }
4470
4671
  if (snapshotKey !== void 0) {
4471
4672
  const kind = Array.isArray(snapshotKey) ? "array" : typeof snapshotKey;
@@ -4504,7 +4705,7 @@ var DEFAULT_ARTIFACTS = {
4504
4705
  function isArtifactName(value) {
4505
4706
  return ARTIFACT_NAMES.includes(value);
4506
4707
  }
4507
- function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
4708
+ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride, only) {
4508
4709
  const artifacts = {
4509
4710
  ...DEFAULT_ARTIFACTS,
4510
4711
  // `includeMonolith` predates this selection and stays the switch for it.
@@ -4529,6 +4730,21 @@ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride)
4529
4730
  if (monolithOverride !== void 0) {
4530
4731
  artifacts.monolith = monolithOverride;
4531
4732
  }
4733
+ if (only !== void 0 && only.length > 0) {
4734
+ const keep = /* @__PURE__ */ new Set();
4735
+ for (const name of only) {
4736
+ if (!isArtifactName(name)) {
4737
+ throw new Error(
4738
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
4739
+ );
4740
+ }
4741
+ keep.add(name);
4742
+ }
4743
+ for (const name of ARTIFACT_NAMES) {
4744
+ artifacts[name] = keep.has(name);
4745
+ }
4746
+ return artifacts;
4747
+ }
4532
4748
  for (const name of skipped ?? []) {
4533
4749
  if (!isArtifactName(name)) {
4534
4750
  throw new Error(
@@ -4829,7 +5045,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
4829
5045
  userConfig?.artifacts,
4830
5046
  cliOptions?.skipArtifacts,
4831
5047
  includeMonolith,
4832
- cliMonolith
5048
+ cliMonolith,
5049
+ cliOptions?.onlyArtifacts
4833
5050
  );
4834
5051
  const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
4835
5052
  const maxPartBytes = boundedBytes(
@@ -5003,7 +5220,18 @@ function configDigest(config) {
5003
5220
  excludePatterns: config.excludePatterns,
5004
5221
  preset: config.preset,
5005
5222
  layersMode: config.layersMode,
5006
- budgets: config.budgets,
5223
+ // `budgets.byLayer` is a plain object, and `JSON.stringify` preserves insertion order — so
5224
+ // `{ docs, api }` and `{ api, docs }` hashed differently despite being the same settings, and
5225
+ // `validate` reported configuration drift for a snapshot nobody had changed. The keys are
5226
+ // canonicalised here; nothing else about the object is order-dependent.
5227
+ budgets: {
5228
+ default: config.budgets.default,
5229
+ byLayer: Object.fromEntries(
5230
+ Object.entries(config.budgets.byLayer).sort(
5231
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
5232
+ )
5233
+ )
5234
+ },
5007
5235
  // The matcher is what makes a layer a layer. Hashing only the names meant that
5008
5236
  // moving `{ name: "api", match: "src/api/**" }` to `"src/v2/**"` produced the same
5009
5237
  // digest, so `validate` reported "valid and up to date" for a snapshot whose files
@@ -5022,9 +5250,21 @@ function configDigest(config) {
5022
5250
  }
5023
5251
 
5024
5252
  // src/partitioner/preview.ts
5253
+ var LINE_ENDINGS = /\r\n|\r/g;
5254
+ var normaliseLineEndings = (content) => content.replace(LINE_ENDINGS, "\n");
5025
5255
  function countLines(content) {
5026
5256
  if (content.length === 0) return 0;
5027
- return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
5257
+ let breaks = 0;
5258
+ for (let index = 0; index < content.length; index += 1) {
5259
+ const code = content.charCodeAt(index);
5260
+ if (code === 10) {
5261
+ breaks += 1;
5262
+ continue;
5263
+ }
5264
+ if (code === 13 && content.charCodeAt(index + 1) !== 10) breaks += 1;
5265
+ }
5266
+ const last = content.charCodeAt(content.length - 1);
5267
+ return last === 10 || last === 13 ? breaks : breaks + 1;
5028
5268
  }
5029
5269
  function truncateToBytes(content, maxBytes) {
5030
5270
  if (maxBytes <= 0) return { content: "", isTruncated: content.length > 0 };
@@ -5035,17 +5275,18 @@ function truncateToBytes(content, maxBytes) {
5035
5275
  return { content: slice, isTruncated: true };
5036
5276
  }
5037
5277
  function truncatePreview(content, headLines, tailLines) {
5038
- const lines = content.split("\n");
5039
- const total = countLines(content);
5278
+ const normalised = normaliseLineEndings(content);
5279
+ const lines = normalised.split("\n");
5280
+ const total = countLines(normalised);
5040
5281
  if (total <= headLines + tailLines) {
5041
5282
  return {
5042
- content,
5283
+ content: normalised,
5043
5284
  isTruncated: false,
5044
5285
  originalLines: total,
5045
5286
  includedLines: total
5046
5287
  };
5047
5288
  }
5048
- const window = content.endsWith("\n") ? lines.slice(0, -1) : lines;
5289
+ const window = normalised.endsWith("\n") ? lines.slice(0, -1) : lines;
5049
5290
  const head = window.slice(0, headLines);
5050
5291
  const tail = tailLines > 0 ? window.slice(window.length - tailLines) : [];
5051
5292
  const omitted = total - head.length - tail.length;
@@ -5148,6 +5389,10 @@ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5148
5389
  ".mjs",
5149
5390
  ".cjs"
5150
5391
  ]);
5392
+ var PYTHON_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".pyi"]);
5393
+ var RUBY_EXTENSIONS = /* @__PURE__ */ new Set([".rb", ".rake", ".gemspec"]);
5394
+ var C_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"]);
5395
+ var PYTHON_SUFFIXES = [".py", ".pyi", "/__init__.py", "/__init__.pyi"];
5151
5396
  function buildImportResolver(files) {
5152
5397
  const byPath = /* @__PURE__ */ new Map();
5153
5398
  const byPathFolded = /* @__PURE__ */ new Map();
@@ -5174,11 +5419,45 @@ function buildImportResolver(files) {
5174
5419
  return { byPath, byPathFolded, byStem, byStemFolded };
5175
5420
  }
5176
5421
  function resolveSpecifier(fromFile, specifier, resolver) {
5422
+ const extension = path10__namespace.posix.extname(fromFile).toLowerCase();
5423
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5424
+ const firstOf = (candidates) => {
5425
+ for (const candidate of candidates) {
5426
+ const hit = exact(candidate);
5427
+ if (hit) return hit;
5428
+ }
5429
+ return null;
5430
+ };
5431
+ if (PYTHON_EXTENSIONS.has(extension) && specifier.startsWith(".")) {
5432
+ const dots = /^\.+/.exec(specifier)?.[0].length ?? 0;
5433
+ const rest = specifier.slice(dots).replace(/\./g, "/");
5434
+ let base = path10__namespace.posix.dirname(fromFile);
5435
+ for (let up = 1; up < dots; up += 1) base = path10__namespace.posix.dirname(base);
5436
+ const joined2 = rest ? path10__namespace.posix.normalize(path10__namespace.posix.join(base, rest)) : base;
5437
+ return firstOf([joined2, ...PYTHON_SUFFIXES.map((suffix) => `${joined2}${suffix}`)]);
5438
+ }
5439
+ if (RUBY_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
5440
+ const joined2 = path10__namespace.posix.normalize(
5441
+ path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
5442
+ );
5443
+ return firstOf([`${joined2}.rb`, joined2]);
5444
+ }
5445
+ if (C_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
5446
+ const joined2 = path10__namespace.posix.normalize(
5447
+ path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
5448
+ );
5449
+ return exact(joined2);
5450
+ }
5451
+ if (extension === ".dart" && !specifier.includes(":")) {
5452
+ const joined2 = path10__namespace.posix.normalize(
5453
+ path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
5454
+ );
5455
+ return firstOf([joined2, `${joined2}.dart`]);
5456
+ }
5177
5457
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5178
5458
  const joined = path10__namespace.posix.normalize(
5179
5459
  path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
5180
5460
  );
5181
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5182
5461
  const direct = exact(joined);
5183
5462
  if (direct) return direct;
5184
5463
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
@@ -5289,10 +5568,13 @@ function describeMatcher(matcher) {
5289
5568
  return String(matcher);
5290
5569
  }
5291
5570
  function normalizeTarget(cwd, target) {
5292
- const absolute = path10__namespace.resolve(cwd, target);
5293
- const relative8 = path10__namespace.relative(cwd, absolute);
5294
- if (relative8.startsWith("..") || path10__namespace.isAbsolute(relative8)) return target;
5295
- return relative8.split(path10__namespace.sep).join("/");
5571
+ const resolvedCwd = path10__namespace.resolve(cwd);
5572
+ const absolute = path10__namespace.resolve(resolvedCwd, target);
5573
+ const relative8 = path10__namespace.relative(resolvedCwd, absolute);
5574
+ if (relative8 === "" || relative8 === ".." || relative8.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative8)) {
5575
+ return { relative: target.replace(/\\/g, "/"), outsideRoot: true };
5576
+ }
5577
+ return { relative: relative8.split(path10__namespace.sep).join("/"), outsideRoot: false };
5296
5578
  }
5297
5579
  function findRuleMatch(rules, relativePath) {
5298
5580
  return rules.find(
@@ -5319,9 +5601,19 @@ function readManifest2(config) {
5319
5601
  }
5320
5602
  }
5321
5603
  function explainPath(config, target) {
5322
- const relative8 = normalizeTarget(config.cwd, target);
5323
- const absolute = path10__namespace.join(config.cwd, relative8);
5604
+ const normalized = normalizeTarget(config.cwd, target);
5605
+ const relative8 = normalized.relative;
5324
5606
  const maskedPath2 = findSecretInPath(relative8, config.secretRules)?.masked ?? relative8;
5607
+ if (normalized.outsideRoot) {
5608
+ return {
5609
+ path: maskedPath2,
5610
+ exists: false,
5611
+ included: false,
5612
+ reason: "outside-root",
5613
+ detail: "path resolves outside the configured repository root and was not inspected"
5614
+ };
5615
+ }
5616
+ const absolute = path10__namespace.join(config.cwd, relative8);
5325
5617
  const result = {
5326
5618
  path: maskedPath2,
5327
5619
  exists: fs8__namespace.existsSync(absolute),
@@ -5471,41 +5763,62 @@ function renderExplainText(result) {
5471
5763
  );
5472
5764
  return lines.join("\n");
5473
5765
  }
5766
+ new async_hooks.AsyncLocalStorage();
5767
+ new async_hooks.AsyncLocalStorage();
5474
5768
 
5475
5769
  // src/ast/structured.ts
5476
5770
  function stripJsonComments2(content) {
5477
5771
  const text = content.replace(/^\uFEFF/, "");
5772
+ const withoutComments = stripComments(text);
5773
+ return stripTrailingCommas2(withoutComments);
5774
+ }
5775
+ function stripComments(text) {
5478
5776
  let out = "";
5479
5777
  let inString = false;
5480
- let inLineComment = false;
5481
- let inBlockComment = false;
5482
- for (let i = 0; i < text.length; i++) {
5483
- const char = text[i];
5484
- const next = text[i + 1];
5485
- if (inLineComment) {
5486
- if (char === "\n") {
5487
- inLineComment = false;
5488
- out += char;
5489
- }
5778
+ let escaped = false;
5779
+ for (let index = 0; index < text.length; index += 1) {
5780
+ const char = text[index];
5781
+ if (inString) {
5782
+ out += char;
5783
+ if (escaped) escaped = false;
5784
+ else if (char === "\\") escaped = true;
5785
+ else if (char === '"') inString = false;
5490
5786
  continue;
5491
5787
  }
5492
- if (inBlockComment) {
5493
- if (char === "*" && next === "/") {
5494
- inBlockComment = false;
5495
- i += 1;
5788
+ if (char === '"') {
5789
+ inString = true;
5790
+ out += char;
5791
+ continue;
5792
+ }
5793
+ if (char === "/" && text[index + 1] === "/") {
5794
+ while (index < text.length && text[index] !== "\n" && text[index] !== "\r") index += 1;
5795
+ index -= 1;
5796
+ continue;
5797
+ }
5798
+ if (char === "/" && text[index + 1] === "*") {
5799
+ index += 2;
5800
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
5801
+ if (text[index] === "\n" || text[index] === "\r") out += text[index];
5802
+ index += 1;
5496
5803
  }
5804
+ index += 1;
5497
5805
  continue;
5498
5806
  }
5807
+ out += char;
5808
+ }
5809
+ return out;
5810
+ }
5811
+ function stripTrailingCommas2(text) {
5812
+ let out = "";
5813
+ let inString = false;
5814
+ let escaped = false;
5815
+ for (let index = 0; index < text.length; index += 1) {
5816
+ const char = text[index];
5499
5817
  if (inString) {
5500
5818
  out += char;
5501
- if (char === "\\") {
5502
- if (next !== void 0) {
5503
- out += next;
5504
- i += 1;
5505
- }
5506
- } else if (char === '"') {
5507
- inString = false;
5508
- }
5819
+ if (escaped) escaped = false;
5820
+ else if (char === "\\") escaped = true;
5821
+ else if (char === '"') inString = false;
5509
5822
  continue;
5510
5823
  }
5511
5824
  if (char === '"') {
@@ -5513,15 +5826,10 @@ function stripJsonComments2(content) {
5513
5826
  out += char;
5514
5827
  continue;
5515
5828
  }
5516
- if (char === "/" && next === "/") {
5517
- inLineComment = true;
5518
- i += 1;
5519
- continue;
5520
- }
5521
- if (char === "/" && next === "*") {
5522
- inBlockComment = true;
5523
- i += 1;
5524
- continue;
5829
+ if (char === ",") {
5830
+ let next = index + 1;
5831
+ while (next < text.length && /\s/.test(text[next])) next += 1;
5832
+ if (text[next] === "}" || text[next] === "]") continue;
5525
5833
  }
5526
5834
  out += char;
5527
5835
  }
@@ -5564,31 +5872,45 @@ function summarizeJson(content) {
5564
5872
  };
5565
5873
  }
5566
5874
  }
5875
+ function frontMatterEnd(lines) {
5876
+ if (lines.length < 3 || lines[0].trim() !== "---") return 0;
5877
+ for (let index = 1; index < lines.length; index += 1) {
5878
+ const line = lines[index];
5879
+ const candidate = line.trim();
5880
+ if (candidate === "---" || candidate === "...") {
5881
+ return index > 1 ? index : 0;
5882
+ }
5883
+ if (candidate === "") continue;
5884
+ if (/^#(\s|$)/.test(candidate)) continue;
5885
+ if (/^(`{3,}|~{3,})/.test(candidate)) return 0;
5886
+ if (/^#{1,6}\s+\S/.test(candidate)) return 0;
5887
+ const isYamlMapping = /^[A-Za-z0-9_.-]+\s*:(\s|$)/.test(candidate);
5888
+ const isListItem = /^-(?:(\s|$))/.test(candidate);
5889
+ const isIndented = /^[ \t]/.test(line);
5890
+ if (!isYamlMapping && !isListItem && !isIndented) return 0;
5891
+ }
5892
+ return 0;
5893
+ }
5567
5894
  function summarizeMarkdown(content) {
5568
5895
  const headings = [];
5569
5896
  const frontmatterKeys = [];
5570
- const lines = content.replace(/^\uFEFF/, "").split("\n");
5897
+ const lines = content.replace(/^\uFEFF/, "").split(/\r\n|\n|\r/);
5571
5898
  let inFrontmatter = false;
5572
5899
  let fence = null;
5573
- let closesFrontmatter = false;
5574
- for (let index = 1; index < lines.length; index++) {
5575
- if (lines[index].trim() === "---") {
5576
- closesFrontmatter = true;
5577
- break;
5578
- }
5579
- }
5900
+ const frontMatterEndLine = frontMatterEnd(lines);
5580
5901
  for (let index = 0; index < lines.length; index++) {
5581
5902
  const line = lines[index];
5582
- if (index === 0 && line.trim() === "---" && closesFrontmatter) {
5903
+ if (index === 0 && frontMatterEndLine > 0) {
5583
5904
  inFrontmatter = true;
5584
5905
  continue;
5585
5906
  }
5586
5907
  if (inFrontmatter) {
5587
- if (line.trim() === "---") {
5908
+ const trimmedFrontmatter = line.trim();
5909
+ if (trimmedFrontmatter === "---" || trimmedFrontmatter === "...") {
5588
5910
  inFrontmatter = false;
5589
5911
  continue;
5590
5912
  }
5591
- const match = /^([a-zA-Z0-9_-]+):/.exec(line);
5913
+ const match = /^([A-Za-z0-9_.-]+)\s*:/.exec(line);
5592
5914
  if (match) frontmatterKeys.push(match[1]);
5593
5915
  continue;
5594
5916
  }
@@ -5620,25 +5942,36 @@ function hasCollapsibleBody(node) {
5620
5942
  const body = node.body;
5621
5943
  return body !== void 0 && ts__default.default.isBlock(body);
5622
5944
  }
5623
- function isTsxFileName(fileName) {
5624
- return fileName.toLowerCase().endsWith(".tsx");
5945
+ function scriptKindFor(fileName) {
5946
+ const lower = fileName.toLowerCase();
5947
+ if (lower.endsWith(".tsx")) return ts__default.default.ScriptKind.TSX;
5948
+ if (lower.endsWith(".jsx")) return ts__default.default.ScriptKind.JSX;
5949
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs")) {
5950
+ return ts__default.default.ScriptKind.JS;
5951
+ }
5952
+ return ts__default.default.ScriptKind.TS;
5625
5953
  }
5626
5954
  function skeletonizeTypeScript(source, fileName = "file.ts") {
5627
- const scriptKind = isTsxFileName(fileName) ? ts__default.default.ScriptKind.TSX : ts__default.default.ScriptKind.TS;
5628
5955
  const sourceFile = ts__default.default.createSourceFile(
5629
5956
  fileName,
5630
5957
  source,
5631
5958
  ts__default.default.ScriptTarget.Latest,
5632
- true,
5633
- scriptKind
5959
+ // `setParentNodes: false`. Nothing here reads `.parent` — the one `getStart` call is given the
5960
+ // source file explicitly, which is the reason that argument exists. Asking for parent pointers
5961
+ // makes the parser run a second full pass over every node in the file to set them, on top of
5962
+ // the summary parse this same file already pays for.
5963
+ false,
5964
+ scriptKindFor(fileName)
5634
5965
  );
5635
5966
  const spans = [];
5636
5967
  const visit = (node) => {
5637
5968
  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)) {
5638
5969
  const body = node.body;
5639
- const start = body.getStart(sourceFile);
5640
- const end = body.getEnd();
5641
- if (end > start) spans.push({ start, end });
5970
+ if (body.statements.length > 0) {
5971
+ const start = body.getStart(sourceFile);
5972
+ const end = body.getEnd();
5973
+ if (end > start) spans.push({ start, end });
5974
+ }
5642
5975
  }
5643
5976
  ts__default.default.forEachChild(node, visit);
5644
5977
  };
@@ -5650,16 +5983,21 @@ function skeletonizeTypeScript(source, fileName = "file.ts") {
5650
5983
  if (last && span.start < last.end) continue;
5651
5984
  outermost.push(span);
5652
5985
  }
5653
- let text = source;
5654
- for (let index = outermost.length - 1; index >= 0; index -= 1) {
5655
- const span = outermost[index];
5656
- text = `${text.slice(0, span.start)}{ /* ... */ }${text.slice(span.end)}`;
5986
+ const chunks = [];
5987
+ let cursor = 0;
5988
+ for (const span of outermost) {
5989
+ chunks.push(source.slice(cursor, span.start), "{ /* ... */ }");
5990
+ cursor = span.end;
5657
5991
  }
5658
- return { text, collapsed: outermost.length };
5992
+ chunks.push(source.slice(cursor));
5993
+ return { text: chunks.join(""), collapsed: outermost.length };
5659
5994
  }
5660
5995
  function isSkeletonizable(filePath) {
5661
5996
  const lower = filePath.toLowerCase();
5662
- return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts");
5997
+ if (lower.endsWith(".d.ts") || lower.endsWith(".d.mts") || lower.endsWith(".d.cts")) {
5998
+ return false;
5999
+ }
6000
+ 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");
5663
6001
  }
5664
6002
  var SARIF_VERSION = "2.1.0";
5665
6003
  var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
@@ -5784,23 +6122,55 @@ function truncateWithBudgetMarker(content, maxBytes, budget, originalLines) {
5784
6122
  };
5785
6123
  }
5786
6124
  function partitionFiles(files, maxPartBytes, contentFingerprint) {
5787
- if (files.length === 0) return [];
5788
- const budget = Math.max(MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES, maxPartBytes);
5789
- const sectionBudget = budget - HEADER_RESERVE_BYTES;
5790
- const prepared = files.map((file) => prepareSection(file, sectionBudget));
5791
- let groups = greedyGroups(prepared, budget);
6125
+ return partitionFilesForBuckets([{ files, budgetBytes: maxPartBytes }], contentFingerprint);
6126
+ }
6127
+ function partitionFilesForBuckets(buckets, contentFingerprint) {
6128
+ const groups = [];
6129
+ for (const bucket of buckets) {
6130
+ if (bucket.files.length === 0) continue;
6131
+ const budget = Math.max(
6132
+ MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES,
6133
+ bucket.budgetBytes
6134
+ );
6135
+ const sectionBudget = budget - HEADER_RESERVE_BYTES;
6136
+ const prepared = bucket.files.map((file) => prepareSection(file, sectionBudget));
6137
+ for (const sections of greedyGroups(prepared, budget)) groups.push({ sections, budget });
6138
+ }
6139
+ if (groups.length === 0) return [];
6140
+ const measurements = /* @__PURE__ */ new Map();
6141
+ const widthOf = (total) => Math.max(3, String(total).length);
6142
+ const measure = (group, index, total) => {
6143
+ const cached = measurements.get(group);
6144
+ const width = widthOf(total);
6145
+ if (cached !== void 0 && cached.index === index && cached.width === width) {
6146
+ return cached.size;
6147
+ }
6148
+ const { content } = assembleGroup(group, index, total, contentFingerprint);
6149
+ const size = Buffer.byteLength(content, "utf8");
6150
+ measurements.set(group, { index, width, size });
6151
+ return size;
6152
+ };
5792
6153
  for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
5793
- const rendered = renderGroups(groups, budget, contentFingerprint);
5794
- const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
5795
- if (oversized === -1) return rendered;
6154
+ const total = groups.length;
6155
+ let oversized = -1;
6156
+ for (let index = 0; index < total; index += 1) {
6157
+ const group2 = groups[index];
6158
+ if (measure(group2, index, total) > group2.budget) {
6159
+ oversized = index;
6160
+ break;
6161
+ }
6162
+ }
6163
+ if (oversized === -1) return renderGroups(groups, contentFingerprint);
5796
6164
  const group = groups[oversized];
5797
- if (group.length > 1) {
5798
- const moved = group.pop();
5799
- groups.splice(oversized + 1, 0, [moved]);
6165
+ if (group.sections.length > 1) {
6166
+ const moved = group.sections.pop();
6167
+ groups.splice(oversized + 1, 0, { sections: [moved], budget: group.budget });
6168
+ measurements.delete(group);
5800
6169
  continue;
5801
6170
  }
5802
- const [section] = group;
5803
- const overflow = rendered[oversized].metadata.byteSize - budget;
6171
+ const [section] = group.sections;
6172
+ if (section === void 0) return renderGroups(groups, contentFingerprint);
6173
+ const overflow = measure(group, oversized, total) - group.budget;
5804
6174
  const currentContent = section.file.content ?? "";
5805
6175
  const shrinkTo = Math.max(
5806
6176
  0,
@@ -5809,26 +6179,33 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
5809
6179
  const shrunk = truncateWithBudgetMarker(
5810
6180
  currentContent,
5811
6181
  shrinkTo,
5812
- budget,
6182
+ group.budget,
5813
6183
  section.file.originalLines
5814
6184
  );
5815
6185
  if (shrunk.isTruncated) {
5816
6186
  applyTruncatedContent(section.file, shrunk.content);
5817
6187
  section.text = renderFileSection(section.file);
5818
6188
  section.byteSize = Buffer.byteLength(section.text, "utf8");
6189
+ measurements.delete(group);
5819
6190
  continue;
5820
6191
  }
6192
+ const rendered = renderGroups(groups, contentFingerprint);
5821
6193
  const marker = TRUNCATION_MARKER;
5822
- assertBudgetHoldsMarker(budget);
6194
+ assertBudgetHoldsMarker(group.budget);
5823
6195
  const target = Math.max(
5824
6196
  0,
5825
- budget - Buffer.byteLength(marker, "utf8") - TOKEN_LINE_SLACK
6197
+ group.budget - Buffer.byteLength(marker, "utf8") - TOKEN_LINE_SLACK
5826
6198
  );
5827
6199
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
5828
- rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker);
6200
+ rendered[oversized] = rehashPart(
6201
+ rendered[oversized],
6202
+ clipped.content + marker
6203
+ );
5829
6204
  return rendered;
5830
6205
  }
5831
- return enforceBudget(renderGroups(groups, budget, contentFingerprint), budget);
6206
+ return renderGroups(groups, contentFingerprint).flatMap(
6207
+ (part) => enforceBudget([part], part.metadata.budgetBytes)
6208
+ );
5832
6209
  }
5833
6210
  var TRUNCATION_MARKER = "\n... [part truncated: cannot fit budget] ...\n";
5834
6211
  function assertBudgetHoldsMarker(budget) {
@@ -5851,69 +6228,7 @@ function enforceBudget(parts, budget) {
5851
6228
  return rehashPart(part, clipped.content + TRUNCATION_MARKER);
5852
6229
  });
5853
6230
  }
5854
- function renumberParts(parts, contentFingerprint) {
5855
- const total = parts.length;
5856
- if (total === 0) return [];
5857
- const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
5858
- const nameFor = (position) => `part-${zeroPad(position)}.md`;
5859
- const marker = "## Contents\n";
5860
- const renumbered = parts.map((part, index) => {
5861
- const markerIndex = part.markdown.indexOf(marker);
5862
- const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker.length);
5863
- const included = part.metadata.includedFiles;
5864
- if (markerIndex === -1) {
5865
- throw new Error(`${part.filename} has no "## Contents" section to re-number.`);
5866
- }
5867
- const layerCounts = {};
5868
- const kindCounts = {};
5869
- for (const file of included) {
5870
- layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
5871
- kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
5872
- }
5873
- const pathLines = included.map((file) => displayPath(file.relativePath));
5874
- const listFence = fenceForText(pathLines.join("\n"), 3);
5875
- const header = [
5876
- `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
5877
- "",
5878
- `- Previous: ${index === 0 ? "none" : nameFor(index)}`,
5879
- `- Next: ${index === total - 1 ? "none" : nameFor(index + 2)}`,
5880
- `- Files: ${included.length}`,
5881
- `- Content Fingerprint: ${contentFingerprint || "none"}`,
5882
- `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
5883
- `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
5884
- "",
5885
- UNTRUSTED_CONTENT_NOTICE,
5886
- "",
5887
- "## Files",
5888
- "",
5889
- `${listFence}text`,
5890
- ...pathLines,
5891
- listFence,
5892
- "",
5893
- "## Contents",
5894
- ""
5895
- ].join("\n");
5896
- const assembled = (header + body).replace(/\r\n/g, "\n");
5897
- const estimatedTokens = estimateTokens(assembled);
5898
- const markdown = insertTokenLine(assembled, estimatedTokens);
5899
- const metadata = {
5900
- ...part.metadata,
5901
- index: index + 1,
5902
- filename: nameFor(index + 1),
5903
- byteSize: Buffer.byteLength(markdown, "utf8"),
5904
- estimatedTokens,
5905
- sha256: crypto2__namespace.createHash("sha256").update(markdown).digest("hex"),
5906
- lines: countLines(markdown),
5907
- fileCount: included.length,
5908
- layersCovered: Object.keys(layerCounts).sort()
5909
- };
5910
- return { filename: metadata.filename, markdown, metadata };
5911
- });
5912
- return renumbered.map(
5913
- (part) => enforceBudget([part], part.metadata.budgetBytes)[0] ?? part
5914
- );
5915
- }
5916
- var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
6231
+ var SECTION_TRUNCATION_NOTICE = "... [section truncated: the fixed metadata and a code fence did not fit the part budget] ...";
5917
6232
  var METADATA_VALUE_MAX = 200;
5918
6233
  var DISPLAY_PATH_MAX = 300;
5919
6234
  function capValue(value, max) {
@@ -5955,13 +6270,57 @@ function prepareSection(file, sectionBudget) {
5955
6270
  shrinkBody();
5956
6271
  }
5957
6272
  if (byteSize > sectionBudget) {
5958
- const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
5959
- const truncated = truncateToBytes(text, Math.max(0, target));
5960
- text = truncated.content + SECTION_TRUNCATION_NOTICE;
6273
+ applyTruncatedContent(file, SECTION_TRUNCATION_NOTICE);
6274
+ file.summary = void 0;
6275
+ file.summaryOmitted = true;
6276
+ text = renderFileSection(file);
5961
6277
  byteSize = Buffer.byteLength(text, "utf8");
6278
+ if (byteSize > sectionBudget) {
6279
+ text = truncateRenderedSection(text, sectionBudget, fenceForContent(file.content ?? ""));
6280
+ byteSize = Buffer.byteLength(text, "utf8");
6281
+ }
5962
6282
  }
5963
6283
  return { file, text, byteSize };
5964
6284
  }
6285
+ function truncateRenderedSection(text, sectionBudget, fence) {
6286
+ const closing = `
6287
+ ${SECTION_TRUNCATION_NOTICE}
6288
+ ${fence}
6289
+ `;
6290
+ const bodyRoom = sectionBudget - Buffer.byteLength(closing, "utf8");
6291
+ if (bodyRoom > 0) {
6292
+ const cut2 = truncateToBytes(text, bodyRoom).content;
6293
+ const lastBreak2 = cut2.lastIndexOf("\n");
6294
+ const kept = lastBreak2 >= 0 ? cut2.slice(0, lastBreak2 + 1) : "";
6295
+ if (fenceIsOpen(kept, fence)) return `${kept}${closing}`;
6296
+ }
6297
+ const probe = text.slice(0, 512);
6298
+ const emptyFence = fenceForText(probe.slice(0, Math.max(0, probe.lastIndexOf("\n") + 1)), 4);
6299
+ const block = `
6300
+ ${emptyFence}
6301
+ ${SECTION_TRUNCATION_NOTICE}
6302
+ ${emptyFence}
6303
+ `;
6304
+ const prefixRoom = sectionBudget - Buffer.byteLength(block, "utf8");
6305
+ const cut = prefixRoom > 0 ? truncateToBytes(text, prefixRoom).content : "";
6306
+ const lastBreak = cut.lastIndexOf("\n");
6307
+ const prefix = lastBreak >= 0 ? cut.slice(0, lastBreak + 1) : "";
6308
+ return `${dropTrailingOpenFence(prefix, fence)}${block}`;
6309
+ }
6310
+ function dropTrailingOpenFence(text, fence) {
6311
+ if (!fenceIsOpen(text, fence)) return text;
6312
+ const lines = text.split("\n");
6313
+ const opening = lines.findIndex((line) => line.startsWith(fence));
6314
+ if (opening < 0) return text;
6315
+ return lines.slice(0, opening).join("\n") + (opening > 0 ? "\n" : "");
6316
+ }
6317
+ function fenceIsOpen(text, fence) {
6318
+ let open = false;
6319
+ for (const line of text.split("\n")) {
6320
+ if (line.startsWith(fence)) open = !open;
6321
+ }
6322
+ return open;
6323
+ }
5965
6324
  function applyTruncatedContent(file, content) {
5966
6325
  file.content = content;
5967
6326
  file.isPreviewTruncated = true;
@@ -6009,52 +6368,64 @@ function greedyGroups(sections, budget) {
6009
6368
  if (current.length > 0) groups.push(current);
6010
6369
  return groups;
6011
6370
  }
6012
- function renderGroups(groups, budget, contentFingerprint) {
6371
+ function assembleGroup(group, index, total, contentFingerprint) {
6372
+ const width = Math.max(3, String(total).length);
6373
+ const zeroPad = (value) => String(value).padStart(width, "0");
6374
+ const filename = `part-${zeroPad(index + 1)}.md`;
6375
+ const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
6376
+ const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
6377
+ for (const section of group.sections) {
6378
+ section.file.part = filename;
6379
+ }
6380
+ const layerCounts = {};
6381
+ const kindCounts = {};
6382
+ for (const section of group.sections) {
6383
+ layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
6384
+ kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
6385
+ }
6386
+ const pathLines = group.sections.map((s) => displayPath(s.file.relativePath));
6387
+ const listFence = fenceForText(pathLines.join("\n"), 3);
6388
+ const header = [
6389
+ `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
6390
+ "",
6391
+ `- Previous: ${prev}`,
6392
+ `- Next: ${next}`,
6393
+ `- Files: ${group.sections.length}`,
6394
+ `- Content Fingerprint: ${contentFingerprint || "none"}`,
6395
+ `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
6396
+ `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
6397
+ "",
6398
+ UNTRUSTED_CONTENT_NOTICE,
6399
+ "",
6400
+ "## Files",
6401
+ "",
6402
+ `${listFence}text`,
6403
+ ...pathLines,
6404
+ listFence,
6405
+ "",
6406
+ "## Contents",
6407
+ ""
6408
+ ].join("\n");
6409
+ const assembled = (header + group.sections.map((s) => s.text).join("")).replace(
6410
+ /\r\n|\r/g,
6411
+ "\n"
6412
+ );
6413
+ const estimatedTokens = estimateTokens(assembled);
6414
+ return { filename, content: insertTokenLine(assembled, estimatedTokens), estimatedTokens };
6415
+ }
6416
+ function renderGroups(groups, contentFingerprint) {
6013
6417
  const total = groups.length;
6014
- const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
6015
6418
  return groups.map((group, index) => {
6016
- const filename = `part-${zeroPad(index + 1)}.md`;
6017
- const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
6018
- const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
6019
- for (const section of group) {
6020
- section.file.part = filename;
6021
- }
6022
- const layersCovered = Array.from(new Set(group.map((s) => s.file.layer))).sort();
6023
- const layerCounts = {};
6024
- const kindCounts = {};
6025
- for (const section of group) {
6026
- layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
6027
- kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
6028
- }
6029
- const pathLines = group.map((s) => displayPath(s.file.relativePath));
6030
- const listFence = fenceForText(pathLines.join("\n"), 3);
6031
- const header = [
6032
- `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
6033
- "",
6034
- `- Previous: ${prev}`,
6035
- `- Next: ${next}`,
6036
- `- Files: ${group.length}`,
6037
- `- Content Fingerprint: ${contentFingerprint || "none"}`,
6038
- `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
6039
- `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
6040
- "",
6041
- UNTRUSTED_CONTENT_NOTICE,
6042
- "",
6043
- "## Files",
6044
- "",
6045
- `${listFence}text`,
6046
- ...pathLines,
6047
- listFence,
6048
- "",
6049
- "## Contents",
6050
- ""
6051
- ].join("\n");
6052
- const assembled = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
6053
- const estimatedTokens = estimateTokens(assembled);
6054
- const content = insertTokenLine(assembled, estimatedTokens);
6419
+ const { filename, content, estimatedTokens } = assembleGroup(
6420
+ group,
6421
+ index,
6422
+ total,
6423
+ contentFingerprint
6424
+ );
6055
6425
  const byteSize = Buffer.byteLength(content, "utf8");
6056
6426
  const sha256 = crypto2__namespace.createHash("sha256").update(content).digest("hex");
6057
- const includedFiles = group.map(({ file }) => ({
6427
+ const layersCovered = Array.from(new Set(group.sections.map((s) => s.file.layer))).sort();
6428
+ const includedFiles = group.sections.map(({ file }) => ({
6058
6429
  relativePath: file.relativePath,
6059
6430
  sourceBytes: file.sizeBytes,
6060
6431
  includedBytes: file.includedBytes,
@@ -6067,10 +6438,10 @@ function renderGroups(groups, budget, contentFingerprint) {
6067
6438
  filename,
6068
6439
  byteSize,
6069
6440
  estimatedTokens,
6070
- budgetBytes: budget,
6441
+ budgetBytes: group.budget,
6071
6442
  sha256,
6072
6443
  lines: countLines(content),
6073
- fileCount: group.length,
6444
+ fileCount: group.sections.length,
6074
6445
  layersCovered,
6075
6446
  includedFiles
6076
6447
  };
@@ -6078,7 +6449,7 @@ function renderGroups(groups, budget, contentFingerprint) {
6078
6449
  });
6079
6450
  }
6080
6451
  function emittedBody(content) {
6081
- const normalised = content.replace(/\r\n/g, "\n");
6452
+ const normalised = content.replace(/\r\n|\r/g, "\n");
6082
6453
  return normalised.endsWith("\n") ? normalised.slice(0, -1) : normalised;
6083
6454
  }
6084
6455
  function compactMetadata(file) {
@@ -6146,7 +6517,7 @@ function generateMonolithJson(manifest, filesWithContent) {
6146
6517
  }
6147
6518
  function generateMonolithMarkdown(manifest, filesWithContent) {
6148
6519
  const sections = filesWithContent.map((file) => {
6149
- const content = (file.content ?? "").replace(/\r\n/g, "\n");
6520
+ const content = (file.content ?? "").replace(/\r\n|\r/g, "\n");
6150
6521
  const fence = fenceForContent(content);
6151
6522
  return [
6152
6523
  `## File: ${escapeControls(file.relativePath)}`,
@@ -6365,9 +6736,13 @@ function describeManifestShapeProblem(value) {
6365
6736
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
6366
6737
  return 'a "parts" entry is not an object';
6367
6738
  }
6368
- if (typeof entry.filename !== "string") {
6739
+ const filename = entry.filename;
6740
+ if (typeof filename !== "string" || filename.trim() === "") {
6369
6741
  return 'a "parts" entry has no "filename"';
6370
6742
  }
6743
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("\0") || filename === "." || filename === ".." || path10__namespace.isAbsolute(filename)) {
6744
+ return 'a "parts" entry "filename" must be a bare filename inside parts/';
6745
+ }
6371
6746
  }
6372
6747
  if (!candidate.layers || typeof candidate.layers !== "object" || Array.isArray(candidate.layers)) {
6373
6748
  return 'missing "layers" object';
@@ -6478,7 +6853,7 @@ function validateSnapshot(configInput) {
6478
6853
  });
6479
6854
  for (const diskFile of collection.files) {
6480
6855
  const snapshotHash = manifestFileMap.get(diskFile);
6481
- if (!snapshotHash) {
6856
+ if (snapshotHash === void 0) {
6482
6857
  const skipReason = skippedReasonOnDisk(diskFile);
6483
6858
  if (skipReason === "unchanged") {
6484
6859
  if (unchangedScope !== null) {
@@ -6492,6 +6867,11 @@ function validateSnapshot(configInput) {
6492
6867
  drift.missingInSnapshot.push(diskFile);
6493
6868
  continue;
6494
6869
  }
6870
+ if (unchangedScope !== null && !unchangedScope.has(diskFile) && !becameDependent(diskFile)) {
6871
+ drift.configMismatches.push(
6872
+ `scope: ${maskedPath(diskFile, config.secretRules)} is in the snapshot but is no longer in the changed-files scope`
6873
+ );
6874
+ }
6495
6875
  let diskHash;
6496
6876
  try {
6497
6877
  diskHash = sha256Hex(fs8__namespace.readFileSync(path10__namespace.join(config.cwd, diskFile)));
@@ -6632,6 +7012,41 @@ function validateSnapshot(configInput) {
6632
7012
  `includeSensitiveFiles: manifest ${recorded.includeSensitiveFiles} vs current ${config.includeSensitiveFiles}`
6633
7013
  );
6634
7014
  }
7015
+ if ((recorded.maxPartTokens ?? void 0) !== (config.maxPartTokens ?? void 0)) {
7016
+ drift.configMismatches.push(
7017
+ `maxPartTokens: manifest ${String(recorded.maxPartTokens ?? "unset")} vs current ${String(config.maxPartTokens ?? "unset")}`
7018
+ );
7019
+ }
7020
+ if ((recorded.maxMonolithBytes ?? null) !== (config.maxMonolithBytes ?? null)) {
7021
+ drift.configMismatches.push(
7022
+ `maxMonolithBytes: manifest ${String(recorded.maxMonolithBytes ?? null)} vs current ${String(config.maxMonolithBytes ?? null)}`
7023
+ );
7024
+ }
7025
+ if (Boolean(recorded.skeleton) !== Boolean(config.skeleton)) {
7026
+ drift.configMismatches.push(
7027
+ `skeleton: manifest ${Boolean(recorded.skeleton)} vs current ${Boolean(config.skeleton)}`
7028
+ );
7029
+ }
7030
+ if (Boolean(recorded.sarif) !== Boolean(config.sarif)) {
7031
+ drift.configMismatches.push(
7032
+ `sarif: manifest ${Boolean(recorded.sarif)} vs current ${Boolean(config.sarif)}`
7033
+ );
7034
+ }
7035
+ if ((recorded.changedSince ?? void 0) !== (config.changedSince ?? void 0)) {
7036
+ drift.configMismatches.push(
7037
+ `changedSince: manifest ${String(recorded.changedSince ?? "unset")} vs current ${String(config.changedSince ?? "unset")}`
7038
+ );
7039
+ }
7040
+ if (Boolean(recorded.staged) !== Boolean(config.staged)) {
7041
+ drift.configMismatches.push(
7042
+ `staged: manifest ${Boolean(recorded.staged)} vs current ${Boolean(config.staged)}`
7043
+ );
7044
+ }
7045
+ if (Boolean(recorded.withDependents) !== Boolean(config.withDependents)) {
7046
+ drift.configMismatches.push(
7047
+ `withDependents: manifest ${Boolean(recorded.withDependents)} vs current ${Boolean(config.withDependents)}`
7048
+ );
7049
+ }
6635
7050
  const effectiveDiscovery = config.discovery ?? "auto";
6636
7051
  const recordedDiscovery = recorded.discovery;
6637
7052
  if (recordedDiscovery !== void 0 && recordedDiscovery !== effectiveDiscovery) {
@@ -6852,7 +7267,7 @@ function validateSnapshot(configInput) {
6852
7267
 
6853
7268
  // src/version.ts
6854
7269
  var GENERATOR_NAME = "codebase-snapshot";
6855
- var GENERATOR_VERSION = "0.9.12";
7270
+ var GENERATOR_VERSION = "0.9.14";
6856
7271
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
6857
7272
  function isOurGeneratorName(name) {
6858
7273
  if (typeof name !== "string") return false;
@@ -6873,7 +7288,10 @@ const { parentPort, workerData } = require("node:worker_threads");
6873
7288
  parentPort.postMessage({ ok: false, message: loaded.errors.join("; ") });
6874
7289
  return;
6875
7290
  }
6876
- const config = mod.resolveConfig(loaded.config, workerData.cliOptions, workerData.cwd);
7291
+ // \`hooks: {}\` is a value, not an absence: the config is applied with the hook list emptied,
7292
+ // so the run is the documented MCP run rather than the CLI run with one setting removed.
7293
+ const base = loaded.config ? { ...loaded.config, hooks: {} } : null;
7294
+ const config = mod.resolveConfig(base, workerData.cliOptions, workerData.cwd);
6877
7295
  const manifest = mod.generateSnapshot(config);
6878
7296
  parentPort.postMessage({
6879
7297
  ok: true,
@@ -6896,14 +7314,15 @@ const { parentPort, workerData } = require("node:worker_threads");
6896
7314
  })();
6897
7315
  `;
6898
7316
  function generateInWorker(cwd, cliOptions) {
6899
- return new Promise((resolve8) => {
7317
+ return new Promise((resolve9) => {
6900
7318
  let settled = false;
7319
+ let worker = null;
6901
7320
  const finish = (result) => {
6902
7321
  if (settled) return;
6903
7322
  settled = true;
6904
- resolve8(result);
7323
+ if (worker !== null) void worker.terminate();
7324
+ resolve9(result);
6905
7325
  };
6906
- let worker;
6907
7326
  const isCjs = typeof __dirname !== "undefined";
6908
7327
  const directory = isCjs ? __dirname : path10__namespace.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('mcp.cjs', document.baseURI).href))));
6909
7328
  try {
@@ -6936,7 +7355,100 @@ function generateInWorker(cwd, cliOptions) {
6936
7355
 
6937
7356
  // src/index.ts
6938
7357
  var SCHEMA_VERSION = "3.0.0";
6939
- var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
7358
+ var SUMMARY_MAX_SOURCE_BYTES2 = SUMMARY_MAX_SOURCE_BYTES;
7359
+ var FILE_DERIVATIONS = /* @__PURE__ */ new Map();
7360
+ var FILE_DERIVATION_LIMIT_BYTES = 64 * 1024 * 1024;
7361
+ var fileDerivationBytes = 0;
7362
+ function takeDerivation(path14, hash, settings) {
7363
+ const cached = FILE_DERIVATIONS.get(path14);
7364
+ if (cached === void 0 || cached.hash !== hash || cached.settings !== settings) return void 0;
7365
+ return cached;
7366
+ }
7367
+ function rememberDerivation(path14, hash, settings, derivation) {
7368
+ const bodyBytes = derivation.finalContent === derivation.redacted ? derivation.redacted.length : derivation.redacted.length + derivation.finalContent.length;
7369
+ const bytes = bodyBytes * 2 + 512;
7370
+ if (bytes > FILE_DERIVATION_LIMIT_BYTES / 4) return;
7371
+ while (fileDerivationBytes + bytes > FILE_DERIVATION_LIMIT_BYTES) {
7372
+ const oldest = FILE_DERIVATIONS.keys().next();
7373
+ if (oldest.done === true) break;
7374
+ const evicted = FILE_DERIVATIONS.get(oldest.value);
7375
+ FILE_DERIVATIONS.delete(oldest.value);
7376
+ if (evicted !== void 0) fileDerivationBytes -= evicted.bytes;
7377
+ }
7378
+ FILE_DERIVATIONS.set(path14, { ...derivation, hash, settings, bytes });
7379
+ fileDerivationBytes += bytes;
7380
+ }
7381
+ function derivationSettings(config) {
7382
+ const rules = config.secretRules.map(
7383
+ (rule) => `${rule.rule}\0${rule.pattern.source}\0${rule.pattern.flags}\0${rule.valueGroup ?? ""}`
7384
+ ).join("");
7385
+ return `${GENERATOR_VERSION}${configDigest(config)}${sha256Hex2(rules)}`;
7386
+ }
7387
+ function deriveFile(relativePath, rawContent, sizeBytes, config) {
7388
+ const lineCount = countLines(rawContent);
7389
+ const { redacted, findings } = scanAndRedactSecrets(rawContent, config.secretRules, relativePath);
7390
+ const isLarge = sizeBytes > config.largeFileBytes;
7391
+ let finalContent = redacted;
7392
+ let isPreviewTruncated = false;
7393
+ let includedLines = lineCount;
7394
+ let inclusionMode = "full";
7395
+ if (config.skeleton === true && isSkeletonizable(relativePath)) {
7396
+ const skeleton = skeletonizeTypeScript(redacted, relativePath);
7397
+ if (skeleton.collapsed > 0) {
7398
+ finalContent = skeleton.text;
7399
+ inclusionMode = "signatures";
7400
+ includedLines = countLines(skeleton.text);
7401
+ }
7402
+ }
7403
+ if (isLarge && inclusionMode !== "signatures") {
7404
+ const preview = truncatePreview(redacted, config.previewHeadLines, config.previewTailLines);
7405
+ finalContent = preview.content;
7406
+ isPreviewTruncated = preview.isTruncated;
7407
+ includedLines = preview.includedLines;
7408
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
7409
+ }
7410
+ let summary;
7411
+ let summaryQuality;
7412
+ let summarySkip = null;
7413
+ let summaryWarning = null;
7414
+ let dependentImports = null;
7415
+ if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES2) {
7416
+ summarySkip = "oversize";
7417
+ dependentImports = [];
7418
+ } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
7419
+ summarySkip = "preview";
7420
+ dependentImports = importsOfText(relativePath, rawContent);
7421
+ } else {
7422
+ try {
7423
+ const outcome = summarizeFile(relativePath, rawContent);
7424
+ summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
7425
+ summaryQuality = outcome?.quality;
7426
+ if (isSourceSummary(summary) && summary.parseErrors) {
7427
+ summaryWarning = `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`;
7428
+ }
7429
+ } catch (err) {
7430
+ summaryWarning = `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
7431
+ /\s+/g,
7432
+ " "
7433
+ );
7434
+ }
7435
+ }
7436
+ return {
7437
+ redacted,
7438
+ findings,
7439
+ lineCount,
7440
+ finalContent,
7441
+ inclusionMode,
7442
+ isPreviewTruncated,
7443
+ includedLines,
7444
+ isLarge,
7445
+ summary,
7446
+ summaryQuality,
7447
+ summarySkip,
7448
+ summaryWarning,
7449
+ dependentImports
7450
+ };
7451
+ }
6940
7452
  function sha256Hex2(input) {
6941
7453
  return crypto2__namespace.createHash("sha256").update(input).digest("hex");
6942
7454
  }
@@ -7393,23 +7905,14 @@ function partitionByBudget(files, config, fingerprint) {
7393
7905
  if (bucket) bucket.push(file);
7394
7906
  else buckets.set(budget, [file]);
7395
7907
  }
7396
- if (buckets.size <= 1) {
7397
- const only = buckets.size === 1 ? Array.from(buckets.keys())[0] : config.maxPartBytes;
7398
- return partitionFiles(files, only, fingerprint);
7399
- }
7400
- const parts = [];
7401
- for (const budget of Array.from(buckets.keys()).sort((a, b) => a - b)) {
7402
- parts.push(...partitionFiles(buckets.get(budget) ?? [], budget, fingerprint));
7403
- }
7404
- const merged = renumberParts(parts, fingerprint);
7405
- const byPath = new Map(files.map((file) => [file.relativePath, file]));
7406
- for (const part of merged) {
7407
- for (const included of part.metadata.includedFiles) {
7408
- const file = byPath.get(included.relativePath);
7409
- if (file) file.part = part.filename;
7410
- }
7908
+ if (buckets.size === 0) return [];
7909
+ if (buckets.size === 1) {
7910
+ return partitionFiles(files, Array.from(buckets.keys())[0], fingerprint);
7411
7911
  }
7412
- return merged;
7912
+ return partitionFilesForBuckets(
7913
+ Array.from(buckets.entries()).sort(([left], [right]) => left - right).map(([budgetBytes, bucketFiles]) => ({ files: bucketFiles, budgetBytes })),
7914
+ fingerprint
7915
+ );
7413
7916
  }
7414
7917
  function generateSnapshot(configInput, options) {
7415
7918
  assertUsableConfigInput(configInput);
@@ -7440,6 +7943,7 @@ function generateSnapshot(configInput, options) {
7440
7943
  return false;
7441
7944
  }) : collection.files;
7442
7945
  const files = [];
7946
+ const derivationKey = derivationSettings(config);
7443
7947
  const monolithBodies = /* @__PURE__ */ new Map();
7444
7948
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
7445
7949
  const secretFindings = [];
@@ -7452,6 +7956,7 @@ function generateSnapshot(configInput, options) {
7452
7956
  const fileWarnings = [];
7453
7957
  const oversizeSummaryPaths = [];
7454
7958
  const previewSummaryPaths = [];
7959
+ const skippedSummaryImports = /* @__PURE__ */ new Map();
7455
7960
  const layerCounts = {};
7456
7961
  const kindCounts = {};
7457
7962
  let totalLines = 0;
@@ -7516,8 +8021,6 @@ function generateSnapshot(configInput, options) {
7516
8021
  }
7517
8022
  const hashSha256 = sha256Hex2(buffer);
7518
8023
  const rawContent = decoded.text;
7519
- const lineCount = countLines(rawContent);
7520
- totalLines += lineCount;
7521
8024
  totalSizeBytes += stats.size;
7522
8025
  if (decoded.encoding !== "utf-8") {
7523
8026
  encodingNotes.push({
@@ -7531,66 +8034,25 @@ function generateSnapshot(configInput, options) {
7531
8034
  const language = detectLanguage(relativePath);
7532
8035
  bump(layerCounts, layer2);
7533
8036
  bump(kindCounts, kind);
7534
- const { redacted, findings } = scanAndRedactSecrets(
7535
- rawContent,
7536
- config.secretRules,
7537
- relativePath
7538
- );
7539
- secretFindings.push(...findings);
7540
- const isLarge = stats.size > config.largeFileBytes;
7541
- if (isLarge) largeFileCount += 1;
7542
- let finalContent = redacted;
7543
- let isPreviewTruncated = false;
7544
- let includedLines = lineCount;
7545
- let inclusionMode = "full";
7546
- if (config.skeleton === true && isSkeletonizable(relativePath)) {
7547
- const skeleton = skeletonizeTypeScript(redacted, relativePath);
7548
- if (skeleton.collapsed > 0) {
7549
- finalContent = skeleton.text;
7550
- inclusionMode = "signatures";
7551
- includedLines = countLines(skeleton.text);
7552
- }
7553
- }
7554
- if (isLarge && inclusionMode !== "signatures") {
7555
- const preview = truncatePreview(
7556
- redacted,
7557
- config.previewHeadLines,
7558
- config.previewTailLines
7559
- );
7560
- finalContent = preview.content;
7561
- isPreviewTruncated = preview.isTruncated;
7562
- includedLines = preview.includedLines;
7563
- inclusionMode = isPreviewTruncated ? "preview" : "full";
7564
- }
7565
- if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
7566
- monolithBodies.set(relativePath, redacted);
7567
- }
7568
- let summary;
7569
- let summaryQuality;
7570
- if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES) {
7571
- oversizeSummaryPaths.push(relativePath);
7572
- } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
7573
- previewSummaryPaths.push(relativePath);
7574
- } else {
7575
- try {
7576
- const outcome = summarizeFile(relativePath, rawContent);
7577
- summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
7578
- summaryQuality = outcome?.quality;
7579
- if (isSourceSummary(summary) && summary.parseErrors) {
7580
- fileWarnings.push({
7581
- path: relativePath,
7582
- message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
7583
- });
7584
- }
7585
- } catch (err) {
7586
- fileWarnings.push({
7587
- path: relativePath,
7588
- message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
7589
- /\s+/g,
7590
- " "
7591
- )
7592
- });
7593
- }
8037
+ const derivation = takeDerivation(absolutePath, hashSha256, derivationKey) ?? (() => {
8038
+ const fresh = deriveFile(relativePath, rawContent, stats.size, config);
8039
+ rememberDerivation(absolutePath, hashSha256, derivationKey, fresh);
8040
+ return fresh;
8041
+ })();
8042
+ const { lineCount } = derivation;
8043
+ totalLines += lineCount;
8044
+ secretFindings.push(...derivation.findings);
8045
+ if (derivation.isLarge) largeFileCount += 1;
8046
+ if (derivation.summarySkip === "oversize") oversizeSummaryPaths.push(relativePath);
8047
+ else if (derivation.summarySkip === "preview") previewSummaryPaths.push(relativePath);
8048
+ if (derivation.dependentImports !== null) {
8049
+ skippedSummaryImports.set(relativePath, derivation.dependentImports);
8050
+ }
8051
+ if (derivation.summaryWarning !== null) {
8052
+ fileWarnings.push({ path: relativePath, message: derivation.summaryWarning });
8053
+ }
8054
+ if (config.includeMonolith && (derivation.isPreviewTruncated || derivation.inclusionMode === "signatures")) {
8055
+ monolithBodies.set(relativePath, derivation.redacted);
7594
8056
  }
7595
8057
  files.push({
7596
8058
  relativePath,
@@ -7601,24 +8063,25 @@ function generateSnapshot(configInput, options) {
7601
8063
  layer: layer2,
7602
8064
  kind,
7603
8065
  language,
7604
- inclusionMode,
7605
- isLarge,
7606
- isPreviewTruncated,
8066
+ inclusionMode: derivation.inclusionMode,
8067
+ isLarge: derivation.isLarge,
8068
+ isPreviewTruncated: derivation.isPreviewTruncated,
7607
8069
  originalLines: lineCount,
7608
- includedLines,
7609
- includedBytes: Buffer.byteLength(finalContent, "utf8"),
7610
- summary,
7611
- summaryQuality,
8070
+ includedLines: derivation.includedLines,
8071
+ includedBytes: Buffer.byteLength(derivation.finalContent, "utf8"),
8072
+ summary: derivation.summary,
8073
+ summaryQuality: derivation.summaryQuality,
7612
8074
  ...decoded.encoding === "utf-8" ? {} : { encoding: decoded.encoding },
7613
- content: finalContent
8075
+ content: derivation.finalContent
7614
8076
  });
7615
8077
  }
7616
8078
  if (changedScope !== null && changedPaths !== null && config.withDependents === true) {
7617
8079
  const keep = new Set(changedPaths);
7618
8080
  const resolver = buildImportResolver(files);
7619
8081
  for (const file of files) {
7620
- if (!isSourceSummary(file.summary)) continue;
7621
- if (importsChangedFile(file.relativePath, file.summary.imports, changedPaths, resolver)) {
8082
+ const fileImports = isSourceSummary(file.summary) ? file.summary.imports : skippedSummaryImports.get(file.relativePath) ?? [];
8083
+ if (fileImports.length === 0) continue;
8084
+ if (importsChangedFile(file.relativePath, fileImports, changedPaths, resolver)) {
7622
8085
  keep.add(file.relativePath);
7623
8086
  }
7624
8087
  }
@@ -7649,6 +8112,9 @@ function generateSnapshot(configInput, options) {
7649
8112
  for (const path14 of [...monolithBodies.keys()]) {
7650
8113
  if (!keep.has(path14)) monolithBodies.delete(path14);
7651
8114
  }
8115
+ for (const path14 of [...skippedSummaryImports.keys()]) {
8116
+ if (!keep.has(path14)) skippedSummaryImports.delete(path14);
8117
+ }
7652
8118
  }
7653
8119
  if (changedScope !== null) {
7654
8120
  for (const relativePath of unchangedPaths) {
@@ -7684,7 +8150,7 @@ function generateSnapshot(configInput, options) {
7684
8150
  const shown = oversizeSummaryPaths.slice(0, ENCODING_PATHS_SHOWN).map((entry) => maskPathText(entry, config.secretRules));
7685
8151
  const hidden = oversizeSummaryPaths.length - ENCODING_PATHS_SHOWN;
7686
8152
  warnings.push(
7687
- `${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.`
8153
+ `${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.`
7688
8154
  );
7689
8155
  }
7690
8156
  for (const [encoding, bucket] of encodingBuckets) {
@@ -7708,11 +8174,13 @@ function generateSnapshot(configInput, options) {
7708
8174
  ).join("\n")
7709
8175
  );
7710
8176
  const fullContents = /* @__PURE__ */ new Map();
7711
- for (const file of files) {
7712
- fullContents.set(
7713
- file.relativePath,
7714
- monolithBodies.get(file.relativePath) ?? file.content ?? ""
7715
- );
8177
+ if (config.includeMonolith) {
8178
+ for (const file of files) {
8179
+ fullContents.set(
8180
+ file.relativePath,
8181
+ monolithBodies.get(file.relativePath) ?? file.content ?? ""
8182
+ );
8183
+ }
7716
8184
  }
7717
8185
  const parts = partitionByBudget(files, config, fingerprint);
7718
8186
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
@@ -8051,12 +8519,35 @@ async function openWorkspace(root, overrides = {}) {
8051
8519
  }
8052
8520
  cliOptions.maxPartBytes = parsed;
8053
8521
  }
8522
+ if (overrides.maxPartTokens !== void 0) {
8523
+ const parsed = parseByteSize(overrides.maxPartTokens);
8524
+ if (parsed === void 0) {
8525
+ return {
8526
+ ok: false,
8527
+ message: '"maxPartTokens" must be a token count, for example 32000 or "32k".'
8528
+ };
8529
+ }
8530
+ cliOptions.maxPartTokens = parsed;
8531
+ }
8054
8532
  if (overrides.outDir !== void 0) {
8055
8533
  if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
8056
8534
  return { ok: false, message: '"outDir" must be a non-empty string.' };
8057
8535
  }
8058
8536
  cliOptions.outDir = overrides.outDir.trim();
8059
8537
  }
8538
+ if (overrides.changedSince !== void 0) {
8539
+ if (typeof overrides.changedSince !== "string" || overrides.changedSince.trim().length === 0) {
8540
+ return {
8541
+ ok: false,
8542
+ message: '"changedSince" must be a git ref such as "main" or "HEAD~3".'
8543
+ };
8544
+ }
8545
+ cliOptions.changedSince = overrides.changedSince.trim();
8546
+ }
8547
+ if (overrides.skeleton === true) cliOptions.skeleton = true;
8548
+ if (overrides.sarif === true) cliOptions.sarif = true;
8549
+ if (overrides.staged === true) cliOptions.staged = true;
8550
+ if (overrides.withDependents === true) cliOptions.withDependents = true;
8060
8551
  try {
8061
8552
  const config = resolveConfig(loaded.config, cliOptions, root);
8062
8553
  return { ok: true, workspace: { root, config, configFile: loaded.file, cliOptions } };
@@ -8068,6 +8559,16 @@ var manifestCache = /* @__PURE__ */ new Map();
8068
8559
  function manifestPathFor(workspace) {
8069
8560
  return path10__namespace.resolve(workspace.root, workspace.config.outDir, "manifest.json");
8070
8561
  }
8562
+ function partPathFor(workspace, filename) {
8563
+ if (filename.length === 0 || filename.includes("\0")) return null;
8564
+ const partsDir = path10__namespace.resolve(workspace.root, workspace.config.outDir, "parts");
8565
+ const resolved = path10__namespace.resolve(partsDir, filename);
8566
+ const relative8 = path10__namespace.relative(partsDir, resolved);
8567
+ if (relative8.length === 0 || relative8.includes(path10__namespace.sep) || relative8.includes("/") || relative8.includes("\\") || path10__namespace.isAbsolute(relative8)) {
8568
+ return null;
8569
+ }
8570
+ return resolved;
8571
+ }
8071
8572
  function readManifest3(workspace) {
8072
8573
  const file = manifestPathFor(workspace);
8073
8574
  const shown = path10__namespace.relative(workspace.root, file).split(path10__namespace.sep).join("/") || file;
@@ -8138,6 +8639,44 @@ var OUT_DIR_PROPERTY = {
8138
8639
  description: "Snapshot directory, relative to the root. Defaults to the project configuration's outDir."
8139
8640
  };
8140
8641
  var outDirOverride = (args) => ({ outDir: args.outDir });
8642
+ var SCOPE_PROPERTIES = {
8643
+ maxPartTokens: {
8644
+ type: "string",
8645
+ description: 'Token ceiling per part, for example 32000 or "32k". Narrower than maxPartBytes; it converts at four bytes per token.'
8646
+ },
8647
+ skeleton: {
8648
+ type: "boolean",
8649
+ description: "Replace every function body with `{ /* ... */ }`, keeping signatures, imports and exports. Applies to TypeScript, JavaScript and JSX."
8650
+ },
8651
+ sarif: {
8652
+ type: "boolean",
8653
+ description: "Also write reports/audit.sarif beside reports/audit.md, for CI code scanning."
8654
+ },
8655
+ changedSince: {
8656
+ type: "string",
8657
+ description: 'Only files changed since this git ref, for example "main" or "HEAD~3".'
8658
+ },
8659
+ staged: {
8660
+ type: "boolean",
8661
+ description: "Only files staged in the git index."
8662
+ },
8663
+ withDependents: {
8664
+ type: "boolean",
8665
+ description: "With staged or changedSince, also include the files that import the changed ones (one hop)."
8666
+ }
8667
+ };
8668
+ function workspaceOverrides(args) {
8669
+ return {
8670
+ outDir: args.outDir,
8671
+ maxPartBytes: args.maxPartBytes,
8672
+ maxPartTokens: args.maxPartTokens,
8673
+ skeleton: args.skeleton,
8674
+ sarif: args.sarif,
8675
+ changedSince: args.changedSince,
8676
+ staged: args.staged,
8677
+ withDependents: args.withDependents
8678
+ };
8679
+ }
8141
8680
  function failure(message) {
8142
8681
  return { text: message, isError: true };
8143
8682
  }
@@ -8168,12 +8707,13 @@ function summarizeDrift(drift) {
8168
8707
  }
8169
8708
  function toRelativePath(root, value) {
8170
8709
  const cleaned = value.trim();
8171
- if (cleaned.length === 0) return null;
8172
- if (!path10__namespace.isAbsolute(cleaned)) {
8173
- return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
8710
+ if (cleaned.length === 0 || cleaned.includes("\0")) return null;
8711
+ const resolvedRoot = path10__namespace.resolve(root);
8712
+ const absolute = path10__namespace.resolve(resolvedRoot, cleaned);
8713
+ const relative8 = path10__namespace.relative(resolvedRoot, absolute);
8714
+ if (relative8 === "" || relative8 === ".." || relative8.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative8)) {
8715
+ return null;
8174
8716
  }
8175
- const relative8 = path10__namespace.relative(root, cleaned);
8176
- if (relative8.startsWith("..") || path10__namespace.isAbsolute(relative8)) return null;
8177
8717
  return relative8.split(path10__namespace.sep).join("/");
8178
8718
  }
8179
8719
  function symbolsOf(entry) {
@@ -8309,7 +8849,8 @@ var planTool = {
8309
8849
  maxPartBytes: {
8310
8850
  type: "string",
8311
8851
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8312
- }
8852
+ },
8853
+ ...SCOPE_PROPERTIES
8313
8854
  },
8314
8855
  additionalProperties: false
8315
8856
  },
@@ -8326,7 +8867,7 @@ var planTool = {
8326
8867
  },
8327
8868
  annotations: READ_ONLY,
8328
8869
  async handler(args, context) {
8329
- const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes, outDir: args.outDir });
8870
+ const prepared = await prepare(args, context, workspaceOverrides(args));
8330
8871
  if (!prepared.ok) return prepared.outcome;
8331
8872
  const workspace = prepared.workspace;
8332
8873
  let manifest;
@@ -8384,7 +8925,8 @@ var generateTool = {
8384
8925
  maxPartBytes: {
8385
8926
  type: "string",
8386
8927
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
8387
- }
8928
+ },
8929
+ ...SCOPE_PROPERTIES
8388
8930
  },
8389
8931
  additionalProperties: false
8390
8932
  },
@@ -8400,10 +8942,7 @@ var generateTool = {
8400
8942
  },
8401
8943
  annotations: WRITES,
8402
8944
  async handler(args, context) {
8403
- const prepared = await prepare(args, context, {
8404
- maxPartBytes: args.maxPartBytes,
8405
- outDir: args.outDir
8406
- });
8945
+ const prepared = await prepare(args, context, workspaceOverrides(args));
8407
8946
  if (!prepared.ok) return prepared.outcome;
8408
8947
  const workspace = prepared.workspace;
8409
8948
  const result = await generateInWorker(workspace.root, workspace.cliOptions);
@@ -8442,7 +8981,7 @@ var validateTool = {
8442
8981
  description: "Re-read the repository and report whether the snapshot on disk still matches it. Use it before trusting context that came from an earlier session, or after changing the config.",
8443
8982
  inputSchema: {
8444
8983
  type: "object",
8445
- properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY },
8984
+ properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY, ...SCOPE_PROPERTIES },
8446
8985
  additionalProperties: false
8447
8986
  },
8448
8987
  outputSchema: {
@@ -8454,7 +8993,7 @@ var validateTool = {
8454
8993
  },
8455
8994
  annotations: READ_ONLY,
8456
8995
  async handler(args, context) {
8457
- const prepared = await prepare(args, context, outDirOverride(args));
8996
+ const prepared = await prepare(args, context, workspaceOverrides(args));
8458
8997
  if (!prepared.ok) return prepared.outcome;
8459
8998
  const workspace = prepared.workspace;
8460
8999
  let result;
@@ -8799,7 +9338,12 @@ var readTool = {
8799
9338
  `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
8800
9339
  );
8801
9340
  }
8802
- const partPath = path10__namespace.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
9341
+ const partPath = partPathFor(workspace, entry.part);
9342
+ if (partPath === null) {
9343
+ return failure(
9344
+ `${entry.part} is not a part of this snapshot: its name leaves ${workspace.config.outDir}/parts. Run snapshot_generate to rebuild it.`
9345
+ );
9346
+ }
8803
9347
  let partText;
8804
9348
  try {
8805
9349
  partText = fs8__namespace.readFileSync(partPath, "utf8");
@@ -8879,7 +9423,8 @@ var explainTool = {
8879
9423
  description: "Path relative to the repository root. Directories are explained as a whole."
8880
9424
  },
8881
9425
  root: ROOT_PROPERTY,
8882
- outDir: OUT_DIR_PROPERTY
9426
+ outDir: OUT_DIR_PROPERTY,
9427
+ ...SCOPE_PROPERTIES
8883
9428
  },
8884
9429
  required: ["path"],
8885
9430
  additionalProperties: false
@@ -8894,7 +9439,7 @@ var explainTool = {
8894
9439
  },
8895
9440
  annotations: READ_ONLY,
8896
9441
  async handler(args, context) {
8897
- const prepared = await prepare(args, context, outDirOverride(args));
9442
+ const prepared = await prepare(args, context, workspaceOverrides(args));
8898
9443
  if (!prepared.ok) return prepared.outcome;
8899
9444
  const workspace = prepared.workspace;
8900
9445
  if (typeof args.path !== "string") return failure('"path" must be a string.');
@@ -9607,7 +10152,7 @@ function buildAgentContract() {
9607
10152
  { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
9608
10153
  { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
9609
10154
  { key: "maxPartTokens", type: "number | string | null", default: null, summary: "Per-part token ceiling; narrows maxPartBytes. Minimum 1024." },
9610
- { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TypeScript files." },
10155
+ { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TS, JS, TSX and JSX files." },
9611
10156
  { key: "sarif", type: "boolean", default: false, summary: "Also write reports/audit.sarif." },
9612
10157
  { key: "changedSince", type: "string | null", default: null, summary: "Snapshot only files that differ from this git ref." },
9613
10158
  { key: "staged", type: "boolean", default: false, summary: "Snapshot only files staged in the index." },
@@ -9998,9 +10543,9 @@ function buildResourceProvider() {
9998
10543
  const index = Number(partMatch[1]);
9999
10544
  const part = read.manifest.parts.find((candidate) => candidate.index === index);
10000
10545
  if (!part) return null;
10001
- const text = readFileIfPresent(
10002
- path10__namespace.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
10003
- );
10546
+ const partPath = partPathFor(workspace, part.filename);
10547
+ if (partPath === null) return null;
10548
+ const text = readFileIfPresent(partPath);
10004
10549
  if (text === null) return null;
10005
10550
  return { uri, mimeType: "text/markdown", text: capUtf8(text, MAX_RESOURCE_BYTES).text };
10006
10551
  }
@@ -10032,11 +10577,11 @@ var MAX_LINE_BYTES = 32 * 1024 * 1024;
10032
10577
  function createWriter(output) {
10033
10578
  return {
10034
10579
  write(value) {
10035
- return new Promise((resolve8, reject) => {
10580
+ return new Promise((resolve9, reject) => {
10036
10581
  output.write(`${JSON.stringify(value)}
10037
10582
  `, (error) => {
10038
10583
  if (error) reject(error);
10039
- else resolve8();
10584
+ else resolve9();
10040
10585
  });
10041
10586
  });
10042
10587
  }
@@ -10044,6 +10589,7 @@ function createWriter(output) {
10044
10589
  }
10045
10590
  function createLineReader(input, options) {
10046
10591
  let buffer = "";
10592
+ let bufferBytes = 0;
10047
10593
  let discarding = false;
10048
10594
  let discardedBytes = 0;
10049
10595
  const deliver = (line) => {
@@ -10056,6 +10602,13 @@ function createLineReader(input, options) {
10056
10602
  if (trimmed.trim().length === 0) return;
10057
10603
  options.onLine(trimmed);
10058
10604
  };
10605
+ const discardBuffered = () => {
10606
+ discarding = true;
10607
+ discardedBytes += bufferBytes;
10608
+ buffer = "";
10609
+ bufferBytes = 0;
10610
+ options.onOverflow?.(discardedBytes);
10611
+ };
10059
10612
  const drain = () => {
10060
10613
  let index = buffer.indexOf("\n");
10061
10614
  while (index >= 0) {
@@ -10063,42 +10616,57 @@ function createLineReader(input, options) {
10063
10616
  buffer = buffer.slice(index + 1);
10064
10617
  if (discarding) {
10065
10618
  deliver(line);
10066
- } else if (line.length > MAX_LINE_BYTES) {
10067
- options.onOverflow?.(line.length);
10068
10619
  } else {
10069
- deliver(line);
10620
+ const lineBytes = Buffer.byteLength(line, "utf8");
10621
+ if (lineBytes > MAX_LINE_BYTES) options.onOverflow?.(lineBytes);
10622
+ else deliver(line);
10070
10623
  }
10071
10624
  index = buffer.indexOf("\n");
10072
10625
  }
10626
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
10627
+ if (!discarding && bufferBytes > MAX_LINE_BYTES) discardBuffered();
10073
10628
  };
10074
10629
  const onData = (chunk) => {
10630
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
10075
10631
  if (discarding) {
10076
10632
  const index = chunk.indexOf("\n");
10077
10633
  if (index < 0) {
10078
- discardedBytes += chunk.length;
10634
+ discardedBytes += chunkBytes;
10079
10635
  return;
10080
10636
  }
10081
10637
  buffer = chunk.slice(index + 1);
10638
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
10082
10639
  deliver("");
10083
10640
  drain();
10084
10641
  return;
10085
10642
  }
10086
- buffer += chunk;
10087
- drain();
10088
- if (buffer.length > MAX_LINE_BYTES) {
10643
+ if (chunk.indexOf("\n") < 0 && bufferBytes + chunkBytes > MAX_LINE_BYTES) {
10089
10644
  discarding = true;
10090
- discardedBytes = buffer.length;
10645
+ discardedBytes = bufferBytes + chunkBytes;
10091
10646
  buffer = "";
10647
+ bufferBytes = 0;
10092
10648
  options.onOverflow?.(discardedBytes);
10093
10649
  return;
10094
10650
  }
10651
+ buffer += chunk;
10652
+ drain();
10095
10653
  };
10096
10654
  const onEnd = () => {
10097
- if (buffer.length > 0) {
10098
- const rest = buffer;
10099
- buffer = "";
10100
- deliver(rest);
10655
+ const rest = buffer;
10656
+ const restBytes = bufferBytes || Buffer.byteLength(rest, "utf8");
10657
+ buffer = "";
10658
+ bufferBytes = 0;
10659
+ if (rest.length === 0) return;
10660
+ if (discarding) {
10661
+ discarding = false;
10662
+ discardedBytes = 0;
10663
+ return;
10664
+ }
10665
+ if (restBytes > MAX_LINE_BYTES) {
10666
+ options.onOverflow?.(restBytes);
10667
+ return;
10101
10668
  }
10669
+ deliver(rest);
10102
10670
  };
10103
10671
  input.setEncoding("utf8");
10104
10672
  input.on("data", onData);
@@ -10149,8 +10717,8 @@ async function runStdioServer(options) {
10149
10717
  let inFlight = 0;
10150
10718
  let inputEnded = false;
10151
10719
  let resolveDone = () => void 0;
10152
- const finished = new Promise((resolve8) => {
10153
- resolveDone = resolve8;
10720
+ const finished = new Promise((resolve9) => {
10721
+ resolveDone = resolve9;
10154
10722
  });
10155
10723
  const finishIfIdle = () => {
10156
10724
  if (inputEnded && inFlight === 0) resolveDone(0);