tamperward 2.16.5 → 2.17.1

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.
Files changed (3) hide show
  1. package/README.md +16 -6
  2. package/dist/cli/index.js +700 -133
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1385,6 +1385,7 @@ var lintSuppression = {
1385
1385
  };
1386
1386
 
1387
1387
  // src/detectors/test-skip.ts
1388
+ import ts2 from "typescript";
1388
1389
  var RULE3 = "test-skip";
1389
1390
  var acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
1390
1391
  var JS_RUNNER = "\\b(?:it|test|describe|suite)";
@@ -1393,10 +1394,10 @@ var PATTERNS2 = {
1393
1394
  js: [
1394
1395
  // The skip/only/todo family on a runner, by dot-or-bracket access, with one optional
1395
1396
  // vitest concurrency modifier hop: test.skip, test . skip, test['skip'], it.concurrent.skip.
1396
- { re: new RegExp(JS_RUNNER + acc(JS_MOD) + "?" + acc("skip|only|todo")), why: "a .skip/.only/.todo marker" },
1397
- { re: new RegExp(JS_RUNNER + acc("skipIf|runIf") + "\\s*\\("), why: "a .skipIf()/.runIf() condition (the test runs only when the condition allows)" },
1398
- { re: new RegExp(JS_RUNNER + acc("fails|failing") + "\\s*\\("), why: "a .fails/.failing marker (the test now passes by failing)" },
1399
- { re: /\b(?:it|test|describe)\.each\s*\(\s*\[\s*\]\s*\)/, why: "an empty .each table (no case ever runs)" },
1397
+ { re: new RegExp(JS_RUNNER + acc(JS_MOD) + "?" + acc("skip|only|todo")), why: "a .skip/.only/.todo marker", astOwned: true },
1398
+ { re: new RegExp(JS_RUNNER + acc("skipIf|runIf") + "\\s*\\("), why: "a .skipIf()/.runIf() condition (the test runs only when the condition allows)", astOwned: true },
1399
+ { re: new RegExp(JS_RUNNER + acc("fails|failing") + "\\s*\\("), why: "a .fails/.failing marker (the test now passes by failing)", astOwned: true },
1400
+ { re: /\b(?:it|test|describe)\.each\s*\(\s*\[\s*\]\s*\)/, why: "an empty .each table (no case ever runs)", astOwned: true },
1400
1401
  { re: /\b(?:xit|xtest|xdescribe)\s*\(/, why: "an x-prefixed disabled test" },
1401
1402
  { re: /\b(?:fit|ftest|fdescribe)\s*\(/, why: "an f-prefixed focused test (narrows the suite)" },
1402
1403
  { re: /\bpending\(\s*\)/, why: "a pending() marker" },
@@ -1406,7 +1407,7 @@ var PATTERNS2 = {
1406
1407
  // In options position (right after the title) ANY value but false/0 skips — a
1407
1408
  // non-literal `{ skip: process.env.CI }` included. Elsewhere on a line only the
1408
1409
  // literal forms count: `list({ skip: 1, take: 5 })` is pagination.
1409
- { re: /\b(?:it|test|describe|suite)\s*\(\s*(['"`])(?:(?!\1).)*\1\s*,\s*\{[^}]*?\bskip:(?!\s*(?:false|0)\b)/, why: "a { skip: ... } option disabling the test (node:test options form)" },
1410
+ { re: /\b(?:it|test|describe|suite)\s*\(\s*(['"`])(?:(?!\1).)*\1\s*,\s*\{[^}]*?\bskip:(?!\s*(?:false|0)\b)/, why: "a { skip: ... } option disabling the test (node:test options form)", astOwned: true },
1410
1411
  { re: /\bskip:\s*(?:true|['"`])/, why: "a { skip: ... } option disabling the test (node:test options form)" },
1411
1412
  { re: /\btodo:\s*(?:true|['"`])/, why: "a { todo: ... } option (the test no longer fails the run)" },
1412
1413
  { re: new RegExp("\\b(?:t|ctx|context|this)" + acc("skip") + "\\s*\\("), why: "a runtime t.skip()/this.skip() call" }
@@ -1477,6 +1478,343 @@ function matchesOutsideString(p, content, lang) {
1477
1478
  }
1478
1479
  return false;
1479
1480
  }
1481
+ var JS_RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
1482
+ var JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle"]);
1483
+ var JS_TEST_MODULES = /* @__PURE__ */ new Set([
1484
+ "vitest",
1485
+ "@jest/globals",
1486
+ "node:test",
1487
+ "node:test/promises",
1488
+ "mocha",
1489
+ "bun:test",
1490
+ "@playwright/test"
1491
+ ]);
1492
+ function scriptKind(path) {
1493
+ if (/\.tsx$/i.test(path)) return ts2.ScriptKind.TSX;
1494
+ if (/\.jsx$/i.test(path)) return ts2.ScriptKind.JSX;
1495
+ if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts2.ScriptKind.JS;
1496
+ return ts2.ScriptKind.TS;
1497
+ }
1498
+ function addedLineNumbers(c) {
1499
+ const out2 = /* @__PURE__ */ new Set();
1500
+ for (const h of c.hunks) {
1501
+ for (const line of h.lines) {
1502
+ if (line.type === "add" && line.newLine != null) out2.add(line.newLine);
1503
+ }
1504
+ }
1505
+ return out2;
1506
+ }
1507
+ function moduleName(expr) {
1508
+ return ts2.isStringLiteralLike(expr) ? expr.text : null;
1509
+ }
1510
+ function requiredModule(expr) {
1511
+ if (!ts2.isCallExpression(expr) || !ts2.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1512
+ return moduleName(expr.arguments[0]);
1513
+ }
1514
+ function astContext(path, source) {
1515
+ const rootName = path.startsWith("/") ? path : "/tamperward/" + path;
1516
+ const sf = ts2.createSourceFile(
1517
+ rootName,
1518
+ source,
1519
+ ts2.ScriptTarget.Latest,
1520
+ true,
1521
+ scriptKind(path)
1522
+ );
1523
+ const diagnostics = sf.parseDiagnostics ?? [];
1524
+ if (diagnostics.length > 0) return null;
1525
+ const options = {
1526
+ allowJs: true,
1527
+ checkJs: false,
1528
+ noLib: true,
1529
+ noResolve: true,
1530
+ target: ts2.ScriptTarget.Latest,
1531
+ module: ts2.ModuleKind.ESNext
1532
+ };
1533
+ const host = ts2.createCompilerHost(options, true);
1534
+ host.getSourceFile = (fileName) => fileName === rootName ? sf : void 0;
1535
+ host.fileExists = (fileName) => fileName === rootName;
1536
+ host.readFile = (fileName) => fileName === rootName ? source : void 0;
1537
+ host.writeFile = () => {
1538
+ };
1539
+ host.getDefaultLibFileName = () => "";
1540
+ host.getCurrentDirectory = () => "/tamperward";
1541
+ host.getCanonicalFileName = (fileName) => fileName;
1542
+ host.useCaseSensitiveFileNames = () => true;
1543
+ host.getNewLine = () => "\n";
1544
+ const program = ts2.createProgram([rootName], options, host);
1545
+ return { sf, checker: program.getTypeChecker() };
1546
+ }
1547
+ function symbolAt(ctx, node) {
1548
+ return ctx.checker.getSymbolAtLocation(node) ?? null;
1549
+ }
1550
+ function importAliases(ctx) {
1551
+ const out2 = /* @__PURE__ */ new Map();
1552
+ const simpleAliases = [];
1553
+ const add = (id, causes = [id]) => {
1554
+ const sym = symbolAt(ctx, id);
1555
+ if (sym) out2.set(sym, { causes });
1556
+ };
1557
+ for (const stmt of ctx.sf.statements) {
1558
+ if (ts2.isImportDeclaration(stmt) && ts2.isStringLiteralLike(stmt.moduleSpecifier)) {
1559
+ if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
1560
+ const clause = stmt.importClause;
1561
+ if (!clause) continue;
1562
+ if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
1563
+ const bindings = clause.namedBindings;
1564
+ if (bindings && ts2.isNamedImports(bindings)) {
1565
+ for (const el of bindings.elements) {
1566
+ const imported = el.propertyName?.text ?? el.name.text;
1567
+ if (JS_RUNNERS.has(imported)) add(el.name);
1568
+ }
1569
+ }
1570
+ continue;
1571
+ }
1572
+ if (!ts2.isVariableStatement(stmt)) continue;
1573
+ for (const decl of stmt.declarationList.declarations) {
1574
+ if (ts2.isObjectBindingPattern(decl.name) && decl.initializer) {
1575
+ const mod = requiredModule(decl.initializer);
1576
+ if (!mod || !JS_TEST_MODULES.has(mod)) continue;
1577
+ for (const el of decl.name.elements) {
1578
+ if (!ts2.isIdentifier(el.name)) continue;
1579
+ const imported = el.propertyName && ts2.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1580
+ if (JS_RUNNERS.has(imported)) add(el.name);
1581
+ }
1582
+ } else if (ts2.isIdentifier(decl.name) && decl.initializer && ts2.isIdentifier(decl.initializer)) {
1583
+ simpleAliases.push({ local: decl.name, source: decl.initializer });
1584
+ }
1585
+ }
1586
+ }
1587
+ for (let pass = 0; pass < simpleAliases.length + 1; pass++) {
1588
+ let changed = false;
1589
+ for (const alias of simpleAliases) {
1590
+ const localSym = symbolAt(ctx, alias.local);
1591
+ if (!localSym || out2.has(localSym)) continue;
1592
+ const sourceSym = symbolAt(ctx, alias.source);
1593
+ const sourceBinding = sourceSym ? out2.get(sourceSym) : void 0;
1594
+ const implicitGlobal = sourceSym == null && JS_RUNNERS.has(alias.source.text);
1595
+ if (!sourceBinding && !implicitGlobal) continue;
1596
+ out2.set(localSym, {
1597
+ causes: [alias.local, ...sourceBinding?.causes ?? []]
1598
+ });
1599
+ changed = true;
1600
+ }
1601
+ if (!changed) break;
1602
+ }
1603
+ return out2;
1604
+ }
1605
+ function topLevelStaticStrings(ctx) {
1606
+ const pending = /* @__PURE__ */ new Map();
1607
+ for (const stmt of ctx.sf.statements) {
1608
+ if (!ts2.isVariableStatement(stmt)) continue;
1609
+ const isConst = (stmt.declarationList.flags & ts2.NodeFlags.Const) !== 0;
1610
+ if (!isConst) continue;
1611
+ for (const decl of stmt.declarationList.declarations) {
1612
+ if (!ts2.isIdentifier(decl.name) || !decl.initializer) continue;
1613
+ const sym = symbolAt(ctx, decl.name);
1614
+ if (sym) pending.set(sym, { decl: decl.name, expr: decl.initializer });
1615
+ }
1616
+ }
1617
+ const resolved = /* @__PURE__ */ new Map();
1618
+ const resolving = /* @__PURE__ */ new Set();
1619
+ const valueOf = (expr) => {
1620
+ if (ts2.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
1621
+ if (ts2.isParenthesizedExpression(expr)) return valueOf(expr.expression);
1622
+ if (ts2.isBinaryExpression(expr) && expr.operatorToken.kind === ts2.SyntaxKind.PlusToken) {
1623
+ const left = valueOf(expr.left);
1624
+ const right = valueOf(expr.right);
1625
+ if (!left || !right) return null;
1626
+ return { value: left.value + right.value, causes: [...left.causes, ...right.causes] };
1627
+ }
1628
+ if (ts2.isIdentifier(expr)) {
1629
+ const sym = symbolAt(ctx, expr);
1630
+ if (!sym) return null;
1631
+ const cached = resolved.get(sym);
1632
+ if (cached) return cached;
1633
+ const item = pending.get(sym);
1634
+ if (!item || resolving.has(sym)) return null;
1635
+ resolving.add(sym);
1636
+ const nested = valueOf(item.expr);
1637
+ resolving.delete(sym);
1638
+ if (!nested) return null;
1639
+ const result = { value: nested.value, causes: [item.decl, ...nested.causes] };
1640
+ resolved.set(sym, result);
1641
+ return result;
1642
+ }
1643
+ return null;
1644
+ };
1645
+ for (const [sym, item] of pending) {
1646
+ if (resolved.has(sym)) continue;
1647
+ resolving.add(sym);
1648
+ const nested = valueOf(item.expr);
1649
+ resolving.delete(sym);
1650
+ if (nested) resolved.set(sym, { value: nested.value, causes: [item.decl, ...nested.causes] });
1651
+ }
1652
+ return resolved;
1653
+ }
1654
+ function staticPropertyName(expr, ctx, strings) {
1655
+ if (ts2.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
1656
+ if (ts2.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
1657
+ if (ts2.isBinaryExpression(expr) && expr.operatorToken.kind === ts2.SyntaxKind.PlusToken) {
1658
+ const left = staticPropertyName(expr.left, ctx, strings);
1659
+ const right = staticPropertyName(expr.right, ctx, strings);
1660
+ if (!left || !right) return null;
1661
+ return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
1662
+ }
1663
+ if (ts2.isIdentifier(expr)) {
1664
+ const sym = symbolAt(ctx, expr);
1665
+ if (!sym) return null;
1666
+ const value = strings.get(sym);
1667
+ return value ? { name: value.value, causes: value.causes } : null;
1668
+ }
1669
+ return null;
1670
+ }
1671
+ function runnerChain(expr, ctx, runners, strings) {
1672
+ const props = [];
1673
+ const causes = [];
1674
+ let cur = expr;
1675
+ let terminalNode = expr;
1676
+ while (ts2.isPropertyAccessExpression(cur) || ts2.isElementAccessExpression(cur)) {
1677
+ if (ts2.isPropertyAccessExpression(cur)) {
1678
+ props.unshift(cur.name.text);
1679
+ if (props.length === 1) terminalNode = cur.name;
1680
+ cur = cur.expression;
1681
+ continue;
1682
+ }
1683
+ if (!cur.argumentExpression) return null;
1684
+ const property = staticPropertyName(cur.argumentExpression, ctx, strings);
1685
+ if (!property) return null;
1686
+ props.unshift(property.name);
1687
+ causes.push(...property.causes);
1688
+ if (props.length === 1) terminalNode = cur.argumentExpression;
1689
+ cur = cur.expression;
1690
+ }
1691
+ if (!ts2.isIdentifier(cur)) return null;
1692
+ const sym = symbolAt(ctx, cur);
1693
+ if (sym) {
1694
+ const binding = runners.get(sym);
1695
+ if (!binding) return null;
1696
+ causes.push(...binding.causes);
1697
+ } else if (!JS_RUNNERS.has(cur.text)) {
1698
+ return null;
1699
+ }
1700
+ return { root: cur.text, props, terminalNode, causes };
1701
+ }
1702
+ function optionDisables(value) {
1703
+ return !(value.kind === ts2.SyntaxKind.FalseKeyword || ts2.isNumericLiteral(value) && Number(value.text) === 0);
1704
+ }
1705
+ function shorthandOptionDisables(prop, ctx) {
1706
+ const sym = ctx.checker.getShorthandAssignmentValueSymbol(prop) ?? symbolAt(ctx, prop.name);
1707
+ const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts2.isVariableDeclaration(d));
1708
+ if (!decl || !ts2.isVariableDeclaration(decl) || !decl.initializer) return true;
1709
+ return optionDisables(decl.initializer);
1710
+ }
1711
+ function structuralNodeKey(node, sf) {
1712
+ const children = node.getChildren(sf);
1713
+ const text = node.text;
1714
+ if (children.length === 0) {
1715
+ return typeof text === "string" ? `${node.kind}:${JSON.stringify(text)}` : String(node.kind);
1716
+ }
1717
+ return `${node.kind}(${children.map((child) => structuralNodeKey(child, sf)).join(",")})`;
1718
+ }
1719
+ function semanticSkipHits(ctx) {
1720
+ const runners = importAliases(ctx);
1721
+ const strings = topLevelStaticStrings(ctx);
1722
+ const hits = [];
1723
+ const callOrdinals = /* @__PURE__ */ new Map();
1724
+ const visit = (node) => {
1725
+ if (ts2.isCallExpression(node)) {
1726
+ const callText = node.getText(ctx.sf).trim();
1727
+ const callIdentity = structuralNodeKey(node, ctx.sf);
1728
+ const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
1729
+ callOrdinals.set(callIdentity, ordinal);
1730
+ const callKey = `${callIdentity}\0${ordinal}`;
1731
+ const chain = runnerChain(node.expression, ctx, runners, strings);
1732
+ if (chain) {
1733
+ const { props, terminalNode, causes } = chain;
1734
+ const terminal = props.at(-1);
1735
+ const push = (target, extraCauses, why) => {
1736
+ hits.push({
1737
+ semanticKey: `${callKey}\0${why}`,
1738
+ terminalNode: target,
1739
+ causeNodes: [...causes, ...extraCauses],
1740
+ why,
1741
+ evidence: callText
1742
+ });
1743
+ };
1744
+ if (terminal && (terminal === "skip" || terminal === "only" || terminal === "todo") && props.slice(0, -1).every((p) => JS_CHAIN_MODIFIERS.has(p))) {
1745
+ push(terminalNode, [], "a .skip/.only/.todo marker");
1746
+ } else if (terminal && ["skipIf", "runIf"].includes(terminal) && props.length === 1) {
1747
+ push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
1748
+ } else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
1749
+ push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
1750
+ } else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts2.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
1751
+ push(terminalNode, [], "an empty .each table (no case ever runs)");
1752
+ }
1753
+ if (props.length === 0) {
1754
+ for (const arg of node.arguments) {
1755
+ if (!ts2.isObjectLiteralExpression(arg)) continue;
1756
+ for (const prop of arg.properties) {
1757
+ if (ts2.isPropertyAssignment(prop)) {
1758
+ let name = null;
1759
+ let propertyCauses = [];
1760
+ if (ts2.isIdentifier(prop.name) || ts2.isStringLiteralLike(prop.name)) {
1761
+ name = prop.name.text;
1762
+ } else if (ts2.isComputedPropertyName(prop.name)) {
1763
+ const resolved = staticPropertyName(prop.name.expression, ctx, strings);
1764
+ name = resolved?.name ?? null;
1765
+ propertyCauses = resolved?.causes ?? [];
1766
+ }
1767
+ if (!name || !["skip", "todo", "only"].includes(name)) continue;
1768
+ if (!optionDisables(prop.initializer)) continue;
1769
+ const why = name === "only" ? "an { only: ... } option focusing the test" : name === "todo" ? "a { todo: ... } option (the test no longer fails the run)" : "a { skip: ... } option disabling the test";
1770
+ push(prop.name, propertyCauses, why);
1771
+ } else if (ts2.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1772
+ if (!shorthandOptionDisables(prop, ctx)) continue;
1773
+ push(prop.name, [], `a { ${prop.name.text} } option that conditionally narrows the test run`);
1774
+ }
1775
+ }
1776
+ }
1777
+ }
1778
+ }
1779
+ }
1780
+ ts2.forEachChild(node, visit);
1781
+ };
1782
+ visit(ctx.sf);
1783
+ return hits;
1784
+ }
1785
+ function astSkipHits(c) {
1786
+ if (c.after == null) return { hits: [], authoritative: false };
1787
+ const added = addedLineNumbers(c);
1788
+ if (added.size === 0) return { hits: [], authoritative: false };
1789
+ const afterCtx = astContext(c.path, c.after);
1790
+ if (!afterCtx) return { hits: [], authoritative: false };
1791
+ const beforeCtx = c.before == null ? null : astContext(c.path, c.before);
1792
+ const beforeKeys = beforeCtx == null ? null : new Set(semanticSkipHits(beforeCtx).map((hit) => hit.semanticKey));
1793
+ const lineOf = (node) => afterCtx.sf.getLineAndCharacterOfPosition(node.getStart(afterCtx.sf)).line + 1;
1794
+ const hits = [];
1795
+ const seen = /* @__PURE__ */ new Set();
1796
+ for (const hit of semanticSkipHits(afterCtx)) {
1797
+ const directLine = lineOf(hit.terminalNode);
1798
+ let findingLine = added.has(directLine) ? directLine : null;
1799
+ if (findingLine == null) {
1800
+ for (const cause of hit.causeNodes) {
1801
+ const line = lineOf(cause);
1802
+ if (added.has(line)) {
1803
+ findingLine = line;
1804
+ break;
1805
+ }
1806
+ }
1807
+ if (findingLine == null) continue;
1808
+ if (beforeKeys == null) continue;
1809
+ }
1810
+ if (beforeKeys?.has(hit.semanticKey)) continue;
1811
+ const dedupe = `${findingLine}\0${hit.semanticKey}`;
1812
+ if (seen.has(dedupe)) continue;
1813
+ seen.add(dedupe);
1814
+ hits.push({ line: findingLine, why: hit.why, evidence: hit.evidence });
1815
+ }
1816
+ return { hits, authoritative: true };
1817
+ }
1480
1818
  var testSkip = {
1481
1819
  id: RULE3,
1482
1820
  surface: ["file"],
@@ -1488,9 +1826,29 @@ var testSkip = {
1488
1826
  if (!isProtected(c.path, policy, "tests")) continue;
1489
1827
  const lang = langOf(c.path);
1490
1828
  const patterns = PATTERNS2[lang ?? "js"];
1829
+ const astHitLines = /* @__PURE__ */ new Set();
1830
+ let astAuthoritative = false;
1831
+ if (lang === "js" && c.after != null) {
1832
+ const analysis = astSkipHits(c);
1833
+ astAuthoritative = analysis.authoritative;
1834
+ for (const hit of analysis.hits) {
1835
+ astHitLines.add(hit.line);
1836
+ out2.push(
1837
+ makeFinding(RULE3, policy, {
1838
+ file: c.path,
1839
+ line: hit.line,
1840
+ message: `Test skipped or narrowed: ${hit.why}.`,
1841
+ evidence: hit.evidence,
1842
+ remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
1843
+ })
1844
+ );
1845
+ }
1846
+ }
1491
1847
  for (const l of addedLines(c)) {
1848
+ if (l.newLine != null && astHitLines.has(l.newLine)) continue;
1492
1849
  const comment = isCommentLine(l.content.trim(), lang);
1493
1850
  for (const p of patterns) {
1851
+ if (astAuthoritative && p.astOwned) continue;
1494
1852
  if (comment && !p.comment) continue;
1495
1853
  if (matchesOutsideString(p, l.content, lang)) {
1496
1854
  out2.push(
@@ -1513,7 +1871,7 @@ var testSkip = {
1513
1871
 
1514
1872
  // src/detectors/coverage-lowering.ts
1515
1873
  import picomatch2 from "picomatch";
1516
- import ts2 from "typescript";
1874
+ import ts3 from "typescript";
1517
1875
 
1518
1876
  // src/detectors/repo.ts
1519
1877
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -1641,29 +1999,29 @@ var METRICS = ["branches", "functions", "lines", "statements"];
1641
1999
  var isMetric = (k) => METRICS.includes(k);
1642
2000
  var norm = (p) => p.replace(/^\.\//, "");
1643
2001
  function keyName(name) {
1644
- if (ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name)) return name.text;
2002
+ if (ts3.isIdentifier(name) || ts3.isStringLiteral(name) || ts3.isNumericLiteral(name)) return name.text;
1645
2003
  return null;
1646
2004
  }
1647
2005
  function numericOf(e) {
1648
- if (ts2.isNumericLiteral(e)) return Number(e.text);
1649
- if (ts2.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
1650
- if (ts2.isPrefixUnaryExpression(e) && e.operator === ts2.SyntaxKind.MinusToken && ts2.isNumericLiteral(e.operand)) {
2006
+ if (ts3.isNumericLiteral(e)) return Number(e.text);
2007
+ if (ts3.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
2008
+ if (ts3.isPrefixUnaryExpression(e) && e.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(e.operand)) {
1651
2009
  return -Number(e.operand.text);
1652
2010
  }
1653
2011
  return void 0;
1654
2012
  }
1655
2013
  function metricsOf(expr, resolve9) {
1656
2014
  const obj = resolve9(expr);
1657
- if (!ts2.isObjectLiteralExpression(obj)) return void 0;
2015
+ if (!ts3.isObjectLiteralExpression(obj)) return void 0;
1658
2016
  const out2 = { values: {}, opaque: false };
1659
2017
  let any = false;
1660
2018
  for (const p of obj.properties) {
1661
- if (ts2.isSpreadAssignment(p)) {
2019
+ if (ts3.isSpreadAssignment(p)) {
1662
2020
  out2.opaque = true;
1663
2021
  any = true;
1664
2022
  continue;
1665
2023
  }
1666
- if (!ts2.isPropertyAssignment(p)) continue;
2024
+ if (!ts3.isPropertyAssignment(p)) continue;
1667
2025
  const k = keyName(p.name);
1668
2026
  if (k === null || !isMetric(k)) continue;
1669
2027
  any = true;
@@ -1675,34 +2033,34 @@ function metricsOf(expr, resolve9) {
1675
2033
  }
1676
2034
  function underKey(node, key2) {
1677
2035
  for (let p = node.parent; p; p = p.parent) {
1678
- if (ts2.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2036
+ if (ts3.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
1679
2037
  }
1680
2038
  return false;
1681
2039
  }
1682
2040
  function ownerKey(node) {
1683
2041
  const obj = node.parent;
1684
- if (!obj || !ts2.isObjectLiteralExpression(obj)) return null;
2042
+ if (!obj || !ts3.isObjectLiteralExpression(obj)) return null;
1685
2043
  let q = obj.parent;
1686
- while (q && (ts2.isParenthesizedExpression(q) || ts2.isAsExpression(q) || q.kind === ts2.SyntaxKind.SatisfiesExpression)) q = q.parent;
1687
- return q && ts2.isPropertyAssignment(q) ? keyName(q.name) : null;
2044
+ while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
2045
+ return q && ts3.isPropertyAssignment(q) ? keyName(q.name) : null;
1688
2046
  }
1689
2047
  function asExpression(src) {
1690
2048
  const t = src.trim();
1691
2049
  return t.startsWith("{") ? `(${t})` : src;
1692
2050
  }
1693
2051
  function sourceOf(src) {
1694
- return ts2.createSourceFile("cfg.ts", asExpression(src), ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
2052
+ return ts3.createSourceFile("cfg.ts", asExpression(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
1695
2053
  }
1696
2054
  function resolverFor(sf) {
1697
2055
  const consts = /* @__PURE__ */ new Map();
1698
2056
  for (const st of sf.statements) {
1699
- if (!ts2.isVariableStatement(st)) continue;
2057
+ if (!ts3.isVariableStatement(st)) continue;
1700
2058
  for (const d of st.declarationList.declarations) {
1701
- if (ts2.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2059
+ if (ts3.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
1702
2060
  }
1703
2061
  }
1704
2062
  return (e) => {
1705
- if (ts2.isIdentifier(e)) {
2063
+ if (ts3.isIdentifier(e)) {
1706
2064
  const bound = consts.get(e.text);
1707
2065
  return bound ?? e;
1708
2066
  }
@@ -1715,25 +2073,25 @@ function parseThresholds(src) {
1715
2073
  const sf = sourceOf(src);
1716
2074
  const resolve9 = resolverFor(sf);
1717
2075
  const visit = (node) => {
1718
- if (ts2.isPropertyAssignment(node)) {
2076
+ if (ts3.isPropertyAssignment(node)) {
1719
2077
  const key2 = keyName(node.name);
1720
2078
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
1721
2079
  res.present = true;
1722
2080
  const init = resolve9(node.initializer);
1723
- if (!ts2.isObjectLiteralExpression(init)) {
2081
+ if (!ts3.isObjectLiteralExpression(init)) {
1724
2082
  res.opaque = true;
1725
2083
  } else {
1726
2084
  const flat = metricsOf(init, resolve9);
1727
2085
  if (flat) res.global = merge(res.global, flat);
1728
2086
  for (const p of init.properties) {
1729
- if (ts2.isSpreadAssignment(p)) {
2087
+ if (ts3.isSpreadAssignment(p)) {
1730
2088
  res.opaque = true;
1731
2089
  continue;
1732
2090
  }
1733
- if (!ts2.isPropertyAssignment(p)) continue;
2091
+ if (!ts3.isPropertyAssignment(p)) continue;
1734
2092
  const k = keyName(p.name);
1735
2093
  if (k === null || isMetric(k)) continue;
1736
- if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts2.SyntaxKind.TrueKeyword) {
2094
+ if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts3.SyntaxKind.TrueKeyword) {
1737
2095
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
1738
2096
  continue;
1739
2097
  }
@@ -1745,7 +2103,7 @@ function parseThresholds(src) {
1745
2103
  }
1746
2104
  }
1747
2105
  }
1748
- ts2.forEachChild(node, visit);
2106
+ ts3.forEachChild(node, visit);
1749
2107
  };
1750
2108
  visit(sf);
1751
2109
  } catch {
@@ -1756,12 +2114,12 @@ function merge(a, b) {
1756
2114
  return { values: { ...a?.values ?? {}, ...b.values }, opaque: (a?.opaque ?? false) || b.opaque };
1757
2115
  }
1758
2116
  function literals(e) {
1759
- if (ts2.isStringLiteral(e)) return { items: [e.text], opaque: false };
1760
- if (!ts2.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2117
+ if (ts3.isStringLiteral(e)) return { items: [e.text], opaque: false };
2118
+ if (!ts3.isArrayLiteralExpression(e)) return { items: [], opaque: true };
1761
2119
  const items = [];
1762
2120
  let opaque = false;
1763
2121
  for (const el of e.elements) {
1764
- if (ts2.isStringLiteral(el) || ts2.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2122
+ if (ts3.isStringLiteral(el) || ts3.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
1765
2123
  else opaque = true;
1766
2124
  }
1767
2125
  return { items, opaque };
@@ -1772,7 +2130,7 @@ function parseLists(src) {
1772
2130
  const sf = sourceOf(src);
1773
2131
  const resolve9 = resolverFor(sf);
1774
2132
  const visit = (node) => {
1775
- if (ts2.isPropertyAssignment(node)) {
2133
+ if (ts3.isPropertyAssignment(node)) {
1776
2134
  const k = keyName(node.name);
1777
2135
  const vitestCoverage = ownerKey(node) === "coverage";
1778
2136
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
@@ -1786,7 +2144,7 @@ function parseLists(src) {
1786
2144
  if (opaque) res.opaqueIgnore = true;
1787
2145
  }
1788
2146
  }
1789
- ts2.forEachChild(node, visit);
2147
+ ts3.forEachChild(node, visit);
1790
2148
  };
1791
2149
  visit(sf);
1792
2150
  } catch {
@@ -1797,15 +2155,15 @@ function parseSwitches(src) {
1797
2155
  const res = {};
1798
2156
  try {
1799
2157
  const sf = sourceOf(src);
1800
- const bool = (e) => e.kind === ts2.SyntaxKind.TrueKeyword ? true : e.kind === ts2.SyntaxKind.FalseKeyword ? false : void 0;
2158
+ const bool = (e) => e.kind === ts3.SyntaxKind.TrueKeyword ? true : e.kind === ts3.SyntaxKind.FalseKeyword ? false : void 0;
1801
2159
  const visit = (node) => {
1802
- if (ts2.isPropertyAssignment(node)) {
2160
+ if (ts3.isPropertyAssignment(node)) {
1803
2161
  const k = keyName(node.name);
1804
2162
  const owner = ownerKey(node);
1805
2163
  if (k === "enabled" && owner === "coverage") res.enabled = bool(node.initializer);
1806
2164
  else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool(node.initializer);
1807
2165
  }
1808
- ts2.forEachChild(node, visit);
2166
+ ts3.forEachChild(node, visit);
1809
2167
  };
1810
2168
  visit(sf);
1811
2169
  } catch {
@@ -1818,11 +2176,11 @@ function sectionText(src, key2) {
1818
2176
  let out2 = "";
1819
2177
  const visit = (node) => {
1820
2178
  if (out2) return;
1821
- if (ts2.isPropertyAssignment(node) && keyName(node.name) === key2 && ts2.isObjectLiteralExpression(node.initializer)) {
2179
+ if (ts3.isPropertyAssignment(node) && keyName(node.name) === key2 && ts3.isObjectLiteralExpression(node.initializer)) {
1822
2180
  out2 = node.initializer.getText(sf);
1823
2181
  return;
1824
2182
  }
1825
- ts2.forEachChild(node, visit);
2183
+ ts3.forEachChild(node, visit);
1826
2184
  };
1827
2185
  visit(sf);
1828
2186
  return out2;
@@ -5172,11 +5530,11 @@ var hookTampering = {
5172
5530
  };
5173
5531
 
5174
5532
  // src/detectors/test-deletion.ts
5175
- import ts4 from "typescript";
5533
+ import ts5 from "typescript";
5176
5534
 
5177
5535
  // src/detectors/suite-config.ts
5178
5536
  import picomatch3 from "picomatch";
5179
- import ts3 from "typescript";
5537
+ import ts4 from "typescript";
5180
5538
  var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
5181
5539
  var JEST_DEFAULT_IGNORE = ["/node_modules/"];
5182
5540
  var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
@@ -5207,18 +5565,18 @@ var PYTEST_CANONICAL_SAMPLES = [
5207
5565
  "tests/conftest.py"
5208
5566
  ];
5209
5567
  function keyName2(name) {
5210
- if (ts3.isIdentifier(name) || ts3.isStringLiteral(name)) return name.text;
5568
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name)) return name.text;
5211
5569
  return null;
5212
5570
  }
5213
- var isStr = (e) => ts3.isStringLiteral(e) || ts3.isNoSubstitutionTemplateLiteral(e);
5571
+ var isStr = (e) => ts4.isStringLiteral(e) || ts4.isNoSubstitutionTemplateLiteral(e);
5214
5572
  function literals2(e) {
5215
5573
  if (isStr(e)) return { items: [e.text], opaque: false };
5216
- if (!ts3.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5574
+ if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5217
5575
  const items = [];
5218
5576
  let opaque = false;
5219
5577
  for (const el of e.elements) {
5220
5578
  if (isStr(el)) items.push(el.text);
5221
- else if (ts3.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5579
+ else if (ts4.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5222
5580
  items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5223
5581
  } else opaque = true;
5224
5582
  }
@@ -5226,10 +5584,10 @@ function literals2(e) {
5226
5584
  }
5227
5585
  function ownerKey2(node) {
5228
5586
  const obj = node.parent;
5229
- if (!obj || !ts3.isObjectLiteralExpression(obj)) return null;
5587
+ if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
5230
5588
  let q = obj.parent;
5231
- while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
5232
- return q && ts3.isPropertyAssignment(q) ? keyName2(q.name) : null;
5589
+ while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
5590
+ return q && ts4.isPropertyAssignment(q) ? keyName2(q.name) : null;
5233
5591
  }
5234
5592
  function asExpression2(src) {
5235
5593
  const t = src.trim();
@@ -5370,15 +5728,15 @@ function collect(root, runner) {
5370
5728
  projectList = e;
5371
5729
  };
5372
5730
  const visit = (node) => {
5373
- if (ts3.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5731
+ if (ts4.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5374
5732
  sel.present = true;
5375
5733
  sel.opaque = true;
5376
5734
  }
5377
- if (ts3.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts3.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5735
+ if (ts4.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts4.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5378
5736
  sel.present = true;
5379
5737
  sel.opaque = true;
5380
5738
  }
5381
- if (ts3.isPropertyAssignment(node)) {
5739
+ if (ts4.isPropertyAssignment(node)) {
5382
5740
  const k = keyName2(node.name);
5383
5741
  if (runner === "jest") {
5384
5742
  if (k === "testMatch") sel.include = take(k, node.initializer);
@@ -5408,7 +5766,7 @@ function collect(root, runner) {
5408
5766
  }
5409
5767
  }
5410
5768
  }
5411
- ts3.forEachChild(node, visit);
5769
+ ts4.forEachChild(node, visit);
5412
5770
  };
5413
5771
  visit(root);
5414
5772
  if (projectList) sel.projects = readProjects(projectList, sel, runner);
@@ -5417,30 +5775,30 @@ function collect(root, runner) {
5417
5775
  }
5418
5776
  function unwrapConfigCall(e) {
5419
5777
  let x = e;
5420
- while (ts3.isParenthesizedExpression(x) || ts3.isAsExpression(x) || x.kind === ts3.SyntaxKind.SatisfiesExpression) {
5778
+ while (ts4.isParenthesizedExpression(x) || ts4.isAsExpression(x) || x.kind === ts4.SyntaxKind.SatisfiesExpression) {
5421
5779
  x = x.expression;
5422
5780
  }
5423
- if (ts3.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5781
+ if (ts4.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5424
5782
  return x;
5425
5783
  }
5426
5784
  var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
5427
5785
  function spreadsIntoConfig(node, runner) {
5428
5786
  const obj = node.parent;
5429
- if (!ts3.isObjectLiteralExpression(obj)) return false;
5430
- const keys = obj.properties.map((p) => ts3.isPropertyAssignment(p) ? keyName2(p.name) : null);
5787
+ if (!ts4.isObjectLiteralExpression(obj)) return false;
5788
+ const keys = obj.properties.map((p) => ts4.isPropertyAssignment(p) ? keyName2(p.name) : null);
5431
5789
  if (runner === "vitest") {
5432
5790
  if (keys.includes("test")) return true;
5433
5791
  let q = obj.parent;
5434
- while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
5435
- return !!q && ts3.isPropertyAssignment(q) && keyName2(q.name) === "test";
5792
+ while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
5793
+ return !!q && ts4.isPropertyAssignment(q) && keyName2(q.name) === "test";
5436
5794
  }
5437
5795
  return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
5438
5796
  }
5439
5797
  function extendsOf(el) {
5440
5798
  for (const p of el.properties) {
5441
- if (!ts3.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
5442
- if (p.initializer.kind === ts3.SyntaxKind.TrueKeyword) return true;
5443
- if (p.initializer.kind === ts3.SyntaxKind.FalseKeyword) return null;
5799
+ if (!ts4.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
5800
+ if (p.initializer.kind === ts4.SyntaxKind.TrueKeyword) return true;
5801
+ if (p.initializer.kind === ts4.SyntaxKind.FalseKeyword) return null;
5444
5802
  return "path";
5445
5803
  }
5446
5804
  return null;
@@ -5461,14 +5819,14 @@ function readProject(el, root, runner) {
5461
5819
  return p;
5462
5820
  }
5463
5821
  function readProjects(e, root, runner) {
5464
- if (!ts3.isArrayLiteralExpression(e)) {
5822
+ if (!ts4.isArrayLiteralExpression(e)) {
5465
5823
  root.opaque = true;
5466
5824
  return [];
5467
5825
  }
5468
5826
  const list2 = [];
5469
5827
  for (const el of e.elements) {
5470
5828
  const lit = unwrapConfigCall(el);
5471
- if (ts3.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
5829
+ if (ts4.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
5472
5830
  else root.opaque = true;
5473
5831
  }
5474
5832
  return list2;
@@ -5476,14 +5834,14 @@ function readProjects(e, root, runner) {
5476
5834
  function workspaceArray(sf) {
5477
5835
  for (const st of sf.statements) {
5478
5836
  let e;
5479
- if (ts3.isExportAssignment(st)) e = st.expression;
5480
- else if (ts3.isExpressionStatement(st) && ts3.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
5837
+ if (ts4.isExportAssignment(st)) e = st.expression;
5838
+ else if (ts4.isExpressionStatement(st) && ts4.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
5481
5839
  if (!e) continue;
5482
- while (ts3.isParenthesizedExpression(e) || ts3.isAsExpression(e) || e.kind === ts3.SyntaxKind.SatisfiesExpression) {
5840
+ while (ts4.isParenthesizedExpression(e) || ts4.isAsExpression(e) || e.kind === ts4.SyntaxKind.SatisfiesExpression) {
5483
5841
  e = e.expression;
5484
5842
  }
5485
- if (ts3.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
5486
- return ts3.isArrayLiteralExpression(e) ? e : null;
5843
+ if (ts4.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
5844
+ return ts4.isArrayLiteralExpression(e) ? e : null;
5487
5845
  }
5488
5846
  return null;
5489
5847
  }
@@ -5491,7 +5849,7 @@ function parseSelection(src, runner, path = "") {
5491
5849
  let sel = empty();
5492
5850
  if (runner === "pytest") return pytestSelection(src);
5493
5851
  try {
5494
- const sf = ts3.createSourceFile("cfg.ts", asExpression2(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
5852
+ const sf = ts4.createSourceFile("cfg.ts", asExpression2(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
5495
5853
  if (runner === "vitest" && isWorkspaceFile(path)) {
5496
5854
  const arr = workspaceArray(sf);
5497
5855
  if (arr) {
@@ -5598,21 +5956,21 @@ var TEST_DEFS = {
5598
5956
  cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
5599
5957
  };
5600
5958
  function calleeName(expr) {
5601
- if (ts4.isIdentifier(expr)) return expr.text;
5602
- if (ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression)) {
5959
+ if (ts5.isIdentifier(expr)) return expr.text;
5960
+ if (ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression)) {
5603
5961
  return expr.expression.text;
5604
5962
  }
5605
5963
  return null;
5606
5964
  }
5607
5965
  var TABLE_METHOD = /^(?:each|for)$/;
5608
- var isEachOf = (expr) => ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
5609
- var isDescribeEach = (expr) => ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
5966
+ var isEachOf = (expr) => ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
5967
+ var isDescribeEach = (expr) => ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
5610
5968
  function loopRows(iterable) {
5611
- if (ts4.isArrayLiteralExpression(iterable)) {
5969
+ if (ts5.isArrayLiteralExpression(iterable)) {
5612
5970
  let n = 0;
5613
5971
  let open = false;
5614
5972
  for (const el of iterable.elements) {
5615
- if (ts4.isSpreadElement(el)) open = true;
5973
+ if (ts5.isSpreadElement(el)) open = true;
5616
5974
  else n++;
5617
5975
  }
5618
5976
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -5624,20 +5982,20 @@ function templateRows(node) {
5624
5982
  return Math.max(rows, 1);
5625
5983
  }
5626
5984
  function hasSubstantiveBody(call) {
5627
- const fn = call.arguments.find((a) => ts4.isArrowFunction(a) || ts4.isFunctionExpression(a));
5985
+ const fn = call.arguments.find((a) => ts5.isArrowFunction(a) || ts5.isFunctionExpression(a));
5628
5986
  if (!fn) return true;
5629
5987
  const body = fn.body;
5630
- const text = ts4.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
5988
+ const text = ts5.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
5631
5989
  return text.split("\n").some((l) => isSignificantLine(l.trim(), "js"));
5632
5990
  }
5633
5991
  function eachRows(call) {
5634
5992
  const arg = call.arguments[0];
5635
5993
  if (!arg) return { n: 1, open: true };
5636
- if (ts4.isArrayLiteralExpression(arg)) {
5994
+ if (ts5.isArrayLiteralExpression(arg)) {
5637
5995
  let n = 0;
5638
5996
  let open = false;
5639
5997
  for (const el of arg.elements) {
5640
- if (ts4.isSpreadElement(el)) open = true;
5998
+ if (ts5.isSpreadElement(el)) open = true;
5641
5999
  else n++;
5642
6000
  }
5643
6001
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -5652,35 +6010,35 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
5652
6010
  for (const line of src.split("\n")) if (re.test(line)) n2++;
5653
6011
  return { min: n2, open: false };
5654
6012
  }
5655
- const sf = ts4.createSourceFile("spec.ts", src, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
6013
+ const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
5656
6014
  let n = 0;
5657
6015
  let open = false;
5658
6016
  const visit = (node, mult) => {
5659
- if (ts4.isForOfStatement(node)) {
6017
+ if (ts5.isForOfStatement(node)) {
5660
6018
  const r = loopRows(node.expression);
5661
6019
  if (r.open) open = true;
5662
6020
  visit(node.statement, mult * r.n);
5663
6021
  return;
5664
6022
  }
5665
- if (ts4.isForStatement(node) || ts4.isForInStatement(node) || ts4.isWhileStatement(node) || ts4.isDoStatement(node)) {
6023
+ if (ts5.isForStatement(node) || ts5.isForInStatement(node) || ts5.isWhileStatement(node) || ts5.isDoStatement(node)) {
5666
6024
  open = true;
5667
6025
  }
5668
- if (ts4.isCallExpression(node)) {
6026
+ if (ts5.isCallExpression(node)) {
5669
6027
  const callee = node.expression;
5670
- if (ts4.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6028
+ if (ts5.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
5671
6029
  const r = loopRows(callee.expression);
5672
6030
  if (r.open) open = true;
5673
6031
  for (const a of node.arguments) visit(a, mult * r.n);
5674
6032
  return;
5675
6033
  }
5676
6034
  const head = node.expression;
5677
- if (ts4.isCallExpression(head) && isDescribeEach(head.expression)) {
6035
+ if (ts5.isCallExpression(head) && isDescribeEach(head.expression)) {
5678
6036
  const r = eachRows(head);
5679
6037
  if (r.open) open = true;
5680
6038
  for (const a of node.arguments) visit(a, mult * r.n);
5681
6039
  return;
5682
6040
  }
5683
- if (ts4.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6041
+ if (ts5.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
5684
6042
  for (const a of node.arguments) visit(a, mult * templateRows(head));
5685
6043
  return;
5686
6044
  }
@@ -5692,10 +6050,10 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
5692
6050
  const name = calleeName(node.expression);
5693
6051
  if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
5694
6052
  }
5695
- } else if (ts4.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6053
+ } else if (ts5.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
5696
6054
  n += templateRows(node) * mult;
5697
6055
  }
5698
- ts4.forEachChild(node, (c) => visit(c, mult));
6056
+ ts5.forEachChild(node, (c) => visit(c, mult));
5699
6057
  };
5700
6058
  visit(sf, 1);
5701
6059
  return { min: n, open };
@@ -5997,7 +6355,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
5997
6355
  }
5998
6356
 
5999
6357
  // src/detectors/test-content-removal.ts
6000
- import ts5 from "typescript";
6358
+ import ts6 from "typescript";
6001
6359
  var RULE8 = "test-content-removal";
6002
6360
  var MIN_REMOVED_LINES = 3;
6003
6361
  var MIN_REMOVED_ROWS = 2;
@@ -6007,13 +6365,13 @@ function eachTables(src, path) {
6007
6365
  let open = false;
6008
6366
  if (langOf(path) !== "js" && langOf(path) !== null) return { rows, open };
6009
6367
  try {
6010
- const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
6368
+ const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6011
6369
  const visit = (node) => {
6012
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts5.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
6370
+ if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts6.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
6013
6371
  const arg = node.arguments[0];
6014
- if (arg && ts5.isArrayLiteralExpression(arg)) {
6372
+ if (arg && ts6.isArrayLiteralExpression(arg)) {
6015
6373
  for (const el of arg.elements) {
6016
- if (ts5.isSpreadElement(el)) {
6374
+ if (ts6.isSpreadElement(el)) {
6017
6375
  open = true;
6018
6376
  continue;
6019
6377
  }
@@ -6023,7 +6381,7 @@ function eachTables(src, path) {
6023
6381
  }
6024
6382
  } else open = true;
6025
6383
  }
6026
- ts5.forEachChild(node, visit);
6384
+ ts6.forEachChild(node, visit);
6027
6385
  };
6028
6386
  visit(sf);
6029
6387
  } catch {
@@ -6201,6 +6559,214 @@ var snapshotOnlyRewrite = {
6201
6559
  }
6202
6560
  };
6203
6561
 
6562
+ // src/detectors/assertion-weakening.ts
6563
+ import ts7 from "typescript";
6564
+ var RULE11 = "assertion-weakening";
6565
+ var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
6566
+ var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
6567
+ var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
6568
+ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
6569
+ var compact = (s) => s.replace(/\s+/g, "");
6570
+ function literalTitle(node) {
6571
+ if (!node) return null;
6572
+ if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
6573
+ return null;
6574
+ }
6575
+ function scriptKindForPath(path) {
6576
+ const lower = path.toLowerCase();
6577
+ if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
6578
+ if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
6579
+ return ts7.ScriptKind.TS;
6580
+ }
6581
+ if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
6582
+ return ts7.ScriptKind.JS;
6583
+ }
6584
+ function staticFacts(node) {
6585
+ if (!node) return null;
6586
+ if (node.kind === ts7.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6587
+ if (node.kind === ts7.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6588
+ if (node.kind === ts7.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6589
+ if (ts7.isIdentifier(node) && node.text === "undefined") {
6590
+ return { defined: false, truthy: false };
6591
+ }
6592
+ if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
6593
+ if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
6594
+ return { defined: true, truthy: node.text.length > 0 };
6595
+ }
6596
+ if (ts7.isNumericLiteral(node)) {
6597
+ return { defined: true, truthy: Number(node.text) !== 0 };
6598
+ }
6599
+ if (ts7.isBigIntLiteral(node)) {
6600
+ return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
6601
+ }
6602
+ if (ts7.isPrefixUnaryExpression(node) && (node.operator === ts7.SyntaxKind.MinusToken || node.operator === ts7.SyntaxKind.PlusToken) && ts7.isNumericLiteral(node.operand)) {
6603
+ const value = Number(node.operand.text) * (node.operator === ts7.SyntaxKind.MinusToken ? -1 : 1);
6604
+ return { defined: true, truthy: value !== 0 };
6605
+ }
6606
+ if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
6607
+ return { defined: true, truthy: true };
6608
+ }
6609
+ return null;
6610
+ }
6611
+ function expectAssertion(node, sf) {
6612
+ if (!ts7.isPropertyAccessExpression(node.expression)) return null;
6613
+ const matcher = node.expression.name.text;
6614
+ let receiver = node.expression.expression;
6615
+ let polarity = "";
6616
+ if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6617
+ polarity = "not";
6618
+ receiver = receiver.expression;
6619
+ }
6620
+ if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6621
+ return null;
6622
+ }
6623
+ const subject = compact(receiver.arguments[0].getText(sf));
6624
+ const args = node.arguments.map((arg) => compact(arg.getText(sf)));
6625
+ return {
6626
+ subject,
6627
+ polarity,
6628
+ matcher,
6629
+ args,
6630
+ firstArgFacts: staticFacts(node.arguments[0]),
6631
+ canonical: [subject, polarity, matcher, ...args].join("|"),
6632
+ text: node.getText(sf).replace(/\s+/g, " ")
6633
+ };
6634
+ }
6635
+ function simpleCallName(node) {
6636
+ return ts7.isIdentifier(node.expression) ? node.expression.text : null;
6637
+ }
6638
+ function callbackFor(node) {
6639
+ const cb = node.arguments[1];
6640
+ return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
6641
+ }
6642
+ function blocks(src, path) {
6643
+ const out2 = /* @__PURE__ */ new Map();
6644
+ if (langOf(path) !== "js") return out2;
6645
+ try {
6646
+ const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
6647
+ const visit = (node, suites) => {
6648
+ if (ts7.isCallExpression(node)) {
6649
+ const call = simpleCallName(node);
6650
+ const title = literalTitle(node.arguments[0]);
6651
+ const cb = callbackFor(node);
6652
+ if (call && SUITE_CALLS.has(call) && title !== null && cb) {
6653
+ const nextSuites = [...suites, title];
6654
+ ts7.forEachChild(cb, (child) => visit(child, nextSuites));
6655
+ return;
6656
+ }
6657
+ if (call && TEST_CALLS.has(call) && title !== null && cb) {
6658
+ const assertions = [];
6659
+ const collect2 = (child) => {
6660
+ if (child !== cb && ts7.isCallExpression(child)) {
6661
+ const nestedCall = simpleCallName(child);
6662
+ if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
6663
+ return;
6664
+ }
6665
+ }
6666
+ if (ts7.isCallExpression(child)) {
6667
+ const a = expectAssertion(child, sf);
6668
+ if (a) assertions.push(a);
6669
+ }
6670
+ ts7.forEachChild(child, collect2);
6671
+ };
6672
+ collect2(cb);
6673
+ const identity2 = [...suites, title].join(" > ");
6674
+ const list2 = out2.get(identity2) ?? [];
6675
+ list2.push({ title, identity: identity2, assertions });
6676
+ out2.set(identity2, list2);
6677
+ return;
6678
+ }
6679
+ }
6680
+ ts7.forEachChild(node, (child) => visit(child, suites));
6681
+ };
6682
+ visit(sf, []);
6683
+ } catch {
6684
+ }
6685
+ return out2;
6686
+ }
6687
+ function pureRemoval(before, after) {
6688
+ if (after.length >= before.length) return null;
6689
+ const remaining = /* @__PURE__ */ new Map();
6690
+ for (const a of before) remaining.set(a.canonical, (remaining.get(a.canonical) ?? 0) + 1);
6691
+ for (const a of after) {
6692
+ const n = remaining.get(a.canonical) ?? 0;
6693
+ if (n <= 0) return null;
6694
+ remaining.set(a.canonical, n - 1);
6695
+ }
6696
+ for (const a of before) {
6697
+ const n = remaining.get(a.canonical) ?? 0;
6698
+ if (n > 0) return a;
6699
+ }
6700
+ return null;
6701
+ }
6702
+ function factsProveWeakMatcher(oldA, newMatcher) {
6703
+ if (!EXACT_MATCHERS.has(oldA.matcher) || oldA.args.length !== 1) return false;
6704
+ const facts = oldA.firstArgFacts;
6705
+ if (!facts) return false;
6706
+ if (newMatcher === "toBeDefined") return facts.defined;
6707
+ if (newMatcher === "toBeTruthy") return facts.truthy === true;
6708
+ return false;
6709
+ }
6710
+ function directionalWeakening(before, after) {
6711
+ for (const oldA of before) {
6712
+ for (const newA of after) {
6713
+ if (oldA.subject !== newA.subject) continue;
6714
+ if (oldA.polarity !== "" || newA.polarity !== "") continue;
6715
+ if (WEAK_MATCHERS.has(newA.matcher) && newA.args.length === 0 && factsProveWeakMatcher(oldA, newA.matcher)) {
6716
+ return { before: oldA, after: newA, kind: "matcher" };
6717
+ }
6718
+ if (oldA.matcher === "toThrow" && oldA.args.length > 0 && newA.matcher === "toThrow" && newA.args.length === 0) {
6719
+ return { before: oldA, after: newA, kind: "throw-specificity" };
6720
+ }
6721
+ }
6722
+ }
6723
+ return null;
6724
+ }
6725
+ function findingForBlock(path, before, after, policy) {
6726
+ const weakened = directionalWeakening(before.assertions, after.assertions);
6727
+ if (weakened) {
6728
+ const message = weakened.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened.before.matcher} was replaced by weaker ${weakened.after.matcher} on the same subject.`;
6729
+ return makeFinding(RULE11, policy, {
6730
+ file: path,
6731
+ message,
6732
+ evidence: `${weakened.before.text} -> ${weakened.after.text}`,
6733
+ remediation: "Keep the stronger expectation and fix the code under test. If the contract genuinely changed, review the test change explicitly; this rule is warning-only while its precision is measured."
6734
+ });
6735
+ }
6736
+ const removed = pureRemoval(before.assertions, after.assertions);
6737
+ if (removed) {
6738
+ return makeFinding(RULE11, policy, {
6739
+ file: path,
6740
+ message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
6741
+ evidence: removed.text,
6742
+ remediation: "Keep the assertion and fix the code under test. If the assertion is genuinely obsolete, review the test change explicitly; this rule is warning-only while its precision is measured."
6743
+ });
6744
+ }
6745
+ return null;
6746
+ }
6747
+ var assertionWeakening = {
6748
+ id: RULE11,
6749
+ surface: ["file"],
6750
+ certainty: "heuristic",
6751
+ run(changes, policy) {
6752
+ const out2 = [];
6753
+ for (const c of changes) {
6754
+ if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null || langOf(c.path) !== "js" || !isProtected(c.path, policy, "tests") || isProtected(c.path, policy, "snapshots")) {
6755
+ continue;
6756
+ }
6757
+ const beforeBlocks = blocks(c.before, c.path);
6758
+ const afterBlocks = blocks(c.after, c.path);
6759
+ for (const [identity2, oldList] of beforeBlocks) {
6760
+ const newList = afterBlocks.get(identity2);
6761
+ if (!newList || oldList.length !== 1 || newList.length !== 1) continue;
6762
+ const f = findingForBlock(c.path, oldList[0], newList[0], policy);
6763
+ if (f) out2.push(f);
6764
+ }
6765
+ }
6766
+ return out2;
6767
+ }
6768
+ };
6769
+
6204
6770
  // src/detectors/index.ts
6205
6771
  var allDetectors = [
6206
6772
  noVerify,
@@ -6213,7 +6779,8 @@ var allDetectors = [
6213
6779
  testDeletion,
6214
6780
  testContentRemoval,
6215
6781
  snapshotRewrite,
6216
- snapshotOnlyRewrite
6782
+ snapshotOnlyRewrite,
6783
+ assertionWeakening
6217
6784
  ];
6218
6785
 
6219
6786
  // src/engine.ts
@@ -6575,9 +7142,9 @@ function orderFindings(findings) {
6575
7142
  return (a.line ?? 0) - (b.line ?? 0);
6576
7143
  });
6577
7144
  }
6578
- function countsPhrase(blocks, warns) {
7145
+ function countsPhrase(blocks2, warns) {
6579
7146
  const parts = [];
6580
- if (blocks) parts.push(`${blocks} blocking`);
7147
+ if (blocks2) parts.push(`${blocks2} blocking`);
6581
7148
  if (warns) parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
6582
7149
  return parts.join(", ");
6583
7150
  }
@@ -6589,7 +7156,7 @@ var SIGNOFF_NOTE = "A blocking finding clears only with a human sign-off. In CI
6589
7156
  function renderText(input, opts) {
6590
7157
  const { scanned, ignoredFiles } = input;
6591
7158
  const findings = orderFindings(input.findings);
6592
- const blocks = findings.filter((f) => f.severity === "block");
7159
+ const blocks2 = findings.filter((f) => f.severity === "block");
6593
7160
  const warns = findings.filter((f) => f.severity === "warn");
6594
7161
  const c = opts.colour;
6595
7162
  const out2 = [];
@@ -6597,8 +7164,8 @@ function renderText(input, opts) {
6597
7164
  return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
6598
7165
  `;
6599
7166
  }
6600
- const headColour = blocks.length ? RED : YELLOW;
6601
- out2.push(paint(`tamperward: ${countsPhrase(blocks.length, warns.length)}`, BOLD + headColour, c));
7167
+ const headColour = blocks2.length ? RED : YELLOW;
7168
+ out2.push(paint(`tamperward: ${countsPhrase(blocks2.length, warns.length)}`, BOLD + headColour, c));
6602
7169
  out2.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
6603
7170
  out2.push("");
6604
7171
  const LABEL = "sign-off".length;
@@ -6618,7 +7185,7 @@ function renderText(input, opts) {
6618
7185
  if (f.signoff.required) row("sign-off", [stripControl(f.signoff.command)]);
6619
7186
  out2.push("");
6620
7187
  }
6621
- if (blocks.length > 0) {
7188
+ if (blocks2.length > 0) {
6622
7189
  out2.push(
6623
7190
  wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
6624
7191
  );
@@ -6668,7 +7235,7 @@ function locationCell(f, env) {
6668
7235
  }
6669
7236
  function renderSummary(input, env = process.env) {
6670
7237
  const findings = orderFindings(input.findings);
6671
- const blocks = findings.filter((f) => f.severity === "block");
7238
+ const blocks2 = findings.filter((f) => f.severity === "block");
6672
7239
  const warns = findings.filter((f) => f.severity === "warn");
6673
7240
  const scope = scopePhrase(input.scanned, input.ignoredFiles);
6674
7241
  if (findings.length === 0) {
@@ -6678,7 +7245,7 @@ No integrity findings (${scope}).
6678
7245
  `;
6679
7246
  }
6680
7247
  const out2 = [
6681
- `## Tamperward: ${countsPhrase(blocks.length, warns.length)}`,
7248
+ `## Tamperward: ${countsPhrase(blocks2.length, warns.length)}`,
6682
7249
  "",
6683
7250
  `${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
6684
7251
  "",
@@ -6731,12 +7298,12 @@ function report(input) {
6731
7298
  const { findings, scanned, ignoredFiles } = input;
6732
7299
  const format = resolveFormat(input);
6733
7300
  if (format === "json") {
6734
- const blocks = findings.filter((f) => f.severity === "block").length;
7301
+ const blocks2 = findings.filter((f) => f.severity === "block").length;
6735
7302
  process.stdout.write(
6736
7303
  JSON.stringify(
6737
7304
  // `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
6738
7305
  // is additive, so an existing consumer reading only the old keys is unaffected.
6739
- { findings, scanned, ignoredFiles, summary: { block: blocks, warn: findings.length - blocks } },
7306
+ { findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
6740
7307
  null,
6741
7308
  2
6742
7309
  ) + "\n"
@@ -6833,10 +7400,10 @@ import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writ
6833
7400
  import { createHash as createHash5 } from "node:crypto";
6834
7401
 
6835
7402
  // src/adapters/claude/deny.ts
6836
- function formatDenial(blocks) {
6837
- const lead = blocks[0];
7403
+ function formatDenial(blocks2) {
7404
+ const lead = blocks2[0];
6838
7405
  const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
6839
- for (const f of blocks) {
7406
+ for (const f of blocks2) {
6840
7407
  const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
6841
7408
  lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
6842
7409
  }
@@ -6845,7 +7412,7 @@ function formatDenial(blocks) {
6845
7412
  lines.push(
6846
7413
  "The other shortcuts are blocked too: skipping the hooks, editing the hook or CI workflow, lowering the coverage gate, and rewriting a protected file from the shell will each be denied."
6847
7414
  );
6848
- if (blocks.some((b) => b.signoff.required)) {
7415
+ if (blocks2.some((b) => b.signoff.required)) {
6849
7416
  lines.push(`If this change is genuinely correct, a human must sign off: ${lead.signoff.command}`);
6850
7417
  }
6851
7418
  return lines.join("\n") + "\n";
@@ -7263,7 +7830,7 @@ function runWatch(args) {
7263
7830
 
7264
7831
  // src/detectors/fs-events.ts
7265
7832
  import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7266
- var RULE11 = "transient-protected-mutation";
7833
+ var RULE12 = "transient-protected-mutation";
7267
7834
  var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
7268
7835
  var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
7269
7836
  var DEFAULT_EVENT_READ_IO = {
@@ -7458,7 +8025,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
7458
8025
  };
7459
8026
  }
7460
8027
  function transientFindings(events, persistentPaths, policy, finalHash) {
7461
- if (!isEnabled(RULE11, policy)) return [];
8028
+ if (!isEnabled(RULE12, policy)) return [];
7462
8029
  const byPath = /* @__PURE__ */ new Map();
7463
8030
  for (const e of events) {
7464
8031
  if (!isProtected(e.path, policy)) continue;
@@ -7478,7 +8045,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
7478
8045
  const modeChurn = modes.size > 1;
7479
8046
  if (!contentChurn && !modeChurn) continue;
7480
8047
  const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
7481
- const f = makeFinding(RULE11, policy, {
8048
+ const f = makeFinding(RULE12, policy, {
7482
8049
  file: path,
7483
8050
  message: `Transient protected mutation: ${what}, leaving no net diff.`,
7484
8051
  evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
@@ -7517,11 +8084,11 @@ function parseInput(raw) {
7517
8084
  }
7518
8085
  return parsed;
7519
8086
  }
7520
- function recordDenylog(blocks) {
8087
+ function recordDenylog(blocks2) {
7521
8088
  const log = process.env.TAMPERWARD_DENYLOG;
7522
8089
  if (!log) return;
7523
8090
  try {
7524
- appendFileSync4(log, blocks.map((b) => b.rule).join(",") + "\n");
8091
+ appendFileSync4(log, blocks2.map((b) => b.rule).join(",") + "\n");
7525
8092
  } catch {
7526
8093
  }
7527
8094
  }
@@ -7540,10 +8107,10 @@ function failClosed(kind, detail) {
7540
8107
  };
7541
8108
  return verdict([f], kind);
7542
8109
  }
7543
- function verdict(blocks, kind) {
7544
- if (blocks.length === 0) return { exitCode: 0, stdout: "" };
7545
- recordDenylog(blocks);
7546
- const reason = formatDenial(blocks);
8110
+ function verdict(blocks2, kind) {
8111
+ if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
8112
+ recordDenylog(blocks2);
8113
+ const reason = formatDenial(blocks2);
7547
8114
  const payload = kind === "PreToolUse" ? {
7548
8115
  hookSpecificOutput: {
7549
8116
  hookEventName: "PreToolUse",
@@ -7600,14 +8167,14 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7600
8167
  if (c.oldPath) seen.add(c.oldPath);
7601
8168
  }
7602
8169
  const changes = [];
7603
- const blocks = [];
8170
+ const blocks2 = [];
7604
8171
  const trusted = base ?? "HEAD";
7605
8172
  const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
7606
8173
  for (const rel of [...d.changed, ...d.deleted]) {
7607
8174
  if (seen.has(rel)) continue;
7608
8175
  const unjudgeable = unjudgeableFinding(cwd, rel);
7609
8176
  if (unjudgeable) {
7610
- blocks.push(unjudgeable);
8177
+ blocks2.push(unjudgeable);
7611
8178
  continue;
7612
8179
  }
7613
8180
  const after = fileOnDisk(rel, { cwd });
@@ -7627,7 +8194,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7627
8194
  }
7628
8195
  }
7629
8196
  if (before == null) {
7630
- blocks.push({
8197
+ blocks2.push({
7631
8198
  rule: "hidden-drift",
7632
8199
  severity: "block",
7633
8200
  file: rel,
@@ -7660,12 +8227,12 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7660
8227
  if (seen.has(rel)) continue;
7661
8228
  const unjudgeable = unjudgeableFinding(cwd, rel);
7662
8229
  if (unjudgeable) {
7663
- blocks.push(unjudgeable);
8230
+ blocks2.push(unjudgeable);
7664
8231
  continue;
7665
8232
  }
7666
8233
  const after = fileOnDisk(rel, { cwd });
7667
8234
  if (after == null) {
7668
- blocks.push({
8235
+ blocks2.push({
7669
8236
  rule: "hidden-drift",
7670
8237
  severity: "block",
7671
8238
  file: rel,
@@ -7678,7 +8245,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7678
8245
  }
7679
8246
  changes.push(...synthFileChange(rel, null, after));
7680
8247
  }
7681
- return { changes, blocks };
8248
+ return { changes, blocks: blocks2 };
7682
8249
  }
7683
8250
  function effectDriftBlocks(cwd, sessionId, policy) {
7684
8251
  if (!sessionId || !isGitRepo(cwd)) return null;
@@ -7703,8 +8270,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
7703
8270
  );
7704
8271
  const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
7705
8272
  const judged = [...scoped, ...gap.changes];
7706
- const blocks = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
7707
- if (blocks.length > 0) return blocks;
8273
+ const blocks2 = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
8274
+ if (blocks2.length > 0) return blocks2;
7708
8275
  savePtree(cwd, sessionId, current);
7709
8276
  return null;
7710
8277
  }
@@ -7736,9 +8303,9 @@ function preToolUseVerdict(input) {
7736
8303
  const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
7737
8304
  if (driftBlocks) return verdict(driftBlocks, "PreToolUse");
7738
8305
  const changes = changesFromClaudeHook(input, cwd);
7739
- const blocks = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
7740
- if (blocks.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
7741
- return verdict(blocks, "PreToolUse");
8306
+ const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
8307
+ if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
8308
+ return verdict(blocks2, "PreToolUse");
7742
8309
  } catch (e) {
7743
8310
  return failClosed("PreToolUse", errText2(e));
7744
8311
  }
@@ -7840,7 +8407,7 @@ function stopVerdict(input) {
7840
8407
  if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
7841
8408
  const cwd = input.cwd ?? process.cwd();
7842
8409
  if (!isGitRepo(cwd)) return { exitCode: 0, stdout: "" };
7843
- let blocks;
8410
+ let blocks2;
7844
8411
  let commitCursor = () => {
7845
8412
  };
7846
8413
  try {
@@ -7855,22 +8422,22 @@ function stopVerdict(input) {
7855
8422
  changes = changes.concat(gap.changes);
7856
8423
  hiddenBlocks = gap.blocks;
7857
8424
  }
7858
- blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
8425
+ blocks2 = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
7859
8426
  const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
7860
- blocks = blocks.concat(transient.blocks);
8427
+ blocks2 = blocks2.concat(transient.blocks);
7861
8428
  commitCursor = transient.commit;
7862
- if (blocks.length === 0 && input.session_id && current) {
8429
+ if (blocks2.length === 0 && input.session_id && current) {
7863
8430
  savePtree(cwd, input.session_id, current);
7864
8431
  saveTurnTree(cwd, input.session_id, current);
7865
8432
  }
7866
8433
  } catch (e) {
7867
8434
  return failClosed("Stop", errText2(e));
7868
8435
  }
7869
- if (blocks.length === 0) {
8436
+ if (blocks2.length === 0) {
7870
8437
  advanceTurnBaseline(cwd, input.session_id);
7871
8438
  commitCursor();
7872
8439
  }
7873
- return verdict(blocks, "Stop");
8440
+ return verdict(blocks2, "Stop");
7874
8441
  }
7875
8442
  function emit(r) {
7876
8443
  if (r.stdout) process.stdout.write(r.stdout);