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/cli.cjs CHANGED
@@ -11,6 +11,7 @@ var ts = require('typescript');
11
11
  var readline = require('readline');
12
12
  var async_hooks = require('async_hooks');
13
13
  var os = require('os');
14
+ var http = require('http');
14
15
  var https = require('https');
15
16
  var worker_threads = require('worker_threads');
16
17
 
@@ -42,6 +43,7 @@ var crypto3__namespace = /*#__PURE__*/_interopNamespace(crypto3);
42
43
  var ts__default = /*#__PURE__*/_interopDefault(ts);
43
44
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
44
45
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
46
+ var http__namespace = /*#__PURE__*/_interopNamespace(http);
45
47
  var https__namespace = /*#__PURE__*/_interopNamespace(https);
46
48
 
47
49
  // src/cli.ts
@@ -1281,9 +1283,18 @@ var DEFAULT_SECRET_PATTERNS = [
1281
1283
  // The unquoted twin for YAML, docker-compose and `.env`: those formats never
1282
1284
  // quote their values, so the rules above cannot see them. `valueGroup` keeps
1283
1285
  // `POSTGRES_PASSWORD:` in the output.
1286
+ //
1287
+ // This is the one unquoted rule without `NOT_AFTER_QUOTE`, and the lookbehind is a
1288
+ // string-literal guard rather than a precision rule: a credential assignment *inside* a
1289
+ // literal — `const dsn = "password=hunter2;host=db"`, `curl -d "password=hunter2"` — was
1290
+ // rejected before it could be considered, and a DSN is a credential in a string. What made the
1291
+ // lookbehind redundant is the shape of `UNQUOTED_LITERAL`: a value has to start with a word
1292
+ // character, so `["awsSecret:", "apiKey:"]` — the list of key names the lookbehind was added
1293
+ // for — cannot match here anyway. `redact.ts` still refuses a match whose separator is inside
1294
+ // the literal, which is the case that list actually is.
1284
1295
  rule: "credential-assignment-unquoted",
1285
1296
  pattern: new RegExp(
1286
- `${NOT_AFTER_QUOTE}${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
1297
+ `${UNQUOTED_STRONG_NAME}\\s{0,8}${ASSIGN}\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
1287
1298
  "i"
1288
1299
  ),
1289
1300
  valueGroup: 1
@@ -1318,6 +1329,27 @@ var DEFAULT_SECRET_PATTERNS = [
1318
1329
  pattern: /(?<!["'])(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*(?<![=!<>])[:=](?!=)\s*["']([^"'\s]{8,256})["']/i,
1319
1330
  valueGroup: 1
1320
1331
  },
1332
+ {
1333
+ // `${VAR:-fallback}` — a default value in a shell script, a `.env`, docker-compose or a
1334
+ // GitHub Actions workflow.
1335
+ //
1336
+ // This shape defeated every rule above it, and each refusal was individually reasonable:
1337
+ // the quoted rules need a quote after the separator, and the unquoted ones need the value to
1338
+ // begin with a word character — a fallback begins with `$`. So
1339
+ // `POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-MyProdPassword123!}` was emitted whole, `.env` files
1340
+ // and compose files included, which is where that idiom is the norm.
1341
+ //
1342
+ // The **fallback** is what gets masked, not the whole expansion: the variable name is the
1343
+ // readable context, exactly as `POSTGRES_PASSWORD:` is kept by the rule above. A single
1344
+ // interpolation with no operator (`${DB_PASSWORD}`) still does not match — there is nothing
1345
+ // between the braces to mask, and `isTemplateInterpolation` keeps such a value readable.
1346
+ rule: "interpolation-fallback-secret",
1347
+ pattern: new RegExp(
1348
+ `(?:["']${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})\\}["'\`]?`,
1349
+ "i"
1350
+ ),
1351
+ valueGroup: 1
1352
+ },
1321
1353
  {
1322
1354
  // Covers `.env` / shell assignment syntax, which never quotes its values and
1323
1355
  // was therefore invisible to the rule above. Anchored to the line start and
@@ -1345,8 +1377,8 @@ var DEFAULT_SECRET_PATTERNS = [
1345
1377
  valueGroup: 1
1346
1378
  }
1347
1379
  ];
1348
- var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
1349
- var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
1380
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:(?!-----BEGIN)[\s\S])*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
1381
+ 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;
1350
1382
 
1351
1383
  // src/security/redact.ts
1352
1384
  var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
@@ -1440,6 +1472,20 @@ function toGlobal(rule) {
1440
1472
  return new RegExp(rule.pattern.source, [...flags].join(""));
1441
1473
  }
1442
1474
  var LINE_BREAK = /(\r\n|\n|\r)/;
1475
+ var LINE_TERMINATOR = /\r\n|\n|\r/g;
1476
+ var COMPILED_RULES = /* @__PURE__ */ new WeakMap();
1477
+ function compiledRuleFor(rule) {
1478
+ let compiled = COMPILED_RULES.get(rule);
1479
+ if (compiled === void 0) {
1480
+ compiled = {
1481
+ rule: rule.rule,
1482
+ pattern: toGlobal(rule),
1483
+ ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
1484
+ };
1485
+ COMPILED_RULES.set(rule, compiled);
1486
+ }
1487
+ return compiled;
1488
+ }
1443
1489
  function scanAndRedactSecrets(content, rules, filePath) {
1444
1490
  const findings = [];
1445
1491
  let redacted = content;
@@ -1451,18 +1497,15 @@ function scanAndRedactSecrets(content, rules, filePath) {
1451
1497
  blockPattern.lastIndex += 1;
1452
1498
  continue;
1453
1499
  }
1454
- const line = redacted.slice(0, match.index).split("\n").length;
1455
- const newlines = (match[0].match(/\n/g) ?? []).length;
1500
+ const before = redacted.slice(0, match.index);
1501
+ const line = (before.match(LINE_TERMINATOR) ?? []).length + 1;
1502
+ const newlines = (match[0].match(LINE_TERMINATOR) ?? []).length;
1456
1503
  findings.push({ rules: ["private-key-block"], line, path: filePath });
1457
- redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
1504
+ redacted = before + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
1458
1505
  blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
1459
1506
  }
1460
1507
  }
1461
- const compiled = rules.map((rule) => ({
1462
- rule: rule.rule,
1463
- pattern: toGlobal(rule),
1464
- ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
1465
- }));
1508
+ const compiled = rules.map(compiledRuleFor);
1466
1509
  const segments = redacted.split(LINE_BREAK);
1467
1510
  let lineNumber = 1;
1468
1511
  for (let index = 0; index < segments.length; index += 2) {
@@ -1480,8 +1523,10 @@ function scanAndRedactSecrets(content, rules, filePath) {
1480
1523
  }
1481
1524
  const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
1482
1525
  if (group && isSelfNamingConstant(line, group[0], group[1])) continue;
1483
- if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index)) continue;
1484
- if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index)) {
1526
+ if (valueGroup !== void 0 && startsInsideStringLiteral(line, match.index, group?.[0])) {
1527
+ continue;
1528
+ }
1529
+ if (group && isUnquotedValue(line, group[0]) && isDeclaredBinding(line, match.index, filePath)) {
1485
1530
  continue;
1486
1531
  }
1487
1532
  if (group && isTemplateInterpolation(line, group[0], group[1])) continue;
@@ -1514,14 +1559,20 @@ function expandOverToken(line, start, end) {
1514
1559
  while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
1515
1560
  return { start: left, end: right };
1516
1561
  }
1517
- function startsInsideStringLiteral(line, matchIndex) {
1562
+ function startsInsideStringLiteral(line, matchIndex, valueStart) {
1518
1563
  let runStart = matchIndex;
1519
1564
  while (runStart > 0 && /[A-Za-z0-9_-]/.test(line[runStart - 1])) runStart -= 1;
1520
1565
  const before = runStart > 0 ? line[runStart - 1] : "";
1521
- return before === '"' || before === "'" || before === "`";
1566
+ if (before !== '"' && before !== "'" && before !== "`") return false;
1567
+ if (valueStart === void 0) return true;
1568
+ return /["'`]/.test(line.slice(matchIndex, valueStart));
1522
1569
  }
1523
1570
  function isTemplateInterpolation(line, start, end) {
1524
- return line[start] === "$" && line[start + 1] === "{" && line[end - 1] === "}";
1571
+ if (line[start] !== "$" || line[start + 1] !== "{" || line[end - 1] !== "}") {
1572
+ return false;
1573
+ }
1574
+ const inner = line.slice(start + 2, end - 1).trim();
1575
+ return /^[A-Za-z_$][A-Za-z0-9_$.()[\]]*$/.test(inner);
1525
1576
  }
1526
1577
  function isSelfNamingConstant(line, start, end) {
1527
1578
  const value = line.slice(start, end);
@@ -1546,9 +1597,19 @@ function isUnquotedValue(line, start) {
1546
1597
  const before = start > 0 ? line[start - 1] : "";
1547
1598
  return before !== '"' && before !== "'" && before !== "`";
1548
1599
  }
1549
- var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|declare|export|using)\s+$/;
1600
+ var DECLARATION_KEYWORDS = /\b(?:const|let|var|readonly|public|private|protected|static|using)\s+$/;
1601
+ var ASSIGNMENT_FILE = /\.(?:sh|bash|zsh|ksh|fish|env|ya?ml|toml|ini|conf|cfg|properties|tf|tfvars|service)$/i;
1602
+ var ASSIGNMENT_BASENAME = /^(?:\.env(?:\.[^/]*)?|\.envrc|\.profile|\.bashrc|\.zshrc|\.bash_profile)$/i;
1603
+ function isAssignmentFile(filePath) {
1604
+ if (filePath === void 0) return false;
1605
+ const normalised = filePath.replace(/\\/g, "/");
1606
+ if (ASSIGNMENT_FILE.test(normalised)) return true;
1607
+ const base = normalised.slice(normalised.lastIndexOf("/") + 1);
1608
+ return ASSIGNMENT_BASENAME.test(base);
1609
+ }
1550
1610
  var DECLARATION_WINDOW = 40;
1551
- function isDeclaredBinding(line, matchIndex) {
1611
+ function isDeclaredBinding(line, matchIndex, filePath) {
1612
+ if (isAssignmentFile(filePath)) return false;
1552
1613
  let runStart = matchIndex;
1553
1614
  while (runStart > 0 && /[A-Za-z0-9_$]/.test(line[runStart - 1])) runStart -= 1;
1554
1615
  const from = Math.max(0, runStart - DECLARATION_WINDOW);
@@ -1590,6 +1651,15 @@ function readLinkTarget(linkPath, fallback) {
1590
1651
  return fallback;
1591
1652
  }
1592
1653
  }
1654
+ function brokenLinkRecord(linkPath, relativePath, rootReal) {
1655
+ const rawTarget = readLinkTarget(linkPath, linkPath);
1656
+ const resolvedFallback = path16__namespace.resolve(path16__namespace.dirname(linkPath), rawTarget);
1657
+ return isInside(rootReal, resolvedFallback) ? { path: relativePath, reason: "read-error", detail: "broken symlink" } : {
1658
+ path: relativePath,
1659
+ reason: "outside-root",
1660
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
1661
+ };
1662
+ }
1593
1663
  function isInside(root, candidate) {
1594
1664
  const relative12 = path16__namespace.relative(root, candidate);
1595
1665
  return relative12 === "" || !relative12.startsWith(`..${path16__namespace.sep}`) && relative12 !== ".." && !path16__namespace.isAbsolute(relative12);
@@ -1679,6 +1749,13 @@ function isSensitivePath(relativePath) {
1679
1749
  return matchesSensitiveList(posixPath, base);
1680
1750
  }
1681
1751
  var GLOBAL_PATTERNS = /* @__PURE__ */ new WeakMap();
1752
+ var GLOB_REGEX_CACHE = /* @__PURE__ */ new Map();
1753
+ var GITIGNORE_REGEX_CACHE = /* @__PURE__ */ new Map();
1754
+ var REGEX_CACHE_LIMIT = 4096;
1755
+ function rememberCompiled(cache, key, value) {
1756
+ if (cache.size >= REGEX_CACHE_LIMIT) cache.clear();
1757
+ cache.set(key, value);
1758
+ }
1682
1759
  function globalPattern(pattern) {
1683
1760
  let compiled = GLOBAL_PATTERNS.get(pattern);
1684
1761
  if (compiled === void 0) {
@@ -1689,8 +1766,8 @@ function globalPattern(pattern) {
1689
1766
  }
1690
1767
  var DEFAULT_PATH_RULES = [...DEFAULT_SECRET_PATTERNS];
1691
1768
  function pathScanRules(extraRules) {
1692
- if (extraRules === void 0 || extraRules.length === 0) return DEFAULT_PATH_RULES;
1693
- return [...DEFAULT_SECRET_PATTERNS, ...extraRules];
1769
+ if (extraRules === void 0) return DEFAULT_PATH_RULES;
1770
+ return extraRules;
1694
1771
  }
1695
1772
  function findSecretInPath(relativePath, extraRules) {
1696
1773
  const posixPath = relativePath.replace(/\\/g, "/");
@@ -1739,11 +1816,22 @@ function maskSkipPaths(records, extraRules) {
1739
1816
  });
1740
1817
  }
1741
1818
  function globToRegExp(glob) {
1819
+ const cached = GLOB_REGEX_CACHE.get(glob);
1820
+ if (cached !== void 0) return cached;
1742
1821
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1743
1822
  const anchored = normalized.startsWith("/");
1744
1823
  const body = anchored ? normalized.slice(1) : normalized;
1745
1824
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
1746
- return new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
1825
+ let compiled;
1826
+ try {
1827
+ compiled = new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
1828
+ } catch (error) {
1829
+ throw new Error(
1830
+ `"${glob}" is not a valid glob: ${error.message}. A character class must be ascending, as in "[a-z]".`
1831
+ );
1832
+ }
1833
+ rememberCompiled(GLOB_REGEX_CACHE, glob, compiled);
1834
+ return compiled;
1747
1835
  }
1748
1836
  function globBodyToRegExp(body, allowBraces) {
1749
1837
  let out = "";
@@ -1855,6 +1943,14 @@ function escapeRegExpChar(character) {
1855
1943
  return /[\\^$.*+?()[\]{}|/]/.test(character) ? `\\${character}` : character;
1856
1944
  }
1857
1945
  function matchGitignorePattern(target, pattern) {
1946
+ let compiled = GITIGNORE_REGEX_CACHE.get(pattern);
1947
+ if (compiled === void 0) {
1948
+ compiled = compileGitignorePattern(pattern);
1949
+ rememberCompiled(GITIGNORE_REGEX_CACHE, pattern, compiled);
1950
+ }
1951
+ return compiled.test(target);
1952
+ }
1953
+ function compileGitignorePattern(pattern) {
1858
1954
  const anchored = pattern.startsWith("/");
1859
1955
  const body = anchored ? pattern.slice(1) : pattern;
1860
1956
  let source = "";
@@ -1903,7 +1999,11 @@ function matchGitignorePattern(target, pattern) {
1903
1999
  source += escapeRegExpChar(character);
1904
2000
  }
1905
2001
  const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
1906
- return new RegExp(`${prefix}${source}(?:/.*)?$`).test(target);
2002
+ try {
2003
+ return new RegExp(`${prefix}${source}(?:/.*)?$`);
2004
+ } catch {
2005
+ return /$^/;
2006
+ }
1907
2007
  }
1908
2008
  function matchesGitignoreRule(rule, scoped, isDirectory) {
1909
2009
  if (!rule.dirOnly) return matchGitignorePattern(scoped, rule.pattern);
@@ -1933,14 +2033,31 @@ function gitignoreVerdict(layers, relativePath, isDirectory) {
1933
2033
  }
1934
2034
  return verdict;
1935
2035
  }
1936
- function defaultIgnoreReason(relativePath) {
2036
+ var DEFAULT_TOOL_DIR_NAMES = ["snapshot"];
2037
+ function toolDirNamesFor(toolOwnedPaths) {
2038
+ const names = new Set(DEFAULT_TOOL_DIR_NAMES);
2039
+ for (const entry of toolOwnedPaths) {
2040
+ const base = entry.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop();
2041
+ if (base !== void 0 && base.length > 0) names.add(base);
2042
+ }
2043
+ return [...names];
2044
+ }
2045
+ function isToolLeftoverName(name, dirNames) {
2046
+ for (const dirName of dirNames) {
2047
+ if (name.startsWith(`${dirName}.backup-`) || name.startsWith(`${dirName}.partial-`)) {
2048
+ return true;
2049
+ }
2050
+ }
2051
+ return false;
2052
+ }
2053
+ function defaultIgnoreReason(relativePath, toolDirNames = DEFAULT_TOOL_DIR_NAMES) {
1937
2054
  const posixPath = relativePath.replace(/\\/g, "/");
1938
2055
  const parts = posixPath.split("/");
1939
2056
  for (let i = 0; i < parts.length - 1; i++) {
1940
2057
  if (DEFAULT_IGNORED_DIRS.includes(parts[i])) {
1941
2058
  return `built-in ignore: directory "${parts[i]}"`;
1942
2059
  }
1943
- if (parts[i].startsWith("snapshot.backup-")) {
2060
+ if (isToolLeftoverName(parts[i], toolDirNames)) {
1944
2061
  return `built-in ignore: leftover snapshot backup "${parts[i]}"`;
1945
2062
  }
1946
2063
  }
@@ -1948,6 +2065,9 @@ function defaultIgnoreReason(relativePath) {
1948
2065
  if (DEFAULT_IGNORED_FILES.includes(base)) {
1949
2066
  return `built-in ignore: filename "${base}"`;
1950
2067
  }
2068
+ if (isToolLeftoverName(base, toolDirNames)) {
2069
+ return `built-in ignore: leftover snapshot backup "${base}"`;
2070
+ }
1951
2071
  return null;
1952
2072
  }
1953
2073
  function listIgnoredPaths(cwd) {
@@ -2016,41 +2136,72 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
2016
2136
  }
2017
2137
  const reported = /* @__PURE__ */ new Set();
2018
2138
  const kept = [];
2139
+ const report = (linkedPath, record) => {
2140
+ if (reported.has(linkedPath)) return;
2141
+ reported.add(linkedPath);
2142
+ skipped.push(record);
2143
+ };
2019
2144
  for (const candidate of candidates) {
2020
2145
  const parts = candidate.split("/");
2021
- let linkedDir = null;
2146
+ let linkedPath = null;
2022
2147
  for (let depth = 1; depth <= parts.length; depth += 1) {
2023
2148
  const prefix = parts.slice(0, depth).join("/");
2024
2149
  if (isLink(path16__namespace.join(cwd, prefix))) {
2025
- linkedDir = prefix;
2150
+ linkedPath = prefix;
2026
2151
  break;
2027
2152
  }
2028
2153
  }
2029
- if (linkedDir === null) {
2154
+ if (linkedPath === null) {
2030
2155
  kept.push(candidate);
2031
2156
  continue;
2032
2157
  }
2033
- if (reported.has(linkedDir)) continue;
2034
- reported.add(linkedDir);
2035
- const absolute = path16__namespace.join(cwd, linkedDir);
2036
- let targetReal = absolute;
2158
+ const absolute = path16__namespace.join(cwd, linkedPath);
2159
+ const rawTarget = readLinkTarget(absolute, absolute);
2160
+ let targetReal = null;
2037
2161
  try {
2038
2162
  targetReal = fs14__namespace.realpathSync(absolute);
2039
2163
  } catch {
2164
+ targetReal = null;
2040
2165
  }
2041
- const target = readLinkTarget(absolute, targetReal);
2042
- const what = linkedDir === candidate ? "symbolic link" : "directory link";
2043
- skipped.push(
2044
- isInside(rootReal, targetReal) ? {
2045
- path: linkedDir,
2046
- reason: "symlink",
2047
- detail: `${what} not followed (target: ${target})`
2048
- } : {
2049
- path: linkedDir,
2166
+ if (targetReal === null) {
2167
+ report(linkedPath, brokenLinkRecord(absolute, linkedPath, rootReal));
2168
+ continue;
2169
+ }
2170
+ if (!isInside(rootReal, targetReal)) {
2171
+ report(linkedPath, {
2172
+ path: linkedPath,
2050
2173
  reason: "outside-root",
2051
- detail: `link target resolves outside the scanned root (target: ${target}); not followed`
2052
- }
2053
- );
2174
+ detail: `link target resolves outside the scanned root (target: ${rawTarget}); not followed`
2175
+ });
2176
+ continue;
2177
+ }
2178
+ let stats = null;
2179
+ try {
2180
+ stats = fs14__namespace.statSync(absolute);
2181
+ } catch {
2182
+ stats = null;
2183
+ }
2184
+ if (stats === null) {
2185
+ report(linkedPath, { path: linkedPath, reason: "read-error", detail: "unreadable symlink" });
2186
+ continue;
2187
+ }
2188
+ if (linkedPath !== candidate || stats.isDirectory()) {
2189
+ report(linkedPath, {
2190
+ path: linkedPath,
2191
+ reason: "symlink",
2192
+ detail: `directory link not followed (target: ${rawTarget})`
2193
+ });
2194
+ continue;
2195
+ }
2196
+ if (!stats.isFile()) {
2197
+ report(linkedPath, {
2198
+ path: linkedPath,
2199
+ reason: "symlink",
2200
+ detail: `link to neither a file nor a directory (target: ${rawTarget})`
2201
+ });
2202
+ continue;
2203
+ }
2204
+ kept.push(candidate);
2054
2205
  }
2055
2206
  return kept;
2056
2207
  }
@@ -2097,6 +2248,7 @@ function collectFiles(cwd, options = {}) {
2097
2248
  (options.toolOwnedPaths ?? []).map((entry) => entry.replace(/\\/g, "/"))
2098
2249
  );
2099
2250
  const isToolOwned = makeToolOwnedDecider(cwd, [...toolOwnedPaths]);
2251
+ const toolDirNames = toolDirNamesFor(toolOwnedPaths);
2100
2252
  let candidates;
2101
2253
  const skipped = [];
2102
2254
  const discovery = options.discovery ?? "auto";
@@ -2125,7 +2277,7 @@ function collectFiles(cwd, options = {}) {
2125
2277
  }
2126
2278
  const files = candidates.filter((file) => {
2127
2279
  if (isToolOwned(file)) return false;
2128
- const ignored = defaultIgnoreReason(file);
2280
+ const ignored = defaultIgnoreReason(file, toolDirNames);
2129
2281
  if (ignored) {
2130
2282
  skipped.push({ path: file, reason: "default-ignore", detail: ignored });
2131
2283
  return false;
@@ -2180,6 +2332,7 @@ function rootRealFor(root) {
2180
2332
  function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PURE__ */ new Set(), parentGitignoreLayers = []) {
2181
2333
  const results = { files: [], skipped: [] };
2182
2334
  const stack = [];
2335
+ const toolDirNames = toolDirNamesFor(reservedPaths);
2183
2336
  const relativeToBase = (target) => path16__namespace.relative(baseDir, target).replace(/\\/g, "/") || ".";
2184
2337
  const enter = (current, layers) => {
2185
2338
  if (visited.has(current)) return false;
@@ -2224,11 +2377,11 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2224
2377
  });
2225
2378
  continue;
2226
2379
  }
2227
- if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
2380
+ if (DEFAULT_IGNORED_FILES.includes(entry.name) || isToolLeftoverName(entry.name, toolDirNames)) {
2228
2381
  results.skipped.push({
2229
2382
  path: relPath,
2230
2383
  reason: "default-ignore",
2231
- detail: "built-in ignore: filename"
2384
+ detail: isToolLeftoverName(entry.name, toolDirNames) ? `built-in ignore: leftover snapshot backup "${entry.name}" (not descended)` : "built-in ignore: filename"
2232
2385
  });
2233
2386
  continue;
2234
2387
  }
@@ -2249,7 +2402,7 @@ function walkDirectory(dir, baseDir, rootReal, visited, reservedPaths = /* @__PU
2249
2402
  try {
2250
2403
  targetReal = fs14__namespace.realpathSync(fullPath);
2251
2404
  } catch {
2252
- results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
2405
+ results.skipped.push(brokenLinkRecord(fullPath, relPath, rootReal));
2253
2406
  continue;
2254
2407
  }
2255
2408
  let stats;
@@ -2337,8 +2490,9 @@ function findConfigFile(cwd) {
2337
2490
  const pkgJsonPath = path16__namespace.join(cwd, "package.json");
2338
2491
  if (!fs14__namespace.existsSync(pkgJsonPath)) return null;
2339
2492
  try {
2340
- const parsed = JSON.parse(fs14__namespace.readFileSync(pkgJsonPath, "utf8"));
2341
- return parsed.snapshot === void 0 ? null : pkgJsonPath;
2493
+ const parsed = JSON.parse(stripJsonComments(fs14__namespace.readFileSync(pkgJsonPath, "utf8")));
2494
+ const snapshot = parsed?.snapshot;
2495
+ return typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot) ? pkgJsonPath : null;
2342
2496
  } catch {
2343
2497
  return null;
2344
2498
  }
@@ -2388,15 +2542,19 @@ function normalizeBudgets(budgets, maxPartBytes) {
2388
2542
  }
2389
2543
  function normalizeResolvedConfig(config) {
2390
2544
  const discovery = isDiscoveryMode(config.discovery) ? config.discovery : "auto";
2545
+ const monolithWanted = config.includeMonolith === true || config.artifacts?.monolith === true;
2546
+ const artifacts = resolveArtifacts(
2547
+ config.artifacts,
2548
+ void 0,
2549
+ monolithWanted,
2550
+ monolithWanted
2551
+ );
2391
2552
  return markResolvedConfig({
2392
2553
  ...config,
2393
2554
  discovery,
2555
+ includeMonolith: artifacts.monolith,
2394
2556
  secretRules: compileSecretRules(config.secretRules),
2395
- artifacts: resolveArtifacts(
2396
- config.artifacts,
2397
- void 0,
2398
- config.includeMonolith === true
2399
- ),
2557
+ artifacts,
2400
2558
  include: resolveInclude(config.include),
2401
2559
  preset: typeof config.preset === "string" ? config.preset : "generic",
2402
2560
  presetExclude: [
@@ -2605,6 +2763,16 @@ async function importConfigModule(fullPath) {
2605
2763
  url$1.searchParams.set("codebaseSnapshotLoad", String(moduleConfigLoadToken));
2606
2764
  return await import(url$1.href);
2607
2765
  }
2766
+ function attachConfigDir(config, filePath) {
2767
+ if (typeof config === "object" && config !== null) {
2768
+ Object.defineProperty(config, CONFIG_DIR_SYMBOL, {
2769
+ value: path16__namespace.dirname(filePath),
2770
+ enumerable: false,
2771
+ configurable: true
2772
+ });
2773
+ }
2774
+ return config;
2775
+ }
2608
2776
  async function loadConfigFileDetailed(cwd) {
2609
2777
  const warnings = [];
2610
2778
  const errors = [];
@@ -2620,7 +2788,7 @@ async function loadConfigFileDetailed(cwd) {
2620
2788
  );
2621
2789
  }
2622
2790
  assertKnownConfigKeys(config, fullPath);
2623
- return { config, file: fullPath, warnings, errors };
2791
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
2624
2792
  } catch (err) {
2625
2793
  const raw = err.message;
2626
2794
  const message = raw.includes(fullPath) ? raw : `${fullPath}: ${raw}`;
@@ -2639,7 +2807,7 @@ async function loadConfigFileDetailed(cwd) {
2639
2807
  const imported = await importConfigModule(fullPath);
2640
2808
  const hasDefault = Object.prototype.hasOwnProperty.call(imported, "default");
2641
2809
  const config = readConfigExport(hasDefault ? imported.default : imported, fullPath);
2642
- return { config, file: fullPath, warnings, errors };
2810
+ return { config: attachConfigDir(config, fullPath), file: fullPath, warnings, errors };
2643
2811
  } catch (err) {
2644
2812
  const message = `Could not load ${fullPath}: ${err.message}`;
2645
2813
  warnings.push(message);
@@ -2657,7 +2825,12 @@ async function loadConfigFileDetailed(cwd) {
2657
2825
  snapshotKey,
2658
2826
  `${pkgJsonPath} (the "snapshot" key)`
2659
2827
  );
2660
- return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
2828
+ return {
2829
+ config: attachConfigDir(snapshotKey, pkgJsonPath),
2830
+ file: pkgJsonPath,
2831
+ warnings,
2832
+ errors
2833
+ };
2661
2834
  }
2662
2835
  if (snapshotKey !== void 0) {
2663
2836
  const kind = Array.isArray(snapshotKey) ? "array" : typeof snapshotKey;
@@ -2696,7 +2869,7 @@ var DEFAULT_ARTIFACTS = {
2696
2869
  function isArtifactName(value) {
2697
2870
  return ARTIFACT_NAMES.includes(value);
2698
2871
  }
2699
- function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
2872
+ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride, only) {
2700
2873
  const artifacts = {
2701
2874
  ...DEFAULT_ARTIFACTS,
2702
2875
  // `includeMonolith` predates this selection and stays the switch for it.
@@ -2721,6 +2894,21 @@ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride)
2721
2894
  if (monolithOverride !== void 0) {
2722
2895
  artifacts.monolith = monolithOverride;
2723
2896
  }
2897
+ if (only !== void 0 && only.length > 0) {
2898
+ const keep = /* @__PURE__ */ new Set();
2899
+ for (const name of only) {
2900
+ if (!isArtifactName(name)) {
2901
+ throw new Error(
2902
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
2903
+ );
2904
+ }
2905
+ keep.add(name);
2906
+ }
2907
+ for (const name of ARTIFACT_NAMES) {
2908
+ artifacts[name] = keep.has(name);
2909
+ }
2910
+ return artifacts;
2911
+ }
2724
2912
  for (const name of skipped ?? []) {
2725
2913
  if (!isArtifactName(name)) {
2726
2914
  throw new Error(
@@ -3021,7 +3209,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
3021
3209
  userConfig?.artifacts,
3022
3210
  cliOptions?.skipArtifacts,
3023
3211
  includeMonolith,
3024
- cliMonolith
3212
+ cliMonolith,
3213
+ cliOptions?.onlyArtifacts
3025
3214
  );
3026
3215
  const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
3027
3216
  const maxPartBytes = boundedBytes(
@@ -3195,7 +3384,18 @@ function configDigest(config) {
3195
3384
  excludePatterns: config.excludePatterns,
3196
3385
  preset: config.preset,
3197
3386
  layersMode: config.layersMode,
3198
- budgets: config.budgets,
3387
+ // `budgets.byLayer` is a plain object, and `JSON.stringify` preserves insertion order — so
3388
+ // `{ docs, api }` and `{ api, docs }` hashed differently despite being the same settings, and
3389
+ // `validate` reported configuration drift for a snapshot nobody had changed. The keys are
3390
+ // canonicalised here; nothing else about the object is order-dependent.
3391
+ budgets: {
3392
+ default: config.budgets.default,
3393
+ byLayer: Object.fromEntries(
3394
+ Object.entries(config.budgets.byLayer).sort(
3395
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
3396
+ )
3397
+ )
3398
+ },
3199
3399
  // The matcher is what makes a layer a layer. Hashing only the names meant that
3200
3400
  // moving `{ name: "api", match: "src/api/**" }` to `"src/v2/**"` produced the same
3201
3401
  // digest, so `validate` reported "valid and up to date" for a snapshot whose files
@@ -3297,6 +3497,10 @@ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
3297
3497
  ".mjs",
3298
3498
  ".cjs"
3299
3499
  ]);
3500
+ var PYTHON_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".pyi"]);
3501
+ var RUBY_EXTENSIONS = /* @__PURE__ */ new Set([".rb", ".rake", ".gemspec"]);
3502
+ var C_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"]);
3503
+ var PYTHON_SUFFIXES = [".py", ".pyi", "/__init__.py", "/__init__.pyi"];
3300
3504
  function buildImportResolver(files) {
3301
3505
  const byPath = /* @__PURE__ */ new Map();
3302
3506
  const byPathFolded = /* @__PURE__ */ new Map();
@@ -3323,11 +3527,45 @@ function buildImportResolver(files) {
3323
3527
  return { byPath, byPathFolded, byStem, byStemFolded };
3324
3528
  }
3325
3529
  function resolveSpecifier(fromFile, specifier, resolver) {
3530
+ const extension = path16__namespace.posix.extname(fromFile).toLowerCase();
3531
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
3532
+ const firstOf = (candidates) => {
3533
+ for (const candidate of candidates) {
3534
+ const hit = exact(candidate);
3535
+ if (hit) return hit;
3536
+ }
3537
+ return null;
3538
+ };
3539
+ if (PYTHON_EXTENSIONS.has(extension) && specifier.startsWith(".")) {
3540
+ const dots = /^\.+/.exec(specifier)?.[0].length ?? 0;
3541
+ const rest = specifier.slice(dots).replace(/\./g, "/");
3542
+ let base = path16__namespace.posix.dirname(fromFile);
3543
+ for (let up = 1; up < dots; up += 1) base = path16__namespace.posix.dirname(base);
3544
+ const joined2 = rest ? path16__namespace.posix.normalize(path16__namespace.posix.join(base, rest)) : base;
3545
+ return firstOf([joined2, ...PYTHON_SUFFIXES.map((suffix) => `${joined2}${suffix}`)]);
3546
+ }
3547
+ if (RUBY_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
3548
+ const joined2 = path16__namespace.posix.normalize(
3549
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
3550
+ );
3551
+ return firstOf([`${joined2}.rb`, joined2]);
3552
+ }
3553
+ if (C_EXTENSIONS.has(extension) && specifier.startsWith("./")) {
3554
+ const joined2 = path16__namespace.posix.normalize(
3555
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
3556
+ );
3557
+ return exact(joined2);
3558
+ }
3559
+ if (extension === ".dart" && !specifier.includes(":")) {
3560
+ const joined2 = path16__namespace.posix.normalize(
3561
+ path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
3562
+ );
3563
+ return firstOf([joined2, `${joined2}.dart`]);
3564
+ }
3326
3565
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
3327
3566
  const joined = path16__namespace.posix.normalize(
3328
3567
  path16__namespace.posix.join(path16__namespace.posix.dirname(fromFile), specifier)
3329
3568
  );
3330
- const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
3331
3569
  const direct = exact(joined);
3332
3570
  if (direct) return direct;
3333
3571
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
@@ -3453,13 +3691,23 @@ function summarizeTypeScriptSource(relativePath, content) {
3453
3691
  if (ts__default.default.isImportDeclaration(node) && ts__default.default.isStringLiteral(node.moduleSpecifier)) {
3454
3692
  summary.imports.push(node.moduleSpecifier.text);
3455
3693
  }
3694
+ if (ts__default.default.isImportEqualsDeclaration(node) && ts__default.default.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression !== void 0 && ts__default.default.isStringLiteral(node.moduleReference.expression)) {
3695
+ summary.imports.push(node.moduleReference.expression.text);
3696
+ }
3697
+ if (ts__default.default.isCallExpression(node) && node.arguments.length >= 1) {
3698
+ const firstArgument = node.arguments[0];
3699
+ if (firstArgument !== void 0 && ts__default.default.isStringLiteral(firstArgument)) {
3700
+ const isDynamicImport = node.expression.kind === ts__default.default.SyntaxKind.ImportKeyword;
3701
+ const isRequire = ts__default.default.isIdentifier(node.expression) && node.expression.text === "require";
3702
+ if (isDynamicImport || isRequire) summary.imports.push(firstArgument.text);
3703
+ }
3704
+ }
3456
3705
  if (ts__default.default.isExportDeclaration(node)) {
3457
3706
  if (node.moduleSpecifier && ts__default.default.isStringLiteral(node.moduleSpecifier)) {
3458
3707
  summary.imports.push(node.moduleSpecifier.text);
3708
+ summary.reExports.push(node.moduleSpecifier.text);
3459
3709
  if (node.exportClause && ts__default.default.isNamespaceExport(node.exportClause)) {
3460
3710
  summary.exports.push(node.exportClause.name.text);
3461
- } else if (!node.exportClause) {
3462
- summary.reExports.push(node.moduleSpecifier.text);
3463
3711
  }
3464
3712
  }
3465
3713
  if (node.exportClause && ts__default.default.isNamedExports(node.exportClause)) {
@@ -3604,7 +3852,7 @@ function isCapitalizedName(name) {
3604
3852
  }
3605
3853
 
3606
3854
  // src/ast/profiles/cfamily.ts
3607
- var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
3855
+ var INCLUDE = /^#\s*include\s*([<"])([^>"]+)[>"]/;
3608
3856
  var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
3609
3857
  var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
3610
3858
  var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
@@ -3623,7 +3871,7 @@ var cfamilyProfile = {
3623
3871
  }
3624
3872
  const included = INCLUDE.exec(trimmed);
3625
3873
  if (included) {
3626
- summary.imports.push(included[1]);
3874
+ summary.imports.push(included[1] === '"' ? `./${included[2]}` : included[2]);
3627
3875
  return;
3628
3876
  }
3629
3877
  const used = USING.exec(trimmed);
@@ -4027,7 +4275,15 @@ var pythonProfile = {
4027
4275
  }
4028
4276
  const from = FROM_IMPORT.exec(trimmed);
4029
4277
  if (from) {
4030
- summary.imports.push(from[1]);
4278
+ const module = from[1];
4279
+ if (/^\.+$/.test(module)) {
4280
+ for (const name of from[2].split(",")) {
4281
+ const imported2 = name.replace(/[()]/g, "").trim().split(/\s+as\s+/)[0].trim();
4282
+ if (/^[A-Za-z_]\w*$/.test(imported2)) summary.imports.push(`${module}${imported2}`);
4283
+ }
4284
+ } else {
4285
+ summary.imports.push(module);
4286
+ }
4031
4287
  routePending = false;
4032
4288
  return;
4033
4289
  }
@@ -4076,7 +4332,8 @@ var pythonProfile = {
4076
4332
  };
4077
4333
 
4078
4334
  // src/ast/profiles/ruby.ts
4079
- var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
4335
+ var REQUIRE_RELATIVE = /^require_relative\s+["']([^"']+)["']/;
4336
+ var REQUIRE = /^require\s+["']([^"']+)["']/;
4080
4337
  var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
4081
4338
  var METHOD2 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
4082
4339
  var rubyProfile = {
@@ -4087,6 +4344,11 @@ var rubyProfile = {
4087
4344
  eachLine(content, (line) => {
4088
4345
  const trimmed = line.trim();
4089
4346
  if (!trimmed || trimmed.startsWith("#")) return;
4347
+ const relative12 = REQUIRE_RELATIVE.exec(trimmed);
4348
+ if (relative12) {
4349
+ summary.imports.push(`./${relative12[1]}`);
4350
+ return;
4351
+ }
4090
4352
  const required = REQUIRE.exec(trimmed);
4091
4353
  if (required) {
4092
4354
  summary.imports.push(required[1]);
@@ -4177,7 +4439,8 @@ var rustProfile = {
4177
4439
  };
4178
4440
 
4179
4441
  // src/ast/profiles/sfc.ts
4180
- var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
4442
+ var SCRIPT_BLOCK = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
4443
+ var JSX_LANG = /\blang\s*=\s*["']?[tj]sx\b/i;
4181
4444
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
4182
4445
  function componentName(path22) {
4183
4446
  const base = path22.split("/").pop() ?? path22;
@@ -4188,13 +4451,15 @@ var sfcProfile = {
4188
4451
  extensions: [".vue", ".svelte", ".astro"],
4189
4452
  summarize(path22, content) {
4190
4453
  let script = "";
4454
+ let usesJsx = path22.toLowerCase().endsWith(".astro");
4191
4455
  if (path22.toLowerCase().endsWith(".astro")) {
4192
4456
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
4193
4457
  if (frontmatter) script += `${frontmatter[1]}
4194
4458
  `;
4195
4459
  }
4196
4460
  for (const match of content.matchAll(SCRIPT_BLOCK)) {
4197
- script += `${match[1]}
4461
+ if (JSX_LANG.test(match[1] ?? "")) usesJsx = true;
4462
+ script += `${match[2]}
4198
4463
  `;
4199
4464
  }
4200
4465
  const name = componentName(path22);
@@ -4206,7 +4471,7 @@ var sfcProfile = {
4206
4471
  }
4207
4472
  return { summary: finalizeSummary(summary2), quality: "heuristic" };
4208
4473
  }
4209
- const summary = summarizeTypeScriptSource(`${name}.ts`, script);
4474
+ const summary = summarizeTypeScriptSource(`${name}.${usesJsx ? "tsx" : "ts"}`, script);
4210
4475
  summary.reactComponents.push(name);
4211
4476
  summary.exports.push(name);
4212
4477
  return { summary: finalizeSummary(summary), quality: "ast" };
@@ -4640,18 +4905,22 @@ function isBinaryFile(absolutePath) {
4640
4905
  }
4641
4906
 
4642
4907
  // src/ast/imports.ts
4908
+ var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
4643
4909
  var TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4644
4910
  var isTsSource = (relativePath) => {
4645
4911
  const lower = relativePath.toLowerCase();
4646
4912
  return TS_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
4647
4913
  };
4648
4914
  function importsOfText(relativePath, text2) {
4915
+ if (Buffer.byteLength(text2, "utf8") > SUMMARY_MAX_SOURCE_BYTES) return [];
4649
4916
  if (isTsSource(relativePath)) return summarizeTypeScriptSource(relativePath, text2).imports;
4650
4917
  return summarizeWithProfile(relativePath, text2)?.summary.imports ?? [];
4651
4918
  }
4652
4919
  function importsOfFile(relativePath, absolutePath) {
4653
4920
  let buffer;
4654
4921
  try {
4922
+ const stats = fs14__namespace.statSync(absolutePath);
4923
+ if (!stats.isFile() || stats.size > SUMMARY_MAX_SOURCE_BYTES) return [];
4655
4924
  buffer = fs14__namespace.readFileSync(absolutePath);
4656
4925
  } catch {
4657
4926
  return [];
@@ -4782,9 +5051,13 @@ function describeManifestShapeProblem(value) {
4782
5051
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
4783
5052
  return 'a "parts" entry is not an object';
4784
5053
  }
4785
- if (typeof entry.filename !== "string") {
5054
+ const filename = entry.filename;
5055
+ if (typeof filename !== "string" || filename.trim() === "") {
4786
5056
  return 'a "parts" entry has no "filename"';
4787
5057
  }
5058
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("\0") || filename === "." || filename === ".." || path16__namespace.isAbsolute(filename)) {
5059
+ return 'a "parts" entry "filename" must be a bare filename inside parts/';
5060
+ }
4788
5061
  }
4789
5062
  if (!candidate.layers || typeof candidate.layers !== "object" || Array.isArray(candidate.layers)) {
4790
5063
  return 'missing "layers" object';
@@ -4895,7 +5168,7 @@ function validateSnapshot(configInput) {
4895
5168
  });
4896
5169
  for (const diskFile of collection.files) {
4897
5170
  const snapshotHash = manifestFileMap.get(diskFile);
4898
- if (!snapshotHash) {
5171
+ if (snapshotHash === void 0) {
4899
5172
  const skipReason = skippedReasonOnDisk(diskFile);
4900
5173
  if (skipReason === "unchanged") {
4901
5174
  if (unchangedScope !== null) {
@@ -4909,6 +5182,11 @@ function validateSnapshot(configInput) {
4909
5182
  drift.missingInSnapshot.push(diskFile);
4910
5183
  continue;
4911
5184
  }
5185
+ if (unchangedScope !== null && !unchangedScope.has(diskFile) && !becameDependent(diskFile)) {
5186
+ drift.configMismatches.push(
5187
+ `scope: ${maskedPath(diskFile, config.secretRules)} is in the snapshot but is no longer in the changed-files scope`
5188
+ );
5189
+ }
4912
5190
  let diskHash;
4913
5191
  try {
4914
5192
  diskHash = sha256Hex(fs14__namespace.readFileSync(path16__namespace.join(config.cwd, diskFile)));
@@ -5049,6 +5327,41 @@ function validateSnapshot(configInput) {
5049
5327
  `includeSensitiveFiles: manifest ${recorded.includeSensitiveFiles} vs current ${config.includeSensitiveFiles}`
5050
5328
  );
5051
5329
  }
5330
+ if ((recorded.maxPartTokens ?? void 0) !== (config.maxPartTokens ?? void 0)) {
5331
+ drift.configMismatches.push(
5332
+ `maxPartTokens: manifest ${String(recorded.maxPartTokens ?? "unset")} vs current ${String(config.maxPartTokens ?? "unset")}`
5333
+ );
5334
+ }
5335
+ if ((recorded.maxMonolithBytes ?? null) !== (config.maxMonolithBytes ?? null)) {
5336
+ drift.configMismatches.push(
5337
+ `maxMonolithBytes: manifest ${String(recorded.maxMonolithBytes ?? null)} vs current ${String(config.maxMonolithBytes ?? null)}`
5338
+ );
5339
+ }
5340
+ if (Boolean(recorded.skeleton) !== Boolean(config.skeleton)) {
5341
+ drift.configMismatches.push(
5342
+ `skeleton: manifest ${Boolean(recorded.skeleton)} vs current ${Boolean(config.skeleton)}`
5343
+ );
5344
+ }
5345
+ if (Boolean(recorded.sarif) !== Boolean(config.sarif)) {
5346
+ drift.configMismatches.push(
5347
+ `sarif: manifest ${Boolean(recorded.sarif)} vs current ${Boolean(config.sarif)}`
5348
+ );
5349
+ }
5350
+ if ((recorded.changedSince ?? void 0) !== (config.changedSince ?? void 0)) {
5351
+ drift.configMismatches.push(
5352
+ `changedSince: manifest ${String(recorded.changedSince ?? "unset")} vs current ${String(config.changedSince ?? "unset")}`
5353
+ );
5354
+ }
5355
+ if (Boolean(recorded.staged) !== Boolean(config.staged)) {
5356
+ drift.configMismatches.push(
5357
+ `staged: manifest ${Boolean(recorded.staged)} vs current ${Boolean(config.staged)}`
5358
+ );
5359
+ }
5360
+ if (Boolean(recorded.withDependents) !== Boolean(config.withDependents)) {
5361
+ drift.configMismatches.push(
5362
+ `withDependents: manifest ${Boolean(recorded.withDependents)} vs current ${Boolean(config.withDependents)}`
5363
+ );
5364
+ }
5052
5365
  const effectiveDiscovery = config.discovery ?? "auto";
5053
5366
  const recordedDiscovery = recorded.discovery;
5054
5367
  if (recordedDiscovery !== void 0 && recordedDiscovery !== effectiveDiscovery) {
@@ -5523,9 +5836,21 @@ function renderDiffText(diff) {
5523
5836
  }
5524
5837
 
5525
5838
  // src/partitioner/preview.ts
5839
+ var LINE_ENDINGS = /\r\n|\r/g;
5840
+ var normaliseLineEndings = (content) => content.replace(LINE_ENDINGS, "\n");
5526
5841
  function countLines(content) {
5527
5842
  if (content.length === 0) return 0;
5528
- return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
5843
+ let breaks = 0;
5844
+ for (let index = 0; index < content.length; index += 1) {
5845
+ const code = content.charCodeAt(index);
5846
+ if (code === 10) {
5847
+ breaks += 1;
5848
+ continue;
5849
+ }
5850
+ if (code === 13 && content.charCodeAt(index + 1) !== 10) breaks += 1;
5851
+ }
5852
+ const last = content.charCodeAt(content.length - 1);
5853
+ return last === 10 || last === 13 ? breaks : breaks + 1;
5529
5854
  }
5530
5855
  function truncateToBytes(content, maxBytes) {
5531
5856
  if (maxBytes <= 0) return { content: "", isTruncated: content.length > 0 };
@@ -5536,17 +5861,18 @@ function truncateToBytes(content, maxBytes) {
5536
5861
  return { content: slice, isTruncated: true };
5537
5862
  }
5538
5863
  function truncatePreview(content, headLines, tailLines) {
5539
- const lines = content.split("\n");
5540
- const total = countLines(content);
5864
+ const normalised = normaliseLineEndings(content);
5865
+ const lines = normalised.split("\n");
5866
+ const total = countLines(normalised);
5541
5867
  if (total <= headLines + tailLines) {
5542
5868
  return {
5543
- content,
5869
+ content: normalised,
5544
5870
  isTruncated: false,
5545
5871
  originalLines: total,
5546
5872
  includedLines: total
5547
5873
  };
5548
5874
  }
5549
- const window = content.endsWith("\n") ? lines.slice(0, -1) : lines;
5875
+ const window = normalised.endsWith("\n") ? lines.slice(0, -1) : lines;
5550
5876
  const head = window.slice(0, headLines);
5551
5877
  const tail = tailLines > 0 ? window.slice(window.length - tailLines) : [];
5552
5878
  const omitted = total - head.length - tail.length;
@@ -5666,10 +5992,13 @@ function describeMatcher(matcher) {
5666
5992
  return String(matcher);
5667
5993
  }
5668
5994
  function normalizeTarget(cwd, target) {
5669
- const absolute = path16__namespace.resolve(cwd, target);
5670
- const relative12 = path16__namespace.relative(cwd, absolute);
5671
- if (relative12.startsWith("..") || path16__namespace.isAbsolute(relative12)) return target;
5672
- return relative12.split(path16__namespace.sep).join("/");
5995
+ const resolvedCwd = path16__namespace.resolve(cwd);
5996
+ const absolute = path16__namespace.resolve(resolvedCwd, target);
5997
+ const relative12 = path16__namespace.relative(resolvedCwd, absolute);
5998
+ if (relative12 === "" || relative12 === ".." || relative12.startsWith(`..${path16__namespace.sep}`) || path16__namespace.isAbsolute(relative12)) {
5999
+ return { relative: target.replace(/\\/g, "/"), outsideRoot: true };
6000
+ }
6001
+ return { relative: relative12.split(path16__namespace.sep).join("/"), outsideRoot: false };
5673
6002
  }
5674
6003
  function findRuleMatch(rules, relativePath) {
5675
6004
  return rules.find(
@@ -5696,9 +6025,19 @@ function readManifest2(config) {
5696
6025
  }
5697
6026
  }
5698
6027
  function explainPath(config, target) {
5699
- const relative12 = normalizeTarget(config.cwd, target);
5700
- const absolute = path16__namespace.join(config.cwd, relative12);
6028
+ const normalized = normalizeTarget(config.cwd, target);
6029
+ const relative12 = normalized.relative;
5701
6030
  const maskedPath2 = findSecretInPath(relative12, config.secretRules)?.masked ?? relative12;
6031
+ if (normalized.outsideRoot) {
6032
+ return {
6033
+ path: maskedPath2,
6034
+ exists: false,
6035
+ included: false,
6036
+ reason: "outside-root",
6037
+ detail: "path resolves outside the configured repository root and was not inspected"
6038
+ };
6039
+ }
6040
+ const absolute = path16__namespace.join(config.cwd, relative12);
5702
6041
  const result = {
5703
6042
  path: maskedPath2,
5704
6043
  exists: fs14__namespace.existsSync(absolute),
@@ -5848,6 +6187,178 @@ function renderExplainText(result) {
5848
6187
  );
5849
6188
  return lines.join("\n");
5850
6189
  }
6190
+
6191
+ // src/ast/structured.ts
6192
+ function stripJsonComments2(content) {
6193
+ const text2 = content.replace(/^\uFEFF/, "");
6194
+ const withoutComments = stripComments(text2);
6195
+ return stripTrailingCommas2(withoutComments);
6196
+ }
6197
+ function stripComments(text2) {
6198
+ let out = "";
6199
+ let inString = false;
6200
+ let escaped = false;
6201
+ for (let index = 0; index < text2.length; index += 1) {
6202
+ const char = text2[index];
6203
+ if (inString) {
6204
+ out += char;
6205
+ if (escaped) escaped = false;
6206
+ else if (char === "\\") escaped = true;
6207
+ else if (char === '"') inString = false;
6208
+ continue;
6209
+ }
6210
+ if (char === '"') {
6211
+ inString = true;
6212
+ out += char;
6213
+ continue;
6214
+ }
6215
+ if (char === "/" && text2[index + 1] === "/") {
6216
+ while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
6217
+ index -= 1;
6218
+ continue;
6219
+ }
6220
+ if (char === "/" && text2[index + 1] === "*") {
6221
+ index += 2;
6222
+ while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
6223
+ if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
6224
+ index += 1;
6225
+ }
6226
+ index += 1;
6227
+ continue;
6228
+ }
6229
+ out += char;
6230
+ }
6231
+ return out;
6232
+ }
6233
+ function stripTrailingCommas2(text2) {
6234
+ let out = "";
6235
+ let inString = false;
6236
+ let escaped = false;
6237
+ for (let index = 0; index < text2.length; index += 1) {
6238
+ const char = text2[index];
6239
+ if (inString) {
6240
+ out += char;
6241
+ if (escaped) escaped = false;
6242
+ else if (char === "\\") escaped = true;
6243
+ else if (char === '"') inString = false;
6244
+ continue;
6245
+ }
6246
+ if (char === '"') {
6247
+ inString = true;
6248
+ out += char;
6249
+ continue;
6250
+ }
6251
+ if (char === ",") {
6252
+ let next = index + 1;
6253
+ while (next < text2.length && /\s/.test(text2[next])) next += 1;
6254
+ if (text2[next] === "}" || text2[next] === "]") continue;
6255
+ }
6256
+ out += char;
6257
+ }
6258
+ return out;
6259
+ }
6260
+ function summarizeJson(content) {
6261
+ try {
6262
+ const parsed = JSON.parse(stripJsonComments2(content));
6263
+ if (Array.isArray(parsed)) {
6264
+ const sample = parsed[0];
6265
+ const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
6266
+ return {
6267
+ shape: "array",
6268
+ itemCount: parsed.length,
6269
+ keys: [],
6270
+ sampleKeys
6271
+ };
6272
+ }
6273
+ if (parsed && typeof parsed === "object") {
6274
+ const keys = Object.keys(parsed).sort();
6275
+ return {
6276
+ shape: "object",
6277
+ itemCount: keys.length,
6278
+ keys: keys.slice(0, 30),
6279
+ sampleKeys: []
6280
+ };
6281
+ }
6282
+ return {
6283
+ shape: typeof parsed,
6284
+ itemCount: null,
6285
+ keys: [],
6286
+ sampleKeys: []
6287
+ };
6288
+ } catch {
6289
+ return {
6290
+ shape: "invalid-json",
6291
+ itemCount: null,
6292
+ keys: [],
6293
+ sampleKeys: []
6294
+ };
6295
+ }
6296
+ }
6297
+ function frontMatterEnd(lines) {
6298
+ if (lines.length < 3 || lines[0].trim() !== "---") return 0;
6299
+ for (let index = 1; index < lines.length; index += 1) {
6300
+ const line = lines[index];
6301
+ const candidate = line.trim();
6302
+ if (candidate === "---" || candidate === "...") {
6303
+ return index > 1 ? index : 0;
6304
+ }
6305
+ if (candidate === "") continue;
6306
+ if (/^#(\s|$)/.test(candidate)) continue;
6307
+ if (/^(`{3,}|~{3,})/.test(candidate)) return 0;
6308
+ if (/^#{1,6}\s+\S/.test(candidate)) return 0;
6309
+ const isYamlMapping = /^[A-Za-z0-9_.-]+\s*:(\s|$)/.test(candidate);
6310
+ const isListItem = /^-(?:(\s|$))/.test(candidate);
6311
+ const isIndented = /^[ \t]/.test(line);
6312
+ if (!isYamlMapping && !isListItem && !isIndented) return 0;
6313
+ }
6314
+ return 0;
6315
+ }
6316
+ function summarizeMarkdown(content) {
6317
+ const headings = [];
6318
+ const frontmatterKeys = [];
6319
+ const lines = content.replace(/^\uFEFF/, "").split(/\r\n|\n|\r/);
6320
+ let inFrontmatter = false;
6321
+ let fence = null;
6322
+ const frontMatterEndLine = frontMatterEnd(lines);
6323
+ for (let index = 0; index < lines.length; index++) {
6324
+ const line = lines[index];
6325
+ if (index === 0 && frontMatterEndLine > 0) {
6326
+ inFrontmatter = true;
6327
+ continue;
6328
+ }
6329
+ if (inFrontmatter) {
6330
+ const trimmedFrontmatter = line.trim();
6331
+ if (trimmedFrontmatter === "---" || trimmedFrontmatter === "...") {
6332
+ inFrontmatter = false;
6333
+ continue;
6334
+ }
6335
+ const match = /^([A-Za-z0-9_.-]+)\s*:/.exec(line);
6336
+ if (match) frontmatterKeys.push(match[1]);
6337
+ continue;
6338
+ }
6339
+ const trimmed = line.trim();
6340
+ const fenceMatch = /^(`{3,}|~{3,})(.*)$/.exec(trimmed);
6341
+ if (fenceMatch) {
6342
+ const run = fenceMatch[1];
6343
+ const rest = fenceMatch[2].trim();
6344
+ if (fence === null) {
6345
+ fence = { marker: run[0], length: run.length };
6346
+ } else if (run[0] === fence.marker && run.length >= fence.length && rest === "") {
6347
+ fence = null;
6348
+ }
6349
+ continue;
6350
+ }
6351
+ if (fence !== null) continue;
6352
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(trimmed);
6353
+ if (headingMatch) {
6354
+ headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
6355
+ }
6356
+ }
6357
+ return {
6358
+ headings: headings.slice(0, 40),
6359
+ frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
6360
+ };
6361
+ }
5851
6362
  function appendGitignoreEntries(cwd, entries) {
5852
6363
  const wanted = [...new Set(entries)].filter((entry) => entry.trim().length > 0);
5853
6364
  if (wanted.length === 0) return { added: [], note: null };
@@ -5885,7 +6396,7 @@ function gitignorePath(cwd, filePath) {
5885
6396
 
5886
6397
  // src/version.ts
5887
6398
  var GENERATOR_NAME = "codebase-snapshot";
5888
- var GENERATOR_VERSION = "0.9.12";
6399
+ var GENERATOR_VERSION = "0.9.14";
5889
6400
  var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
5890
6401
  function isOurGeneratorName(name) {
5891
6402
  if (typeof name !== "string") return false;
@@ -6088,6 +6599,24 @@ function displayWidth(line) {
6088
6599
  return width;
6089
6600
  }
6090
6601
  var GUTTER_PATTERN = /^[ \t]*(?:[❯◆◻◼▏✖][ \t]+)*/;
6602
+ function sliceByDisplayWidth(text2, maxCells) {
6603
+ let cells = 0;
6604
+ let lastSpace = -1;
6605
+ let index = 0;
6606
+ for (const character of text2) {
6607
+ const width = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;
6608
+ if (cells + width > maxCells) break;
6609
+ if (character === " ") lastSpace = index;
6610
+ cells += width;
6611
+ index += character.length;
6612
+ }
6613
+ if (index >= text2.length) return { head: text2, tail: "" };
6614
+ if (lastSpace > 0) {
6615
+ return { head: text2.slice(0, lastSpace), tail: text2.slice(lastSpace + 1).trimStart() };
6616
+ }
6617
+ const safeIndex = index > 0 ? index : Array.from(text2)[0]?.length ?? 1;
6618
+ return { head: text2.slice(0, safeIndex), tail: text2.slice(safeIndex).trimStart() };
6619
+ }
6091
6620
  function wrapOneLine(line, limit) {
6092
6621
  if (displayWidth(line) <= limit) return [line];
6093
6622
  const gutter = GUTTER_PATTERN.exec(line)?.[0] ?? "";
@@ -6102,12 +6631,11 @@ function wrapOneLine(line, limit) {
6102
6631
  rows.push(prefix + rest);
6103
6632
  break;
6104
6633
  }
6105
- let cut = room;
6106
- const space = rest.slice(0, room).lastIndexOf(" ");
6107
- if (space > 0) cut = space;
6108
- const taken = rest.slice(0, cut).trimEnd();
6109
- rows.push(`${prefix}${taken.length > 0 ? taken : rest.slice(0, cut)}`);
6110
- rest = rest.slice(cut).trimStart();
6634
+ const { head, tail } = sliceByDisplayWidth(rest, room);
6635
+ rows.push(
6636
+ `${prefix}${head.length > 0 ? head : rest.slice(0, 1)}`
6637
+ );
6638
+ rest = tail;
6111
6639
  prefix = continuation;
6112
6640
  }
6113
6641
  return rows;
@@ -6178,7 +6706,7 @@ function keyReader(input) {
6178
6706
  next() {
6179
6707
  const queued = queue.shift();
6180
6708
  if (queued) return Promise.resolve(queued);
6181
- return new Promise((resolve9) => waiters.push(resolve9));
6709
+ return new Promise((resolve10) => waiters.push(resolve10));
6182
6710
  },
6183
6711
  close() {
6184
6712
  input.off("keypress", onKey);
@@ -6395,17 +6923,25 @@ var CONFIG_CANDIDATES = [
6395
6923
  ".snapshotrc.json",
6396
6924
  ".snapshotrc"
6397
6925
  ];
6398
- function chooseConfigFileName(cwd) {
6399
- if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
6400
- const pkgPath = path16__namespace.join(cwd, "package.json");
6401
- if (!fs14__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
6926
+ function parsePackageJson(raw) {
6927
+ const parsed = JSON.parse(stripJsonComments2(raw));
6928
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
6929
+ throw new Error("package.json is not a JSON object");
6930
+ }
6931
+ return parsed;
6932
+ }
6933
+ function readPackageJson(cwd) {
6402
6934
  try {
6403
- const parsed = JSON.parse(fs14__namespace.readFileSync(pkgPath, "utf8"));
6404
- return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
6935
+ return parsePackageJson(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
6405
6936
  } catch {
6406
- return "snapshot.config.mjs";
6937
+ return null;
6407
6938
  }
6408
6939
  }
6940
+ function chooseConfigFileName(cwd) {
6941
+ if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
6942
+ const parsed = readPackageJson(cwd);
6943
+ return parsed?.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
6944
+ }
6409
6945
  var SIZE_CHOICES = [
6410
6946
  { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
6411
6947
  { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
@@ -6413,12 +6949,9 @@ var SIZE_CHOICES = [
6413
6949
  { value: "custom", label: "Custom\u2026", hint: "type a size like 150k or 1.5mb" }
6414
6950
  ];
6415
6951
  function packageJsonHasConfig(cwd) {
6416
- try {
6417
- const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
6418
- return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
6419
- } catch {
6420
- return false;
6421
- }
6952
+ const parsed = readPackageJson(cwd);
6953
+ if (parsed === null) return false;
6954
+ return typeof parsed.snapshot === "object" && parsed.snapshot !== null && !Array.isArray(parsed.snapshot);
6422
6955
  }
6423
6956
  function artifactAnswers(options) {
6424
6957
  const artifacts = { ...DEFAULT_ANSWERS.artifacts };
@@ -6459,12 +6992,7 @@ function writeFileOrExplain(target, content) {
6459
6992
  function canWriteModuleConfig(name, cwd) {
6460
6993
  if (name.endsWith(".mjs") || name.endsWith(".ts")) return true;
6461
6994
  if (!name.endsWith(".js")) return false;
6462
- try {
6463
- const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
6464
- return parsed.type === "module";
6465
- } catch {
6466
- return false;
6467
- }
6995
+ return readPackageJson(cwd)?.type === "module";
6468
6996
  }
6469
6997
  function configFormatLabel(name) {
6470
6998
  if (name.endsWith(".js")) return 'CommonJS `.js` (package.json has no "type": "module")';
@@ -6492,7 +7020,8 @@ function serializePackageJson(raw, value) {
6492
7020
  const crlf = raw.includes("\r\n");
6493
7021
  const body = JSON.stringify(value, null, spacing);
6494
7022
  const text2 = crlf ? body.replace(/\n/g, "\r\n") : body;
6495
- return `${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
7023
+ const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
7024
+ return `${bom}${text2}${raw.endsWith("\n") ? crlf ? "\r\n" : "\n" : ""}`;
6496
7025
  }
6497
7026
  function updatePackageJson(cwd, notes) {
6498
7027
  const packagePath = path16__namespace.join(cwd, "package.json");
@@ -6508,7 +7037,7 @@ function updatePackageJson(cwd, notes) {
6508
7037
  let parsed;
6509
7038
  try {
6510
7039
  raw = fs14__namespace.readFileSync(packagePath, "utf8");
6511
- parsed = JSON.parse(raw);
7040
+ parsed = parsePackageJson(raw);
6512
7041
  } catch {
6513
7042
  notes.push("Could not parse package.json; npm scripts and the devDependency were not added.");
6514
7043
  return unchanged;
@@ -6920,177 +7449,102 @@ function hasPriorSnapshot(cwd, outDir) {
6920
7449
  } catch {
6921
7450
  return false;
6922
7451
  }
6923
- if (parsed === null || typeof parsed !== "object") return false;
6924
- return isOurGeneratorName(parsed.generator);
6925
- }
6926
-
6927
- // src/ast/structured.ts
6928
- function stripJsonComments2(content) {
6929
- const text2 = content.replace(/^\uFEFF/, "");
6930
- let out = "";
6931
- let inString = false;
6932
- let inLineComment = false;
6933
- let inBlockComment = false;
6934
- for (let i = 0; i < text2.length; i++) {
6935
- const char = text2[i];
6936
- const next = text2[i + 1];
6937
- if (inLineComment) {
6938
- if (char === "\n") {
6939
- inLineComment = false;
6940
- out += char;
6941
- }
6942
- continue;
6943
- }
6944
- if (inBlockComment) {
6945
- if (char === "*" && next === "/") {
6946
- inBlockComment = false;
6947
- i += 1;
6948
- }
6949
- continue;
6950
- }
6951
- if (inString) {
6952
- out += char;
6953
- if (char === "\\") {
6954
- if (next !== void 0) {
6955
- out += next;
6956
- i += 1;
6957
- }
6958
- } else if (char === '"') {
6959
- inString = false;
6960
- }
6961
- continue;
6962
- }
6963
- if (char === '"') {
6964
- inString = true;
6965
- out += char;
6966
- continue;
6967
- }
6968
- if (char === "/" && next === "/") {
6969
- inLineComment = true;
6970
- i += 1;
6971
- continue;
6972
- }
6973
- if (char === "/" && next === "*") {
6974
- inBlockComment = true;
6975
- i += 1;
6976
- continue;
6977
- }
6978
- out += char;
6979
- }
6980
- return out;
6981
- }
6982
- function summarizeJson(content) {
6983
- try {
6984
- const parsed = JSON.parse(stripJsonComments2(content));
6985
- if (Array.isArray(parsed)) {
6986
- const sample = parsed[0];
6987
- const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
6988
- return {
6989
- shape: "array",
6990
- itemCount: parsed.length,
6991
- keys: [],
6992
- sampleKeys
6993
- };
6994
- }
6995
- if (parsed && typeof parsed === "object") {
6996
- const keys = Object.keys(parsed).sort();
6997
- return {
6998
- shape: "object",
6999
- itemCount: keys.length,
7000
- keys: keys.slice(0, 30),
7001
- sampleKeys: []
7002
- };
7003
- }
7004
- return {
7005
- shape: typeof parsed,
7006
- itemCount: null,
7007
- keys: [],
7008
- sampleKeys: []
7009
- };
7010
- } catch {
7011
- return {
7012
- shape: "invalid-json",
7013
- itemCount: null,
7014
- keys: [],
7015
- sampleKeys: []
7016
- };
7017
- }
7018
- }
7019
- function summarizeMarkdown(content) {
7020
- const headings = [];
7021
- const frontmatterKeys = [];
7022
- const lines = content.replace(/^\uFEFF/, "").split("\n");
7023
- let inFrontmatter = false;
7024
- let fence = null;
7025
- let closesFrontmatter = false;
7026
- for (let index = 1; index < lines.length; index++) {
7027
- if (lines[index].trim() === "---") {
7028
- closesFrontmatter = true;
7029
- break;
7030
- }
7031
- }
7032
- for (let index = 0; index < lines.length; index++) {
7033
- const line = lines[index];
7034
- if (index === 0 && line.trim() === "---" && closesFrontmatter) {
7035
- inFrontmatter = true;
7036
- continue;
7037
- }
7038
- if (inFrontmatter) {
7039
- if (line.trim() === "---") {
7040
- inFrontmatter = false;
7041
- continue;
7452
+ if (parsed === null || typeof parsed !== "object") return false;
7453
+ return isOurGeneratorName(parsed.generator);
7454
+ }
7455
+ var hookStdoutScope = new async_hooks.AsyncLocalStorage();
7456
+ var captureScope = new async_hooks.AsyncLocalStorage();
7457
+ var installs = 0;
7458
+ var underlying = null;
7459
+ var router = null;
7460
+ var asText = (chunk) => typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
7461
+ var callbackIn = (rest) => {
7462
+ for (const argument of rest) {
7463
+ if (typeof argument === "function") return argument;
7464
+ }
7465
+ return void 0;
7466
+ };
7467
+ function installStdoutRouter() {
7468
+ if (installs === 0) {
7469
+ underlying = process.stdout.write;
7470
+ const realStdout = underlying.bind(process.stdout);
7471
+ const realStderr = process.stderr.write.bind(process.stderr);
7472
+ router = ((...args) => {
7473
+ if (hookStdoutScope.getStore() === true) return realStderr(...args);
7474
+ const capture = captureScope.getStore();
7475
+ if (capture !== void 0) {
7476
+ capture.write(asText(args[0]));
7477
+ const callback = callbackIn(args.slice(1));
7478
+ if (callback) callback(null);
7479
+ return true;
7042
7480
  }
7043
- const match = /^([a-zA-Z0-9_-]+):/.exec(line);
7044
- if (match) frontmatterKeys.push(match[1]);
7045
- continue;
7046
- }
7047
- const trimmed = line.trim();
7048
- const fenceMatch = /^(`{3,}|~{3,})(.*)$/.exec(trimmed);
7049
- if (fenceMatch) {
7050
- const run = fenceMatch[1];
7051
- const rest = fenceMatch[2].trim();
7052
- if (fence === null) {
7053
- fence = { marker: run[0], length: run.length };
7054
- } else if (run[0] === fence.marker && run.length >= fence.length && rest === "") {
7055
- fence = null;
7481
+ return realStdout(...args);
7482
+ });
7483
+ process.stdout.write = router;
7484
+ }
7485
+ installs += 1;
7486
+ let released = false;
7487
+ return () => {
7488
+ if (released) return;
7489
+ released = true;
7490
+ installs -= 1;
7491
+ if (installs === 0) {
7492
+ if (process.stdout.write === router && underlying !== null) {
7493
+ process.stdout.write = underlying;
7056
7494
  }
7057
- continue;
7058
- }
7059
- if (fence !== null) continue;
7060
- const headingMatch = /^(#{1,6})\s+(.+)$/.exec(trimmed);
7061
- if (headingMatch) {
7062
- headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
7495
+ underlying = null;
7496
+ router = null;
7063
7497
  }
7064
- }
7065
- return {
7066
- headings: headings.slice(0, 40),
7067
- frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
7068
7498
  };
7069
7499
  }
7500
+ async function runWithStdoutCapture(fn) {
7501
+ let text2 = "";
7502
+ const release = installStdoutRouter();
7503
+ try {
7504
+ const value = await captureScope.run(
7505
+ { write: (chunk) => void (text2 += chunk) },
7506
+ fn
7507
+ );
7508
+ return { value, text: text2 };
7509
+ } finally {
7510
+ release();
7511
+ }
7512
+ }
7070
7513
  function hasCollapsibleBody(node) {
7071
7514
  if (!("body" in node)) return false;
7072
7515
  const body = node.body;
7073
7516
  return body !== void 0 && ts__default.default.isBlock(body);
7074
7517
  }
7075
- function isTsxFileName(fileName) {
7076
- return fileName.toLowerCase().endsWith(".tsx");
7518
+ function scriptKindFor(fileName) {
7519
+ const lower = fileName.toLowerCase();
7520
+ if (lower.endsWith(".tsx")) return ts__default.default.ScriptKind.TSX;
7521
+ if (lower.endsWith(".jsx")) return ts__default.default.ScriptKind.JSX;
7522
+ if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs")) {
7523
+ return ts__default.default.ScriptKind.JS;
7524
+ }
7525
+ return ts__default.default.ScriptKind.TS;
7077
7526
  }
7078
7527
  function skeletonizeTypeScript(source, fileName = "file.ts") {
7079
- const scriptKind = isTsxFileName(fileName) ? ts__default.default.ScriptKind.TSX : ts__default.default.ScriptKind.TS;
7080
7528
  const sourceFile = ts__default.default.createSourceFile(
7081
7529
  fileName,
7082
7530
  source,
7083
7531
  ts__default.default.ScriptTarget.Latest,
7084
- true,
7085
- scriptKind
7532
+ // `setParentNodes: false`. Nothing here reads `.parent` — the one `getStart` call is given the
7533
+ // source file explicitly, which is the reason that argument exists. Asking for parent pointers
7534
+ // makes the parser run a second full pass over every node in the file to set them, on top of
7535
+ // the summary parse this same file already pays for.
7536
+ false,
7537
+ scriptKindFor(fileName)
7086
7538
  );
7087
7539
  const spans = [];
7088
7540
  const visit = (node) => {
7089
7541
  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)) {
7090
7542
  const body = node.body;
7091
- const start = body.getStart(sourceFile);
7092
- const end = body.getEnd();
7093
- if (end > start) spans.push({ start, end });
7543
+ if (body.statements.length > 0) {
7544
+ const start = body.getStart(sourceFile);
7545
+ const end = body.getEnd();
7546
+ if (end > start) spans.push({ start, end });
7547
+ }
7094
7548
  }
7095
7549
  ts__default.default.forEachChild(node, visit);
7096
7550
  };
@@ -7102,16 +7556,21 @@ function skeletonizeTypeScript(source, fileName = "file.ts") {
7102
7556
  if (last && span.start < last.end) continue;
7103
7557
  outermost.push(span);
7104
7558
  }
7105
- let text2 = source;
7106
- for (let index = outermost.length - 1; index >= 0; index -= 1) {
7107
- const span = outermost[index];
7108
- text2 = `${text2.slice(0, span.start)}{ /* ... */ }${text2.slice(span.end)}`;
7559
+ const chunks = [];
7560
+ let cursor = 0;
7561
+ for (const span of outermost) {
7562
+ chunks.push(source.slice(cursor, span.start), "{ /* ... */ }");
7563
+ cursor = span.end;
7109
7564
  }
7110
- return { text: text2, collapsed: outermost.length };
7565
+ chunks.push(source.slice(cursor));
7566
+ return { text: chunks.join(""), collapsed: outermost.length };
7111
7567
  }
7112
7568
  function isSkeletonizable(filePath) {
7113
7569
  const lower = filePath.toLowerCase();
7114
- return lower.endsWith(".ts") || lower.endsWith(".tsx") || lower.endsWith(".mts") || lower.endsWith(".cts");
7570
+ if (lower.endsWith(".d.ts") || lower.endsWith(".d.mts") || lower.endsWith(".d.cts")) {
7571
+ return false;
7572
+ }
7573
+ 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");
7115
7574
  }
7116
7575
 
7117
7576
  // src/generator/audit.ts
@@ -7379,23 +7838,55 @@ function truncateWithBudgetMarker(content, maxBytes, budget, originalLines) {
7379
7838
  };
7380
7839
  }
7381
7840
  function partitionFiles(files, maxPartBytes, contentFingerprint) {
7382
- if (files.length === 0) return [];
7383
- const budget = Math.max(MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES, maxPartBytes);
7384
- const sectionBudget = budget - HEADER_RESERVE_BYTES;
7385
- const prepared = files.map((file) => prepareSection(file, sectionBudget));
7386
- let groups = greedyGroups(prepared, budget);
7841
+ return partitionFilesForBuckets([{ files, budgetBytes: maxPartBytes }], contentFingerprint);
7842
+ }
7843
+ function partitionFilesForBuckets(buckets, contentFingerprint) {
7844
+ const groups = [];
7845
+ for (const bucket of buckets) {
7846
+ if (bucket.files.length === 0) continue;
7847
+ const budget = Math.max(
7848
+ MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES,
7849
+ bucket.budgetBytes
7850
+ );
7851
+ const sectionBudget = budget - HEADER_RESERVE_BYTES;
7852
+ const prepared = bucket.files.map((file) => prepareSection(file, sectionBudget));
7853
+ for (const sections of greedyGroups(prepared, budget)) groups.push({ sections, budget });
7854
+ }
7855
+ if (groups.length === 0) return [];
7856
+ const measurements = /* @__PURE__ */ new Map();
7857
+ const widthOf = (total) => Math.max(3, String(total).length);
7858
+ const measure = (group, index, total) => {
7859
+ const cached = measurements.get(group);
7860
+ const width = widthOf(total);
7861
+ if (cached !== void 0 && cached.index === index && cached.width === width) {
7862
+ return cached.size;
7863
+ }
7864
+ const { content } = assembleGroup(group, index, total, contentFingerprint);
7865
+ const size2 = Buffer.byteLength(content, "utf8");
7866
+ measurements.set(group, { index, width, size: size2 });
7867
+ return size2;
7868
+ };
7387
7869
  for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
7388
- const rendered = renderGroups(groups, budget, contentFingerprint);
7389
- const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
7390
- if (oversized === -1) return rendered;
7870
+ const total = groups.length;
7871
+ let oversized = -1;
7872
+ for (let index = 0; index < total; index += 1) {
7873
+ const group2 = groups[index];
7874
+ if (measure(group2, index, total) > group2.budget) {
7875
+ oversized = index;
7876
+ break;
7877
+ }
7878
+ }
7879
+ if (oversized === -1) return renderGroups(groups, contentFingerprint);
7391
7880
  const group = groups[oversized];
7392
- if (group.length > 1) {
7393
- const moved = group.pop();
7394
- groups.splice(oversized + 1, 0, [moved]);
7881
+ if (group.sections.length > 1) {
7882
+ const moved = group.sections.pop();
7883
+ groups.splice(oversized + 1, 0, { sections: [moved], budget: group.budget });
7884
+ measurements.delete(group);
7395
7885
  continue;
7396
7886
  }
7397
- const [section] = group;
7398
- const overflow = rendered[oversized].metadata.byteSize - budget;
7887
+ const [section] = group.sections;
7888
+ if (section === void 0) return renderGroups(groups, contentFingerprint);
7889
+ const overflow = measure(group, oversized, total) - group.budget;
7399
7890
  const currentContent = section.file.content ?? "";
7400
7891
  const shrinkTo = Math.max(
7401
7892
  0,
@@ -7404,26 +7895,33 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
7404
7895
  const shrunk = truncateWithBudgetMarker(
7405
7896
  currentContent,
7406
7897
  shrinkTo,
7407
- budget,
7898
+ group.budget,
7408
7899
  section.file.originalLines
7409
7900
  );
7410
7901
  if (shrunk.isTruncated) {
7411
7902
  applyTruncatedContent(section.file, shrunk.content);
7412
7903
  section.text = renderFileSection(section.file);
7413
7904
  section.byteSize = Buffer.byteLength(section.text, "utf8");
7905
+ measurements.delete(group);
7414
7906
  continue;
7415
7907
  }
7908
+ const rendered = renderGroups(groups, contentFingerprint);
7416
7909
  const marker2 = TRUNCATION_MARKER;
7417
- assertBudgetHoldsMarker(budget);
7910
+ assertBudgetHoldsMarker(group.budget);
7418
7911
  const target = Math.max(
7419
7912
  0,
7420
- budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
7913
+ group.budget - Buffer.byteLength(marker2, "utf8") - TOKEN_LINE_SLACK
7421
7914
  );
7422
7915
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
7423
- rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
7916
+ rendered[oversized] = rehashPart(
7917
+ rendered[oversized],
7918
+ clipped.content + marker2
7919
+ );
7424
7920
  return rendered;
7425
7921
  }
7426
- return enforceBudget(renderGroups(groups, budget, contentFingerprint), budget);
7922
+ return renderGroups(groups, contentFingerprint).flatMap(
7923
+ (part) => enforceBudget([part], part.metadata.budgetBytes)
7924
+ );
7427
7925
  }
7428
7926
  var TRUNCATION_MARKER = "\n... [part truncated: cannot fit budget] ...\n";
7429
7927
  function assertBudgetHoldsMarker(budget) {
@@ -7446,69 +7944,7 @@ function enforceBudget(parts, budget) {
7446
7944
  return rehashPart(part, clipped.content + TRUNCATION_MARKER);
7447
7945
  });
7448
7946
  }
7449
- function renumberParts(parts, contentFingerprint) {
7450
- const total = parts.length;
7451
- if (total === 0) return [];
7452
- const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
7453
- const nameFor = (position) => `part-${zeroPad(position)}.md`;
7454
- const marker2 = "## Contents\n";
7455
- const renumbered = parts.map((part, index) => {
7456
- const markerIndex = part.markdown.indexOf(marker2);
7457
- const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker2.length);
7458
- const included = part.metadata.includedFiles;
7459
- if (markerIndex === -1) {
7460
- throw new Error(`${part.filename} has no "## Contents" section to re-number.`);
7461
- }
7462
- const layerCounts = {};
7463
- const kindCounts = {};
7464
- for (const file of included) {
7465
- layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
7466
- kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
7467
- }
7468
- const pathLines = included.map((file) => displayPath(file.relativePath));
7469
- const listFence = fenceForText(pathLines.join("\n"), 3);
7470
- const header = [
7471
- `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
7472
- "",
7473
- `- Previous: ${index === 0 ? "none" : nameFor(index)}`,
7474
- `- Next: ${index === total - 1 ? "none" : nameFor(index + 2)}`,
7475
- `- Files: ${included.length}`,
7476
- `- Content Fingerprint: ${contentFingerprint || "none"}`,
7477
- `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
7478
- `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
7479
- "",
7480
- UNTRUSTED_CONTENT_NOTICE,
7481
- "",
7482
- "## Files",
7483
- "",
7484
- `${listFence}text`,
7485
- ...pathLines,
7486
- listFence,
7487
- "",
7488
- "## Contents",
7489
- ""
7490
- ].join("\n");
7491
- const assembled = (header + body).replace(/\r\n/g, "\n");
7492
- const estimatedTokens = estimateTokens(assembled);
7493
- const markdown = insertTokenLine(assembled, estimatedTokens);
7494
- const metadata = {
7495
- ...part.metadata,
7496
- index: index + 1,
7497
- filename: nameFor(index + 1),
7498
- byteSize: Buffer.byteLength(markdown, "utf8"),
7499
- estimatedTokens,
7500
- sha256: crypto3__namespace.createHash("sha256").update(markdown).digest("hex"),
7501
- lines: countLines(markdown),
7502
- fileCount: included.length,
7503
- layersCovered: Object.keys(layerCounts).sort()
7504
- };
7505
- return { filename: metadata.filename, markdown, metadata };
7506
- });
7507
- return renumbered.map(
7508
- (part) => enforceBudget([part], part.metadata.budgetBytes)[0] ?? part
7509
- );
7510
- }
7511
- var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
7947
+ var SECTION_TRUNCATION_NOTICE = "... [section truncated: the fixed metadata and a code fence did not fit the part budget] ...";
7512
7948
  var METADATA_VALUE_MAX = 200;
7513
7949
  var DISPLAY_PATH_MAX = 300;
7514
7950
  function capValue(value, max) {
@@ -7550,13 +7986,57 @@ function prepareSection(file, sectionBudget) {
7550
7986
  shrinkBody();
7551
7987
  }
7552
7988
  if (byteSize > sectionBudget) {
7553
- const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
7554
- const truncated = truncateToBytes(text2, Math.max(0, target));
7555
- text2 = truncated.content + SECTION_TRUNCATION_NOTICE;
7989
+ applyTruncatedContent(file, SECTION_TRUNCATION_NOTICE);
7990
+ file.summary = void 0;
7991
+ file.summaryOmitted = true;
7992
+ text2 = renderFileSection(file);
7556
7993
  byteSize = Buffer.byteLength(text2, "utf8");
7994
+ if (byteSize > sectionBudget) {
7995
+ text2 = truncateRenderedSection(text2, sectionBudget, fenceForContent(file.content ?? ""));
7996
+ byteSize = Buffer.byteLength(text2, "utf8");
7997
+ }
7557
7998
  }
7558
7999
  return { file, text: text2, byteSize };
7559
8000
  }
8001
+ function truncateRenderedSection(text2, sectionBudget, fence) {
8002
+ const closing = `
8003
+ ${SECTION_TRUNCATION_NOTICE}
8004
+ ${fence}
8005
+ `;
8006
+ const bodyRoom = sectionBudget - Buffer.byteLength(closing, "utf8");
8007
+ if (bodyRoom > 0) {
8008
+ const cut2 = truncateToBytes(text2, bodyRoom).content;
8009
+ const lastBreak2 = cut2.lastIndexOf("\n");
8010
+ const kept = lastBreak2 >= 0 ? cut2.slice(0, lastBreak2 + 1) : "";
8011
+ if (fenceIsOpen(kept, fence)) return `${kept}${closing}`;
8012
+ }
8013
+ const probe = text2.slice(0, 512);
8014
+ const emptyFence = fenceForText(probe.slice(0, Math.max(0, probe.lastIndexOf("\n") + 1)), 4);
8015
+ const block = `
8016
+ ${emptyFence}
8017
+ ${SECTION_TRUNCATION_NOTICE}
8018
+ ${emptyFence}
8019
+ `;
8020
+ const prefixRoom = sectionBudget - Buffer.byteLength(block, "utf8");
8021
+ const cut = prefixRoom > 0 ? truncateToBytes(text2, prefixRoom).content : "";
8022
+ const lastBreak = cut.lastIndexOf("\n");
8023
+ const prefix = lastBreak >= 0 ? cut.slice(0, lastBreak + 1) : "";
8024
+ return `${dropTrailingOpenFence(prefix, fence)}${block}`;
8025
+ }
8026
+ function dropTrailingOpenFence(text2, fence) {
8027
+ if (!fenceIsOpen(text2, fence)) return text2;
8028
+ const lines = text2.split("\n");
8029
+ const opening = lines.findIndex((line) => line.startsWith(fence));
8030
+ if (opening < 0) return text2;
8031
+ return lines.slice(0, opening).join("\n") + (opening > 0 ? "\n" : "");
8032
+ }
8033
+ function fenceIsOpen(text2, fence) {
8034
+ let open = false;
8035
+ for (const line of text2.split("\n")) {
8036
+ if (line.startsWith(fence)) open = !open;
8037
+ }
8038
+ return open;
8039
+ }
7560
8040
  function applyTruncatedContent(file, content) {
7561
8041
  file.content = content;
7562
8042
  file.isPreviewTruncated = true;
@@ -7604,52 +8084,64 @@ function greedyGroups(sections, budget) {
7604
8084
  if (current.length > 0) groups.push(current);
7605
8085
  return groups;
7606
8086
  }
7607
- function renderGroups(groups, budget, contentFingerprint) {
8087
+ function assembleGroup(group, index, total, contentFingerprint) {
8088
+ const width = Math.max(3, String(total).length);
8089
+ const zeroPad = (value) => String(value).padStart(width, "0");
8090
+ const filename = `part-${zeroPad(index + 1)}.md`;
8091
+ const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
8092
+ const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
8093
+ for (const section of group.sections) {
8094
+ section.file.part = filename;
8095
+ }
8096
+ const layerCounts = {};
8097
+ const kindCounts = {};
8098
+ for (const section of group.sections) {
8099
+ layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
8100
+ kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
8101
+ }
8102
+ const pathLines = group.sections.map((s) => displayPath(s.file.relativePath));
8103
+ const listFence = fenceForText(pathLines.join("\n"), 3);
8104
+ const header = [
8105
+ `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
8106
+ "",
8107
+ `- Previous: ${prev}`,
8108
+ `- Next: ${next}`,
8109
+ `- Files: ${group.sections.length}`,
8110
+ `- Content Fingerprint: ${contentFingerprint || "none"}`,
8111
+ `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
8112
+ `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
8113
+ "",
8114
+ UNTRUSTED_CONTENT_NOTICE,
8115
+ "",
8116
+ "## Files",
8117
+ "",
8118
+ `${listFence}text`,
8119
+ ...pathLines,
8120
+ listFence,
8121
+ "",
8122
+ "## Contents",
8123
+ ""
8124
+ ].join("\n");
8125
+ const assembled = (header + group.sections.map((s) => s.text).join("")).replace(
8126
+ /\r\n|\r/g,
8127
+ "\n"
8128
+ );
8129
+ const estimatedTokens = estimateTokens(assembled);
8130
+ return { filename, content: insertTokenLine(assembled, estimatedTokens), estimatedTokens };
8131
+ }
8132
+ function renderGroups(groups, contentFingerprint) {
7608
8133
  const total = groups.length;
7609
- const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
7610
8134
  return groups.map((group, index) => {
7611
- const filename = `part-${zeroPad(index + 1)}.md`;
7612
- const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
7613
- const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
7614
- for (const section of group) {
7615
- section.file.part = filename;
7616
- }
7617
- const layersCovered = Array.from(new Set(group.map((s) => s.file.layer))).sort();
7618
- const layerCounts = {};
7619
- const kindCounts = {};
7620
- for (const section of group) {
7621
- layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
7622
- kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
7623
- }
7624
- const pathLines = group.map((s) => displayPath(s.file.relativePath));
7625
- const listFence = fenceForText(pathLines.join("\n"), 3);
7626
- const header = [
7627
- `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
7628
- "",
7629
- `- Previous: ${prev}`,
7630
- `- Next: ${next}`,
7631
- `- Files: ${group.length}`,
7632
- `- Content Fingerprint: ${contentFingerprint || "none"}`,
7633
- `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
7634
- `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
7635
- "",
7636
- UNTRUSTED_CONTENT_NOTICE,
7637
- "",
7638
- "## Files",
7639
- "",
7640
- `${listFence}text`,
7641
- ...pathLines,
7642
- listFence,
7643
- "",
7644
- "## Contents",
7645
- ""
7646
- ].join("\n");
7647
- const assembled = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
7648
- const estimatedTokens = estimateTokens(assembled);
7649
- const content = insertTokenLine(assembled, estimatedTokens);
8135
+ const { filename, content, estimatedTokens } = assembleGroup(
8136
+ group,
8137
+ index,
8138
+ total,
8139
+ contentFingerprint
8140
+ );
7650
8141
  const byteSize = Buffer.byteLength(content, "utf8");
7651
8142
  const sha256 = crypto3__namespace.createHash("sha256").update(content).digest("hex");
7652
- const includedFiles = group.map(({ file }) => ({
8143
+ const layersCovered = Array.from(new Set(group.sections.map((s) => s.file.layer))).sort();
8144
+ const includedFiles = group.sections.map(({ file }) => ({
7653
8145
  relativePath: file.relativePath,
7654
8146
  sourceBytes: file.sizeBytes,
7655
8147
  includedBytes: file.includedBytes,
@@ -7662,10 +8154,10 @@ function renderGroups(groups, budget, contentFingerprint) {
7662
8154
  filename,
7663
8155
  byteSize,
7664
8156
  estimatedTokens,
7665
- budgetBytes: budget,
8157
+ budgetBytes: group.budget,
7666
8158
  sha256,
7667
8159
  lines: countLines(content),
7668
- fileCount: group.length,
8160
+ fileCount: group.sections.length,
7669
8161
  layersCovered,
7670
8162
  includedFiles
7671
8163
  };
@@ -7673,7 +8165,7 @@ function renderGroups(groups, budget, contentFingerprint) {
7673
8165
  });
7674
8166
  }
7675
8167
  function emittedBody(content) {
7676
- const normalised = content.replace(/\r\n/g, "\n");
8168
+ const normalised = content.replace(/\r\n|\r/g, "\n");
7677
8169
  return normalised.endsWith("\n") ? normalised.slice(0, -1) : normalised;
7678
8170
  }
7679
8171
  function compactMetadata(file) {
@@ -7741,7 +8233,7 @@ function generateMonolithJson(manifest, filesWithContent) {
7741
8233
  }
7742
8234
  function generateMonolithMarkdown(manifest, filesWithContent) {
7743
8235
  const sections = filesWithContent.map((file) => {
7744
- const content = (file.content ?? "").replace(/\r\n/g, "\n");
8236
+ const content = (file.content ?? "").replace(/\r\n|\r/g, "\n");
7745
8237
  const fence = fenceForContent(content);
7746
8238
  return [
7747
8239
  `## File: ${escapeControls(file.relativePath)}`,
@@ -8001,22 +8493,28 @@ function updateCommand(kind) {
8001
8493
  if (kind === "local") return "npm install codebase-snapshot@latest";
8002
8494
  return "npm install -g codebase-snapshot";
8003
8495
  }
8004
- function versionParts(value) {
8005
- const core = value.trim().replace(/^v/i, "").split("-")[0];
8006
- return core.split(".").map((part) => {
8496
+ function parseSemver(value) {
8497
+ const cleaned = value.trim().replace(/^v/i, "").split("+")[0] ?? "";
8498
+ const dash = cleaned.indexOf("-");
8499
+ const corePart = dash === -1 ? cleaned : cleaned.slice(0, dash);
8500
+ const prerelease = dash === -1 ? null : cleaned.slice(dash + 1);
8501
+ const core = corePart.split(".").map((part) => {
8007
8502
  const parsed = Number(part);
8008
8503
  return Number.isFinite(parsed) ? parsed : 0;
8009
8504
  });
8505
+ return { core, prerelease };
8010
8506
  }
8011
8507
  function isNewer(candidate, current) {
8012
- const left = versionParts(candidate);
8013
- const right = versionParts(current);
8014
- const length = Math.max(left.length, right.length);
8508
+ const left = parseSemver(candidate);
8509
+ const right = parseSemver(current);
8510
+ if (left.prerelease !== null && right.prerelease === null) return false;
8511
+ const length = Math.max(left.core.length, right.core.length);
8015
8512
  for (let index = 0; index < length; index += 1) {
8016
- const a = left[index] ?? 0;
8017
- const b = right[index] ?? 0;
8513
+ const a = left.core[index] ?? 0;
8514
+ const b = right.core[index] ?? 0;
8018
8515
  if (a !== b) return a > b;
8019
8516
  }
8517
+ if (left.prerelease === null && right.prerelease !== null) return true;
8020
8518
  return false;
8021
8519
  }
8022
8520
  function cacheDirectory(env = process.env, platform = process.platform) {
@@ -8084,14 +8582,22 @@ function writeCache(file, entry) {
8084
8582
  }
8085
8583
  }
8086
8584
  function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOUT_MS) {
8087
- return new Promise((resolve9) => {
8585
+ return new Promise((resolve10) => {
8088
8586
  let settled = false;
8587
+ let request = null;
8089
8588
  const finish = (value) => {
8090
8589
  if (settled) return;
8091
8590
  settled = true;
8092
- resolve9(value);
8591
+ clearTimeout(deadline);
8592
+ resolve10(value);
8093
8593
  };
8094
- const request = https__namespace.get(
8594
+ const deadline = setTimeout(() => {
8595
+ request?.destroy();
8596
+ finish(null);
8597
+ }, timeoutMs);
8598
+ deadline.unref();
8599
+ const transport = url.startsWith("http:") ? http__namespace : https__namespace;
8600
+ request = transport.get(
8095
8601
  url,
8096
8602
  {
8097
8603
  headers: {
@@ -8110,7 +8616,7 @@ function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOU
8110
8616
  response.on("data", (chunk) => {
8111
8617
  body += chunk;
8112
8618
  if (body.length > 64 * 1024) {
8113
- request.destroy();
8619
+ request?.destroy();
8114
8620
  finish(null);
8115
8621
  }
8116
8622
  });
@@ -8125,10 +8631,6 @@ function fetchLatestVersion(url = PACKAGE_LATEST_URL, timeoutMs = REQUEST_TIMEOU
8125
8631
  }
8126
8632
  );
8127
8633
  request.on("error", () => finish(null));
8128
- request.setTimeout(timeoutMs, () => {
8129
- request.destroy();
8130
- finish(null);
8131
- });
8132
8634
  });
8133
8635
  }
8134
8636
  async function checkForUpdate(options) {
@@ -8287,7 +8789,10 @@ const { parentPort, workerData } = require("node:worker_threads");
8287
8789
  parentPort.postMessage({ ok: false, message: loaded.errors.join("; ") });
8288
8790
  return;
8289
8791
  }
8290
- const config = mod.resolveConfig(loaded.config, workerData.cliOptions, workerData.cwd);
8792
+ // \`hooks: {}\` is a value, not an absence: the config is applied with the hook list emptied,
8793
+ // so the run is the documented MCP run rather than the CLI run with one setting removed.
8794
+ const base = loaded.config ? { ...loaded.config, hooks: {} } : null;
8795
+ const config = mod.resolveConfig(base, workerData.cliOptions, workerData.cwd);
8291
8796
  const manifest = mod.generateSnapshot(config);
8292
8797
  parentPort.postMessage({
8293
8798
  ok: true,
@@ -8310,14 +8815,15 @@ const { parentPort, workerData } = require("node:worker_threads");
8310
8815
  })();
8311
8816
  `;
8312
8817
  function generateInWorker(cwd, cliOptions) {
8313
- return new Promise((resolve9) => {
8818
+ return new Promise((resolve10) => {
8314
8819
  let settled = false;
8820
+ let worker = null;
8315
8821
  const finish = (result) => {
8316
8822
  if (settled) return;
8317
8823
  settled = true;
8318
- resolve9(result);
8824
+ if (worker !== null) void worker.terminate();
8825
+ resolve10(result);
8319
8826
  };
8320
- let worker;
8321
8827
  const isCjs = typeof __dirname !== "undefined";
8322
8828
  const directory = isCjs ? __dirname : path16__namespace.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
8323
8829
  try {
@@ -8350,7 +8856,100 @@ function generateInWorker(cwd, cliOptions) {
8350
8856
 
8351
8857
  // src/index.ts
8352
8858
  var SCHEMA_VERSION = "3.0.0";
8353
- var SUMMARY_MAX_SOURCE_BYTES = 2 * 1024 * 1024;
8859
+ var SUMMARY_MAX_SOURCE_BYTES2 = SUMMARY_MAX_SOURCE_BYTES;
8860
+ var FILE_DERIVATIONS = /* @__PURE__ */ new Map();
8861
+ var FILE_DERIVATION_LIMIT_BYTES = 64 * 1024 * 1024;
8862
+ var fileDerivationBytes = 0;
8863
+ function takeDerivation(path22, hash, settings) {
8864
+ const cached = FILE_DERIVATIONS.get(path22);
8865
+ if (cached === void 0 || cached.hash !== hash || cached.settings !== settings) return void 0;
8866
+ return cached;
8867
+ }
8868
+ function rememberDerivation(path22, hash, settings, derivation) {
8869
+ const bodyBytes = derivation.finalContent === derivation.redacted ? derivation.redacted.length : derivation.redacted.length + derivation.finalContent.length;
8870
+ const bytes = bodyBytes * 2 + 512;
8871
+ if (bytes > FILE_DERIVATION_LIMIT_BYTES / 4) return;
8872
+ while (fileDerivationBytes + bytes > FILE_DERIVATION_LIMIT_BYTES) {
8873
+ const oldest = FILE_DERIVATIONS.keys().next();
8874
+ if (oldest.done === true) break;
8875
+ const evicted = FILE_DERIVATIONS.get(oldest.value);
8876
+ FILE_DERIVATIONS.delete(oldest.value);
8877
+ if (evicted !== void 0) fileDerivationBytes -= evicted.bytes;
8878
+ }
8879
+ FILE_DERIVATIONS.set(path22, { ...derivation, hash, settings, bytes });
8880
+ fileDerivationBytes += bytes;
8881
+ }
8882
+ function derivationSettings(config) {
8883
+ const rules = config.secretRules.map(
8884
+ (rule) => `${rule.rule}\0${rule.pattern.source}\0${rule.pattern.flags}\0${rule.valueGroup ?? ""}`
8885
+ ).join("");
8886
+ return `${GENERATOR_VERSION}${configDigest(config)}${sha256Hex2(rules)}`;
8887
+ }
8888
+ function deriveFile(relativePath, rawContent, sizeBytes, config) {
8889
+ const lineCount = countLines(rawContent);
8890
+ const { redacted, findings } = scanAndRedactSecrets(rawContent, config.secretRules, relativePath);
8891
+ const isLarge = sizeBytes > config.largeFileBytes;
8892
+ let finalContent = redacted;
8893
+ let isPreviewTruncated = false;
8894
+ let includedLines = lineCount;
8895
+ let inclusionMode = "full";
8896
+ if (config.skeleton === true && isSkeletonizable(relativePath)) {
8897
+ const skeleton = skeletonizeTypeScript(redacted, relativePath);
8898
+ if (skeleton.collapsed > 0) {
8899
+ finalContent = skeleton.text;
8900
+ inclusionMode = "signatures";
8901
+ includedLines = countLines(skeleton.text);
8902
+ }
8903
+ }
8904
+ if (isLarge && inclusionMode !== "signatures") {
8905
+ const preview = truncatePreview(redacted, config.previewHeadLines, config.previewTailLines);
8906
+ finalContent = preview.content;
8907
+ isPreviewTruncated = preview.isTruncated;
8908
+ includedLines = preview.includedLines;
8909
+ inclusionMode = isPreviewTruncated ? "preview" : "full";
8910
+ }
8911
+ let summary;
8912
+ let summaryQuality;
8913
+ let summarySkip = null;
8914
+ let summaryWarning = null;
8915
+ let dependentImports = null;
8916
+ if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES2) {
8917
+ summarySkip = "oversize";
8918
+ dependentImports = [];
8919
+ } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
8920
+ summarySkip = "preview";
8921
+ dependentImports = importsOfText(relativePath, rawContent);
8922
+ } else {
8923
+ try {
8924
+ const outcome = summarizeFile(relativePath, rawContent);
8925
+ summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
8926
+ summaryQuality = outcome?.quality;
8927
+ if (isSourceSummary(summary) && summary.parseErrors) {
8928
+ summaryWarning = `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`;
8929
+ }
8930
+ } catch (err) {
8931
+ summaryWarning = `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
8932
+ /\s+/g,
8933
+ " "
8934
+ );
8935
+ }
8936
+ }
8937
+ return {
8938
+ redacted,
8939
+ findings,
8940
+ lineCount,
8941
+ finalContent,
8942
+ inclusionMode,
8943
+ isPreviewTruncated,
8944
+ includedLines,
8945
+ isLarge,
8946
+ summary,
8947
+ summaryQuality,
8948
+ summarySkip,
8949
+ summaryWarning,
8950
+ dependentImports
8951
+ };
8952
+ }
8354
8953
  function sha256Hex2(input) {
8355
8954
  return crypto3__namespace.createHash("sha256").update(input).digest("hex");
8356
8955
  }
@@ -8894,10 +9493,7 @@ function acquireRunLock(stagingRoot) {
8894
9493
  };
8895
9494
  } catch (err) {
8896
9495
  if (err.code !== "EEXIST") throw err;
8897
- if (attempt === 0 && lockOwnerIsGone(lockPath)) {
8898
- fs14__namespace.rmSync(lockPath, { force: true });
8899
- continue;
8900
- }
9496
+ if (attempt === 0 && reclaimStaleLock(lockPath)) continue;
8901
9497
  throw new Error(
8902
9498
  `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
8903
9499
  );
@@ -8906,14 +9502,25 @@ function acquireRunLock(stagingRoot) {
8906
9502
  throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
8907
9503
  }
8908
9504
  function lockKeyFor(stagingRoot) {
8909
- let key = stagingRoot;
8910
- try {
8911
- key = fs14__namespace.realpathSync(stagingRoot);
8912
- } catch {
8913
- }
9505
+ let key = canonicalLockTarget(stagingRoot);
8914
9506
  if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
8915
9507
  return sha256Hex2(key).slice(0, 16);
8916
9508
  }
9509
+ function canonicalLockTarget(targetPath2) {
9510
+ const resolved = path16__namespace.resolve(targetPath2);
9511
+ let existing = resolved;
9512
+ const tail = [];
9513
+ for (; ; ) {
9514
+ try {
9515
+ return path16__namespace.join(fs14__namespace.realpathSync(existing), ...tail);
9516
+ } catch {
9517
+ const parent = path16__namespace.dirname(existing);
9518
+ if (parent === existing) return resolved;
9519
+ tail.unshift(path16__namespace.basename(existing));
9520
+ existing = parent;
9521
+ }
9522
+ }
9523
+ }
8917
9524
  function lockPathFor(stagingRoot) {
8918
9525
  return path16__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${lockKeyFor(stagingRoot)}.lock`);
8919
9526
  }
@@ -8938,6 +9545,24 @@ function lockOwnerIsGone(lockPath) {
8938
9545
  return err.code === "ESRCH";
8939
9546
  }
8940
9547
  }
9548
+ function reclaimStaleLock(lockPath) {
9549
+ let before;
9550
+ try {
9551
+ before = fs14__namespace.statSync(lockPath);
9552
+ } catch {
9553
+ return true;
9554
+ }
9555
+ if (!lockOwnerIsGone(lockPath)) return false;
9556
+ try {
9557
+ const current = fs14__namespace.statSync(lockPath);
9558
+ const sameFile = current.ino === before.ino && current.birthtimeMs === before.birthtimeMs && current.mtimeMs === before.mtimeMs;
9559
+ if (!sameFile) return false;
9560
+ fs14__namespace.rmSync(lockPath, { force: true });
9561
+ return true;
9562
+ } catch {
9563
+ return false;
9564
+ }
9565
+ }
8941
9566
  function lockFileIsAncient(lockPath) {
8942
9567
  try {
8943
9568
  return Date.now() - fs14__namespace.statSync(lockPath).mtimeMs > STALE_RUN_MS;
@@ -8995,7 +9620,6 @@ function swapOnce(stagingDir, outDir) {
8995
9620
  throw copyErr;
8996
9621
  }
8997
9622
  }
8998
- if (hadPrevious) fs14__namespace.rmSync(backupDir, { recursive: true, force: true });
8999
9623
  } catch (err) {
9000
9624
  if (hadPrevious && fs14__namespace.existsSync(backupDir)) {
9001
9625
  if (fs14__namespace.existsSync(outDir)) fs14__namespace.rmSync(outDir, { recursive: true, force: true });
@@ -9003,6 +9627,16 @@ function swapOnce(stagingDir, outDir) {
9003
9627
  }
9004
9628
  throw err;
9005
9629
  }
9630
+ if (hadPrevious) {
9631
+ try {
9632
+ fs14__namespace.rmSync(backupDir, { recursive: true, force: true });
9633
+ } catch (err) {
9634
+ process.stderr.write(
9635
+ `\u26A0 The previous snapshot could not be removed from ${backupDir} (${err.message}). The new snapshot is in place; delete that directory by hand when convenient.
9636
+ `
9637
+ );
9638
+ }
9639
+ }
9006
9640
  }
9007
9641
  function sleepSync(milliseconds) {
9008
9642
  const buffer = new Int32Array(new SharedArrayBuffer(4));
@@ -9025,22 +9659,18 @@ function artifactWarnings(artifacts, sarif) {
9025
9659
  }
9026
9660
  return messages;
9027
9661
  }
9662
+ function installHookStdoutRedirect() {
9663
+ return installStdoutRouter();
9664
+ }
9028
9665
  function runAfterGenerate(config, manifest) {
9029
9666
  const hook = config.hooks.afterGenerate;
9030
9667
  if (!hook) return null;
9031
- const stdoutWrite = process.stdout.write.bind(process.stdout);
9032
- const stderrWrite = process.stderr.write.bind(process.stderr);
9033
- const hookScope = new async_hooks.AsyncLocalStorage();
9034
- const patchedWrite = ((...args) => hookScope.getStore() === true ? stderrWrite(...args) : stdoutWrite(...args));
9035
- const restoreStdout = () => {
9036
- if (process.stdout.write === patchedWrite) process.stdout.write = stdoutWrite;
9037
- };
9668
+ const releaseRedirect = installHookStdoutRedirect();
9038
9669
  let returned;
9039
9670
  try {
9040
- process.stdout.write = patchedWrite;
9041
- returned = hookScope.run(true, () => hook(manifest));
9671
+ returned = hookStdoutScope.run(true, () => hook(manifest));
9042
9672
  } catch (err) {
9043
- restoreStdout();
9673
+ releaseRedirect();
9044
9674
  const message = maskPathText(err.message, config.secretRules);
9045
9675
  manifest.warnings.push(`hooks.afterGenerate failed: ${message}`);
9046
9676
  manifest.totals.warningCount = manifest.warnings.length;
@@ -9048,10 +9678,10 @@ function runAfterGenerate(config, manifest) {
9048
9678
  }
9049
9679
  const thenable = returned;
9050
9680
  if (!thenable || typeof thenable.then !== "function") {
9051
- restoreStdout();
9681
+ releaseRedirect();
9052
9682
  return null;
9053
9683
  }
9054
- return Promise.resolve(returned).finally(restoreStdout);
9684
+ return Promise.resolve(returned).finally(releaseRedirect);
9055
9685
  }
9056
9686
  function partitionByBudget(files, config, fingerprint) {
9057
9687
  const budgetFor = (file) => config.budgets.byLayer[file.layer] ?? config.budgets.default;
@@ -9062,23 +9692,14 @@ function partitionByBudget(files, config, fingerprint) {
9062
9692
  if (bucket) bucket.push(file);
9063
9693
  else buckets.set(budget, [file]);
9064
9694
  }
9065
- if (buckets.size <= 1) {
9066
- const only = buckets.size === 1 ? Array.from(buckets.keys())[0] : config.maxPartBytes;
9067
- return partitionFiles(files, only, fingerprint);
9695
+ if (buckets.size === 0) return [];
9696
+ if (buckets.size === 1) {
9697
+ return partitionFiles(files, Array.from(buckets.keys())[0], fingerprint);
9068
9698
  }
9069
- const parts = [];
9070
- for (const budget of Array.from(buckets.keys()).sort((a, b) => a - b)) {
9071
- parts.push(...partitionFiles(buckets.get(budget) ?? [], budget, fingerprint));
9072
- }
9073
- const merged = renumberParts(parts, fingerprint);
9074
- const byPath = new Map(files.map((file) => [file.relativePath, file]));
9075
- for (const part of merged) {
9076
- for (const included of part.metadata.includedFiles) {
9077
- const file = byPath.get(included.relativePath);
9078
- if (file) file.part = part.filename;
9079
- }
9080
- }
9081
- return merged;
9699
+ return partitionFilesForBuckets(
9700
+ Array.from(buckets.entries()).sort(([left], [right]) => left - right).map(([budgetBytes, bucketFiles]) => ({ files: bucketFiles, budgetBytes })),
9701
+ fingerprint
9702
+ );
9082
9703
  }
9083
9704
  function generateSnapshot(configInput, options) {
9084
9705
  assertUsableConfigInput(configInput);
@@ -9110,6 +9731,7 @@ function generateSnapshot(configInput, options) {
9110
9731
  return false;
9111
9732
  }) : collection.files;
9112
9733
  const files = [];
9734
+ const derivationKey = derivationSettings(config);
9113
9735
  const monolithBodies = /* @__PURE__ */ new Map();
9114
9736
  const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
9115
9737
  const secretFindings = [];
@@ -9122,6 +9744,7 @@ function generateSnapshot(configInput, options) {
9122
9744
  const fileWarnings = [];
9123
9745
  const oversizeSummaryPaths = [];
9124
9746
  const previewSummaryPaths = [];
9747
+ const skippedSummaryImports = /* @__PURE__ */ new Map();
9125
9748
  const layerCounts = {};
9126
9749
  const kindCounts = {};
9127
9750
  let totalLines = 0;
@@ -9186,8 +9809,6 @@ function generateSnapshot(configInput, options) {
9186
9809
  }
9187
9810
  const hashSha256 = sha256Hex2(buffer);
9188
9811
  const rawContent = decoded.text;
9189
- const lineCount = countLines(rawContent);
9190
- totalLines += lineCount;
9191
9812
  totalSizeBytes += stats.size;
9192
9813
  if (decoded.encoding !== "utf-8") {
9193
9814
  encodingNotes.push({
@@ -9201,66 +9822,25 @@ function generateSnapshot(configInput, options) {
9201
9822
  const language = detectLanguage(relativePath);
9202
9823
  bump(layerCounts, layer2);
9203
9824
  bump(kindCounts, kind);
9204
- const { redacted, findings } = scanAndRedactSecrets(
9205
- rawContent,
9206
- config.secretRules,
9207
- relativePath
9208
- );
9209
- secretFindings.push(...findings);
9210
- const isLarge = stats.size > config.largeFileBytes;
9211
- if (isLarge) largeFileCount += 1;
9212
- let finalContent = redacted;
9213
- let isPreviewTruncated = false;
9214
- let includedLines = lineCount;
9215
- let inclusionMode = "full";
9216
- if (config.skeleton === true && isSkeletonizable(relativePath)) {
9217
- const skeleton = skeletonizeTypeScript(redacted, relativePath);
9218
- if (skeleton.collapsed > 0) {
9219
- finalContent = skeleton.text;
9220
- inclusionMode = "signatures";
9221
- includedLines = countLines(skeleton.text);
9222
- }
9223
- }
9224
- if (isLarge && inclusionMode !== "signatures") {
9225
- const preview = truncatePreview(
9226
- redacted,
9227
- config.previewHeadLines,
9228
- config.previewTailLines
9229
- );
9230
- finalContent = preview.content;
9231
- isPreviewTruncated = preview.isTruncated;
9232
- includedLines = preview.includedLines;
9233
- inclusionMode = isPreviewTruncated ? "preview" : "full";
9234
- }
9235
- if (config.includeMonolith && (isPreviewTruncated || inclusionMode === "signatures")) {
9236
- monolithBodies.set(relativePath, redacted);
9237
- }
9238
- let summary;
9239
- let summaryQuality;
9240
- if (Buffer.byteLength(rawContent, "utf8") > SUMMARY_MAX_SOURCE_BYTES) {
9241
- oversizeSummaryPaths.push(relativePath);
9242
- } else if (isPreviewTruncated && hasStructuralSummary(relativePath)) {
9243
- previewSummaryPaths.push(relativePath);
9244
- } else {
9245
- try {
9246
- const outcome = summarizeFile(relativePath, rawContent);
9247
- summary = outcome?.summary ? maskSummaryStrings(outcome.summary, config.secretRules) : void 0;
9248
- summaryQuality = outcome?.quality;
9249
- if (isSourceSummary(summary) && summary.parseErrors) {
9250
- fileWarnings.push({
9251
- path: relativePath,
9252
- message: `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
9253
- });
9254
- }
9255
- } catch (err) {
9256
- fileWarnings.push({
9257
- path: relativePath,
9258
- message: `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(
9259
- /\s+/g,
9260
- " "
9261
- )
9262
- });
9263
- }
9825
+ const derivation = takeDerivation(absolutePath, hashSha256, derivationKey) ?? (() => {
9826
+ const fresh = deriveFile(relativePath, rawContent, stats.size, config);
9827
+ rememberDerivation(absolutePath, hashSha256, derivationKey, fresh);
9828
+ return fresh;
9829
+ })();
9830
+ const { lineCount } = derivation;
9831
+ totalLines += lineCount;
9832
+ secretFindings.push(...derivation.findings);
9833
+ if (derivation.isLarge) largeFileCount += 1;
9834
+ if (derivation.summarySkip === "oversize") oversizeSummaryPaths.push(relativePath);
9835
+ else if (derivation.summarySkip === "preview") previewSummaryPaths.push(relativePath);
9836
+ if (derivation.dependentImports !== null) {
9837
+ skippedSummaryImports.set(relativePath, derivation.dependentImports);
9838
+ }
9839
+ if (derivation.summaryWarning !== null) {
9840
+ fileWarnings.push({ path: relativePath, message: derivation.summaryWarning });
9841
+ }
9842
+ if (config.includeMonolith && (derivation.isPreviewTruncated || derivation.inclusionMode === "signatures")) {
9843
+ monolithBodies.set(relativePath, derivation.redacted);
9264
9844
  }
9265
9845
  files.push({
9266
9846
  relativePath,
@@ -9271,24 +9851,25 @@ function generateSnapshot(configInput, options) {
9271
9851
  layer: layer2,
9272
9852
  kind,
9273
9853
  language,
9274
- inclusionMode,
9275
- isLarge,
9276
- isPreviewTruncated,
9854
+ inclusionMode: derivation.inclusionMode,
9855
+ isLarge: derivation.isLarge,
9856
+ isPreviewTruncated: derivation.isPreviewTruncated,
9277
9857
  originalLines: lineCount,
9278
- includedLines,
9279
- includedBytes: Buffer.byteLength(finalContent, "utf8"),
9280
- summary,
9281
- summaryQuality,
9858
+ includedLines: derivation.includedLines,
9859
+ includedBytes: Buffer.byteLength(derivation.finalContent, "utf8"),
9860
+ summary: derivation.summary,
9861
+ summaryQuality: derivation.summaryQuality,
9282
9862
  ...decoded.encoding === "utf-8" ? {} : { encoding: decoded.encoding },
9283
- content: finalContent
9863
+ content: derivation.finalContent
9284
9864
  });
9285
9865
  }
9286
9866
  if (changedScope !== null && changedPaths !== null && config.withDependents === true) {
9287
9867
  const keep = new Set(changedPaths);
9288
9868
  const resolver = buildImportResolver(files);
9289
9869
  for (const file of files) {
9290
- if (!isSourceSummary(file.summary)) continue;
9291
- if (importsChangedFile(file.relativePath, file.summary.imports, changedPaths, resolver)) {
9870
+ const fileImports = isSourceSummary(file.summary) ? file.summary.imports : skippedSummaryImports.get(file.relativePath) ?? [];
9871
+ if (fileImports.length === 0) continue;
9872
+ if (importsChangedFile(file.relativePath, fileImports, changedPaths, resolver)) {
9292
9873
  keep.add(file.relativePath);
9293
9874
  }
9294
9875
  }
@@ -9319,6 +9900,9 @@ function generateSnapshot(configInput, options) {
9319
9900
  for (const path22 of [...monolithBodies.keys()]) {
9320
9901
  if (!keep.has(path22)) monolithBodies.delete(path22);
9321
9902
  }
9903
+ for (const path22 of [...skippedSummaryImports.keys()]) {
9904
+ if (!keep.has(path22)) skippedSummaryImports.delete(path22);
9905
+ }
9322
9906
  }
9323
9907
  if (changedScope !== null) {
9324
9908
  for (const relativePath of unchangedPaths) {
@@ -9354,7 +9938,7 @@ function generateSnapshot(configInput, options) {
9354
9938
  const shown = oversizeSummaryPaths.slice(0, ENCODING_PATHS_SHOWN).map((entry) => maskPathText(entry, config.secretRules));
9355
9939
  const hidden = oversizeSummaryPaths.length - ENCODING_PATHS_SHOWN;
9356
9940
  warnings.push(
9357
- `${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.`
9941
+ `${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.`
9358
9942
  );
9359
9943
  }
9360
9944
  for (const [encoding, bucket] of encodingBuckets) {
@@ -9378,11 +9962,13 @@ function generateSnapshot(configInput, options) {
9378
9962
  ).join("\n")
9379
9963
  );
9380
9964
  const fullContents = /* @__PURE__ */ new Map();
9381
- for (const file of files) {
9382
- fullContents.set(
9383
- file.relativePath,
9384
- monolithBodies.get(file.relativePath) ?? file.content ?? ""
9385
- );
9965
+ if (config.includeMonolith) {
9966
+ for (const file of files) {
9967
+ fullContents.set(
9968
+ file.relativePath,
9969
+ monolithBodies.get(file.relativePath) ?? file.content ?? ""
9970
+ );
9971
+ }
9386
9972
  }
9387
9973
  const parts = partitionByBudget(files, config, fingerprint);
9388
9974
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
@@ -9643,7 +10229,7 @@ function generateSnapshot(configInput, options) {
9643
10229
  };
9644
10230
  shipHookState();
9645
10231
  if (pendingHook) {
9646
- pendingHook.then(
10232
+ const tracked = pendingHook.then(
9647
10233
  () => {
9648
10234
  try {
9649
10235
  shipHookState();
@@ -9662,8 +10248,10 @@ function generateSnapshot(configInput, options) {
9662
10248
  } catch {
9663
10249
  }
9664
10250
  }
9665
- );
9666
- pendingHooks.set(outDir, pendingHook);
10251
+ ).finally(() => {
10252
+ if (pendingHooks.get(outDir) === tracked) pendingHooks.delete(outDir);
10253
+ });
10254
+ pendingHooks.set(outDir, tracked);
9667
10255
  }
9668
10256
  return manifest;
9669
10257
  } catch (err) {
@@ -9837,12 +10425,35 @@ async function openWorkspace(root, overrides = {}) {
9837
10425
  }
9838
10426
  cliOptions.maxPartBytes = parsed;
9839
10427
  }
10428
+ if (overrides.maxPartTokens !== void 0) {
10429
+ const parsed = parseByteSize(overrides.maxPartTokens);
10430
+ if (parsed === void 0) {
10431
+ return {
10432
+ ok: false,
10433
+ message: '"maxPartTokens" must be a token count, for example 32000 or "32k".'
10434
+ };
10435
+ }
10436
+ cliOptions.maxPartTokens = parsed;
10437
+ }
9840
10438
  if (overrides.outDir !== void 0) {
9841
10439
  if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
9842
10440
  return { ok: false, message: '"outDir" must be a non-empty string.' };
9843
10441
  }
9844
10442
  cliOptions.outDir = overrides.outDir.trim();
9845
10443
  }
10444
+ if (overrides.changedSince !== void 0) {
10445
+ if (typeof overrides.changedSince !== "string" || overrides.changedSince.trim().length === 0) {
10446
+ return {
10447
+ ok: false,
10448
+ message: '"changedSince" must be a git ref such as "main" or "HEAD~3".'
10449
+ };
10450
+ }
10451
+ cliOptions.changedSince = overrides.changedSince.trim();
10452
+ }
10453
+ if (overrides.skeleton === true) cliOptions.skeleton = true;
10454
+ if (overrides.sarif === true) cliOptions.sarif = true;
10455
+ if (overrides.staged === true) cliOptions.staged = true;
10456
+ if (overrides.withDependents === true) cliOptions.withDependents = true;
9846
10457
  try {
9847
10458
  const config = resolveConfig(loaded.config, cliOptions, root);
9848
10459
  return { ok: true, workspace: { root, config, configFile: loaded.file, cliOptions } };
@@ -9854,6 +10465,16 @@ var manifestCache = /* @__PURE__ */ new Map();
9854
10465
  function manifestPathFor(workspace) {
9855
10466
  return path16__namespace.resolve(workspace.root, workspace.config.outDir, "manifest.json");
9856
10467
  }
10468
+ function partPathFor(workspace, filename) {
10469
+ if (filename.length === 0 || filename.includes("\0")) return null;
10470
+ const partsDir = path16__namespace.resolve(workspace.root, workspace.config.outDir, "parts");
10471
+ const resolved = path16__namespace.resolve(partsDir, filename);
10472
+ const relative12 = path16__namespace.relative(partsDir, resolved);
10473
+ if (relative12.length === 0 || relative12.includes(path16__namespace.sep) || relative12.includes("/") || relative12.includes("\\") || path16__namespace.isAbsolute(relative12)) {
10474
+ return null;
10475
+ }
10476
+ return resolved;
10477
+ }
9857
10478
  function readManifest3(workspace) {
9858
10479
  const file = manifestPathFor(workspace);
9859
10480
  const shown = path16__namespace.relative(workspace.root, file).split(path16__namespace.sep).join("/") || file;
@@ -9924,6 +10545,44 @@ var OUT_DIR_PROPERTY = {
9924
10545
  description: "Snapshot directory, relative to the root. Defaults to the project configuration's outDir."
9925
10546
  };
9926
10547
  var outDirOverride = (args) => ({ outDir: args.outDir });
10548
+ var SCOPE_PROPERTIES = {
10549
+ maxPartTokens: {
10550
+ type: "string",
10551
+ description: 'Token ceiling per part, for example 32000 or "32k". Narrower than maxPartBytes; it converts at four bytes per token.'
10552
+ },
10553
+ skeleton: {
10554
+ type: "boolean",
10555
+ description: "Replace every function body with `{ /* ... */ }`, keeping signatures, imports and exports. Applies to TypeScript, JavaScript and JSX."
10556
+ },
10557
+ sarif: {
10558
+ type: "boolean",
10559
+ description: "Also write reports/audit.sarif beside reports/audit.md, for CI code scanning."
10560
+ },
10561
+ changedSince: {
10562
+ type: "string",
10563
+ description: 'Only files changed since this git ref, for example "main" or "HEAD~3".'
10564
+ },
10565
+ staged: {
10566
+ type: "boolean",
10567
+ description: "Only files staged in the git index."
10568
+ },
10569
+ withDependents: {
10570
+ type: "boolean",
10571
+ description: "With staged or changedSince, also include the files that import the changed ones (one hop)."
10572
+ }
10573
+ };
10574
+ function workspaceOverrides(args) {
10575
+ return {
10576
+ outDir: args.outDir,
10577
+ maxPartBytes: args.maxPartBytes,
10578
+ maxPartTokens: args.maxPartTokens,
10579
+ skeleton: args.skeleton,
10580
+ sarif: args.sarif,
10581
+ changedSince: args.changedSince,
10582
+ staged: args.staged,
10583
+ withDependents: args.withDependents
10584
+ };
10585
+ }
9927
10586
  function failure(message) {
9928
10587
  return { text: message, isError: true };
9929
10588
  }
@@ -9954,12 +10613,13 @@ function summarizeDrift(drift) {
9954
10613
  }
9955
10614
  function toRelativePath(root, value) {
9956
10615
  const cleaned = value.trim();
9957
- if (cleaned.length === 0) return null;
9958
- if (!path16__namespace.isAbsolute(cleaned)) {
9959
- return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
10616
+ if (cleaned.length === 0 || cleaned.includes("\0")) return null;
10617
+ const resolvedRoot = path16__namespace.resolve(root);
10618
+ const absolute = path16__namespace.resolve(resolvedRoot, cleaned);
10619
+ const relative12 = path16__namespace.relative(resolvedRoot, absolute);
10620
+ if (relative12 === "" || relative12 === ".." || relative12.startsWith(`..${path16__namespace.sep}`) || path16__namespace.isAbsolute(relative12)) {
10621
+ return null;
9960
10622
  }
9961
- const relative12 = path16__namespace.relative(root, cleaned);
9962
- if (relative12.startsWith("..") || path16__namespace.isAbsolute(relative12)) return null;
9963
10623
  return relative12.split(path16__namespace.sep).join("/");
9964
10624
  }
9965
10625
  function symbolsOf(entry) {
@@ -10095,7 +10755,8 @@ var planTool = {
10095
10755
  maxPartBytes: {
10096
10756
  type: "string",
10097
10757
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
10098
- }
10758
+ },
10759
+ ...SCOPE_PROPERTIES
10099
10760
  },
10100
10761
  additionalProperties: false
10101
10762
  },
@@ -10112,7 +10773,7 @@ var planTool = {
10112
10773
  },
10113
10774
  annotations: READ_ONLY,
10114
10775
  async handler(args, context) {
10115
- const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes, outDir: args.outDir });
10776
+ const prepared = await prepare(args, context, workspaceOverrides(args));
10116
10777
  if (!prepared.ok) return prepared.outcome;
10117
10778
  const workspace = prepared.workspace;
10118
10779
  let manifest;
@@ -10170,7 +10831,8 @@ var generateTool = {
10170
10831
  maxPartBytes: {
10171
10832
  type: "string",
10172
10833
  description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
10173
- }
10834
+ },
10835
+ ...SCOPE_PROPERTIES
10174
10836
  },
10175
10837
  additionalProperties: false
10176
10838
  },
@@ -10186,10 +10848,7 @@ var generateTool = {
10186
10848
  },
10187
10849
  annotations: WRITES,
10188
10850
  async handler(args, context) {
10189
- const prepared = await prepare(args, context, {
10190
- maxPartBytes: args.maxPartBytes,
10191
- outDir: args.outDir
10192
- });
10851
+ const prepared = await prepare(args, context, workspaceOverrides(args));
10193
10852
  if (!prepared.ok) return prepared.outcome;
10194
10853
  const workspace = prepared.workspace;
10195
10854
  const result = await generateInWorker(workspace.root, workspace.cliOptions);
@@ -10228,7 +10887,7 @@ var validateTool = {
10228
10887
  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.",
10229
10888
  inputSchema: {
10230
10889
  type: "object",
10231
- properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY },
10890
+ properties: { root: ROOT_PROPERTY, outDir: OUT_DIR_PROPERTY, ...SCOPE_PROPERTIES },
10232
10891
  additionalProperties: false
10233
10892
  },
10234
10893
  outputSchema: {
@@ -10240,7 +10899,7 @@ var validateTool = {
10240
10899
  },
10241
10900
  annotations: READ_ONLY,
10242
10901
  async handler(args, context) {
10243
- const prepared = await prepare(args, context, outDirOverride(args));
10902
+ const prepared = await prepare(args, context, workspaceOverrides(args));
10244
10903
  if (!prepared.ok) return prepared.outcome;
10245
10904
  const workspace = prepared.workspace;
10246
10905
  let result;
@@ -10585,7 +11244,12 @@ var readTool = {
10585
11244
  `${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
10586
11245
  );
10587
11246
  }
10588
- const partPath = path16__namespace.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
11247
+ const partPath = partPathFor(workspace, entry.part);
11248
+ if (partPath === null) {
11249
+ return failure(
11250
+ `${entry.part} is not a part of this snapshot: its name leaves ${workspace.config.outDir}/parts. Run snapshot_generate to rebuild it.`
11251
+ );
11252
+ }
10589
11253
  let partText;
10590
11254
  try {
10591
11255
  partText = fs14__namespace.readFileSync(partPath, "utf8");
@@ -10665,7 +11329,8 @@ var explainTool = {
10665
11329
  description: "Path relative to the repository root. Directories are explained as a whole."
10666
11330
  },
10667
11331
  root: ROOT_PROPERTY,
10668
- outDir: OUT_DIR_PROPERTY
11332
+ outDir: OUT_DIR_PROPERTY,
11333
+ ...SCOPE_PROPERTIES
10669
11334
  },
10670
11335
  required: ["path"],
10671
11336
  additionalProperties: false
@@ -10680,7 +11345,7 @@ var explainTool = {
10680
11345
  },
10681
11346
  annotations: READ_ONLY,
10682
11347
  async handler(args, context) {
10683
- const prepared = await prepare(args, context, outDirOverride(args));
11348
+ const prepared = await prepare(args, context, workspaceOverrides(args));
10684
11349
  if (!prepared.ok) return prepared.outcome;
10685
11350
  const workspace = prepared.workspace;
10686
11351
  if (typeof args.path !== "string") return failure('"path" must be a string.');
@@ -11518,7 +12183,7 @@ function buildAgentContract() {
11518
12183
  { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
11519
12184
  { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
11520
12185
  { key: "maxPartTokens", type: "number | string | null", default: null, summary: "Per-part token ceiling; narrows maxPartBytes. Minimum 1024." },
11521
- { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TypeScript files." },
12186
+ { key: "skeleton", type: "boolean", default: false, summary: "Collapse function bodies to `{ /* ... */ }` in TS, JS, TSX and JSX files." },
11522
12187
  { key: "sarif", type: "boolean", default: false, summary: "Also write reports/audit.sarif." },
11523
12188
  { key: "changedSince", type: "string | null", default: null, summary: "Snapshot only files that differ from this git ref." },
11524
12189
  { key: "staged", type: "boolean", default: false, summary: "Snapshot only files staged in the index." },
@@ -12452,9 +13117,9 @@ function buildResourceProvider() {
12452
13117
  const index = Number(partMatch[1]);
12453
13118
  const part = read.manifest.parts.find((candidate) => candidate.index === index);
12454
13119
  if (!part) return null;
12455
- const text2 = readFileIfPresent(
12456
- path16__namespace.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
12457
- );
13120
+ const partPath = partPathFor(workspace, part.filename);
13121
+ if (partPath === null) return null;
13122
+ const text2 = readFileIfPresent(partPath);
12458
13123
  if (text2 === null) return null;
12459
13124
  return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
12460
13125
  }
@@ -12486,11 +13151,11 @@ var MAX_LINE_BYTES = 32 * 1024 * 1024;
12486
13151
  function createWriter(output) {
12487
13152
  return {
12488
13153
  write(value) {
12489
- return new Promise((resolve9, reject) => {
13154
+ return new Promise((resolve10, reject) => {
12490
13155
  output.write(`${JSON.stringify(value)}
12491
13156
  `, (error) => {
12492
13157
  if (error) reject(error);
12493
- else resolve9();
13158
+ else resolve10();
12494
13159
  });
12495
13160
  });
12496
13161
  }
@@ -12498,6 +13163,7 @@ function createWriter(output) {
12498
13163
  }
12499
13164
  function createLineReader(input, options) {
12500
13165
  let buffer = "";
13166
+ let bufferBytes = 0;
12501
13167
  let discarding = false;
12502
13168
  let discardedBytes = 0;
12503
13169
  const deliver = (line) => {
@@ -12510,6 +13176,13 @@ function createLineReader(input, options) {
12510
13176
  if (trimmed.trim().length === 0) return;
12511
13177
  options.onLine(trimmed);
12512
13178
  };
13179
+ const discardBuffered = () => {
13180
+ discarding = true;
13181
+ discardedBytes += bufferBytes;
13182
+ buffer = "";
13183
+ bufferBytes = 0;
13184
+ options.onOverflow?.(discardedBytes);
13185
+ };
12513
13186
  const drain = () => {
12514
13187
  let index = buffer.indexOf("\n");
12515
13188
  while (index >= 0) {
@@ -12517,42 +13190,57 @@ function createLineReader(input, options) {
12517
13190
  buffer = buffer.slice(index + 1);
12518
13191
  if (discarding) {
12519
13192
  deliver(line);
12520
- } else if (line.length > MAX_LINE_BYTES) {
12521
- options.onOverflow?.(line.length);
12522
13193
  } else {
12523
- deliver(line);
13194
+ const lineBytes = Buffer.byteLength(line, "utf8");
13195
+ if (lineBytes > MAX_LINE_BYTES) options.onOverflow?.(lineBytes);
13196
+ else deliver(line);
12524
13197
  }
12525
13198
  index = buffer.indexOf("\n");
12526
13199
  }
13200
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
13201
+ if (!discarding && bufferBytes > MAX_LINE_BYTES) discardBuffered();
12527
13202
  };
12528
13203
  const onData = (chunk) => {
13204
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
12529
13205
  if (discarding) {
12530
13206
  const index = chunk.indexOf("\n");
12531
13207
  if (index < 0) {
12532
- discardedBytes += chunk.length;
13208
+ discardedBytes += chunkBytes;
12533
13209
  return;
12534
13210
  }
12535
13211
  buffer = chunk.slice(index + 1);
13212
+ bufferBytes = Buffer.byteLength(buffer, "utf8");
12536
13213
  deliver("");
12537
13214
  drain();
12538
13215
  return;
12539
13216
  }
12540
- buffer += chunk;
12541
- drain();
12542
- if (buffer.length > MAX_LINE_BYTES) {
13217
+ if (chunk.indexOf("\n") < 0 && bufferBytes + chunkBytes > MAX_LINE_BYTES) {
12543
13218
  discarding = true;
12544
- discardedBytes = buffer.length;
13219
+ discardedBytes = bufferBytes + chunkBytes;
12545
13220
  buffer = "";
13221
+ bufferBytes = 0;
12546
13222
  options.onOverflow?.(discardedBytes);
12547
13223
  return;
12548
13224
  }
13225
+ buffer += chunk;
13226
+ drain();
12549
13227
  };
12550
13228
  const onEnd = () => {
12551
- if (buffer.length > 0) {
12552
- const rest = buffer;
12553
- buffer = "";
12554
- deliver(rest);
13229
+ const rest = buffer;
13230
+ const restBytes = bufferBytes || Buffer.byteLength(rest, "utf8");
13231
+ buffer = "";
13232
+ bufferBytes = 0;
13233
+ if (rest.length === 0) return;
13234
+ if (discarding) {
13235
+ discarding = false;
13236
+ discardedBytes = 0;
13237
+ return;
12555
13238
  }
13239
+ if (restBytes > MAX_LINE_BYTES) {
13240
+ options.onOverflow?.(restBytes);
13241
+ return;
13242
+ }
13243
+ deliver(rest);
12556
13244
  };
12557
13245
  input.setEncoding("utf8");
12558
13246
  input.on("data", onData);
@@ -12603,8 +13291,8 @@ async function runStdioServer(options) {
12603
13291
  let inFlight = 0;
12604
13292
  let inputEnded = false;
12605
13293
  let resolveDone = () => void 0;
12606
- const finished = new Promise((resolve9) => {
12607
- resolveDone = resolve9;
13294
+ const finished = new Promise((resolve10) => {
13295
+ resolveDone = resolve10;
12608
13296
  });
12609
13297
  const finishIfIdle = () => {
12610
13298
  if (inputEnded && inFlight === 0) resolveDone(0);
@@ -13128,7 +13816,9 @@ function nextStepHint(cwd, scriptsAddedNow) {
13128
13816
  let hasScript = scriptsAddedNow;
13129
13817
  if (!hasScript) {
13130
13818
  try {
13131
- const parsed = JSON.parse(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"));
13819
+ const parsed = JSON.parse(
13820
+ stripJsonComments2(fs14__namespace.readFileSync(path16__namespace.join(cwd, "package.json"), "utf8"))
13821
+ );
13132
13822
  hasScript = typeof parsed.scripts?.snapshot === "string";
13133
13823
  } catch {
13134
13824
  hasScript = false;
@@ -13230,18 +13920,8 @@ function installDependencies(cwd) {
13230
13920
  return { manager: manager.name, command, ok: true, detail: seconds };
13231
13921
  }
13232
13922
  async function captureStdout(fn) {
13233
- const original = process.stdout.write.bind(process.stdout);
13234
- let text2 = "";
13235
- process.stdout.write = ((chunk) => {
13236
- text2 += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
13237
- return true;
13238
- });
13239
- try {
13240
- const code = await fn();
13241
- return { code, text: text2 };
13242
- } finally {
13243
- process.stdout.write = original;
13244
- }
13923
+ const { value, text: text2 } = await runWithStdoutCapture(fn);
13924
+ return { code: value, text: text2 };
13245
13925
  }
13246
13926
  async function runSetupCommand(invocation, options = {}) {
13247
13927
  try {
@@ -13356,31 +14036,33 @@ async function runSetupCommand(invocation, options = {}) {
13356
14036
  if (invocation.watch) generateArgs.push("--watch");
13357
14037
  if (invocation.format === "json") {
13358
14038
  const generated = await captureStdout(() => main(generateArgs));
13359
- let manifest = null;
14039
+ let parsed = null;
13360
14040
  try {
13361
- manifest = JSON.parse(generated.text);
14041
+ parsed = JSON.parse(generated.text);
13362
14042
  } catch {
13363
- manifest = null;
14043
+ parsed = null;
14044
+ }
14045
+ const setup = {
14046
+ configPath: init.configPath,
14047
+ configReused: init.reused,
14048
+ preset: init.preset,
14049
+ scripts: init.scripts,
14050
+ dependency: init.dependency,
14051
+ gitignoreEntries: init.gitignoreEntries,
14052
+ install,
14053
+ notes: init.notes,
14054
+ hook
14055
+ };
14056
+ if (generated.code !== 0) {
14057
+ const inner = parsed && typeof parsed.error === "object" && parsed.error !== null ? parsed.error : { message: "Snapshot generation failed during setup.", exitCode: generated.code };
14058
+ process.stdout.write(
14059
+ `${JSON.stringify({ error: inner, setup, manifest: null }, null, 2)}
14060
+ `
14061
+ );
14062
+ return generated.code;
13364
14063
  }
13365
14064
  process.stdout.write(
13366
- `${JSON.stringify(
13367
- {
13368
- setup: {
13369
- configPath: init.configPath,
13370
- configReused: init.reused,
13371
- preset: init.preset,
13372
- scripts: init.scripts,
13373
- dependency: init.dependency,
13374
- gitignoreEntries: init.gitignoreEntries,
13375
- install,
13376
- notes: init.notes,
13377
- hook
13378
- },
13379
- manifest
13380
- },
13381
- null,
13382
- 2
13383
- )}
14065
+ `${JSON.stringify({ setup, manifest: parsed }, null, 2)}
13384
14066
  `
13385
14067
  );
13386
14068
  return generated.code;
@@ -13573,6 +14255,10 @@ ${USAGE}
13573
14255
  largeFileBytes: invocation.largeFileBytes,
13574
14256
  maxMonolithBytes: invocation.maxMonolithBytes,
13575
14257
  skipArtifacts: invocation.skipArtifacts,
14258
+ // `--only-artifacts` is forwarded as itself, not only as the derived suppression list:
14259
+ // suppression cannot switch an artifact *on*, so a config that had turned one of the
14260
+ // requested artifacts off left the run with nothing to write.
14261
+ onlyArtifacts: invocation.onlyArtifacts,
13576
14262
  discovery: invocation.discovery,
13577
14263
  changedSince: invocation.changedSince,
13578
14264
  staged: invocation.staged,
@@ -13584,19 +14270,6 @@ ${USAGE}
13584
14270
  );
13585
14271
  if (invocation.command === "validate") {
13586
14272
  const result = validateSnapshot(resolved);
13587
- if (result.valid) {
13588
- const manifestPath = path16__namespace.resolve(resolved.cwd, resolved.outDir, "manifest.json");
13589
- let manifest;
13590
- try {
13591
- manifest = JSON.parse(fs14__namespace.readFileSync(manifestPath, "utf8"));
13592
- } catch {
13593
- manifest = void 0;
13594
- }
13595
- const shapeError = manifest === void 0 ? null : describeManifestShapeProblem(manifest);
13596
- if (shapeError) {
13597
- return fail(invocation.format, `${manifestPath} is not a snapshot manifest: ${shapeError}`);
13598
- }
13599
- }
13600
14273
  emit(invocation.format, result, () => renderValidateText(result));
13601
14274
  return result.valid ? 0 : 1;
13602
14275
  }
@@ -13791,10 +14464,13 @@ function isGeneratedWatchPath(candidate, paths) {
13791
14464
  const outDir = comparablePath(path16__namespace.resolve(paths.cwd, paths.outDir));
13792
14465
  const tmpDir = comparablePath(path16__namespace.resolve(paths.cwd, paths.tmpDir));
13793
14466
  if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
13794
- const parent = path16__namespace.dirname(absolute);
13795
- if (parent === path16__namespace.dirname(outDir) && path16__namespace.basename(absolute).startsWith(`${path16__namespace.basename(outDir)}.backup-`)) {
13796
- return true;
13797
- }
14467
+ const offsets = [path16__namespace.basename(outDir), path16__namespace.basename(tmpDir)].filter((name) => name.length > 0);
14468
+ const leftover = absolute.split(/[\\/]+/).some(
14469
+ (segment) => offsets.some(
14470
+ (name) => segment.startsWith(`${name}.backup-`) || segment.startsWith(`${name}.partial-`)
14471
+ )
14472
+ );
14473
+ if (leftover) return true;
13798
14474
  return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
13799
14475
  }
13800
14476
  function decideWatchEvent(state, event, paths) {
@@ -13869,6 +14545,7 @@ async function runWatchLoop(invocation, resolved, options) {
13869
14545
  largeFileBytes: invocation.largeFileBytes,
13870
14546
  maxMonolithBytes: invocation.maxMonolithBytes,
13871
14547
  skipArtifacts: invocation.skipArtifacts,
14548
+ onlyArtifacts: invocation.onlyArtifacts,
13872
14549
  discovery: invocation.discovery,
13873
14550
  changedSince: invocation.changedSince,
13874
14551
  staged: invocation.staged,
@@ -13925,12 +14602,12 @@ async function runWatchLoop(invocation, resolved, options) {
13925
14602
  info(`\u26A0 Watch error: ${err.message}`);
13926
14603
  info(" The watcher may have missed changes. Restart with Ctrl+C if they stop arriving.");
13927
14604
  });
13928
- await new Promise((resolve9) => {
14605
+ await new Promise((resolve10) => {
13929
14606
  const stop = () => {
13930
14607
  stopped = true;
13931
14608
  if (timer) clearTimeout(timer);
13932
14609
  watcher.close();
13933
- resolve9();
14610
+ resolve10();
13934
14611
  };
13935
14612
  process.once("SIGINT", stop);
13936
14613
  process.once("SIGTERM", stop);