tamperward 2.17.0 → 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.
package/README.md CHANGED
@@ -513,6 +513,13 @@ Seventeen rules are specified and sixteen ship (see the table in
513
513
  (`ts-any-cast`, `ts-any-launder`, `lint-suppression`), pipeline protection
514
514
  (`ci-tampering`, `hook-tampering`, `no-verify`), and the effect/outcome layers
515
515
  (`transient-protected-mutation`, `pristine-verification`, plus the `run` envelope).
516
+ `test-skip` keeps the established regex coverage for diff-only inputs and non-JS
517
+ ecosystems, while full parse-clean JS/TS BEFORE/AFTER changes also use a conservative
518
+ TypeScript AST path for multiline member chains, statically-computable properties,
519
+ known test-runner aliases and node:test-style option objects. Resolution is lexical
520
+ (symbol-based), dynamic computed properties are not guessed, statically false shorthand
521
+ options are clean, and trivia-free BEFORE/AFTER semantic identity prevents formatting-only
522
+ rewrites from re-blocking an already-existing skip/focus.
516
523
  `assertion-weakening` ships only the AST-proven one-way subset — statically
517
524
  proven exact/structural values weakened to truthy/defined, positive exception
518
525
  specificity removed, or a pure assertion removed from the same suite-qualified
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 {
@@ -6202,7 +6560,7 @@ var snapshotOnlyRewrite = {
6202
6560
  };
6203
6561
 
6204
6562
  // src/detectors/assertion-weakening.ts
6205
- import ts6 from "typescript";
6563
+ import ts7 from "typescript";
6206
6564
  var RULE11 = "assertion-weakening";
6207
6565
  var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
6208
6566
  var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
@@ -6211,55 +6569,55 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
6211
6569
  var compact = (s) => s.replace(/\s+/g, "");
6212
6570
  function literalTitle(node) {
6213
6571
  if (!node) return null;
6214
- if (ts6.isStringLiteral(node) || ts6.isNoSubstitutionTemplateLiteral(node)) return node.text;
6572
+ if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
6215
6573
  return null;
6216
6574
  }
6217
6575
  function scriptKindForPath(path) {
6218
6576
  const lower = path.toLowerCase();
6219
- if (lower.endsWith(".tsx")) return ts6.ScriptKind.TSX;
6577
+ if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
6220
6578
  if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
6221
- return ts6.ScriptKind.TS;
6579
+ return ts7.ScriptKind.TS;
6222
6580
  }
6223
- if (lower.endsWith(".jsx")) return ts6.ScriptKind.JSX;
6224
- return ts6.ScriptKind.JS;
6581
+ if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
6582
+ return ts7.ScriptKind.JS;
6225
6583
  }
6226
6584
  function staticFacts(node) {
6227
6585
  if (!node) return null;
6228
- if (node.kind === ts6.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6229
- if (node.kind === ts6.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6230
- if (node.kind === ts6.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6231
- if (ts6.isIdentifier(node) && node.text === "undefined") {
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") {
6232
6590
  return { defined: false, truthy: false };
6233
6591
  }
6234
- if (ts6.isVoidExpression(node)) return { defined: false, truthy: false };
6235
- if (ts6.isStringLiteral(node) || ts6.isNoSubstitutionTemplateLiteral(node)) {
6592
+ if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
6593
+ if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
6236
6594
  return { defined: true, truthy: node.text.length > 0 };
6237
6595
  }
6238
- if (ts6.isNumericLiteral(node)) {
6596
+ if (ts7.isNumericLiteral(node)) {
6239
6597
  return { defined: true, truthy: Number(node.text) !== 0 };
6240
6598
  }
6241
- if (ts6.isBigIntLiteral(node)) {
6599
+ if (ts7.isBigIntLiteral(node)) {
6242
6600
  return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
6243
6601
  }
6244
- if (ts6.isPrefixUnaryExpression(node) && (node.operator === ts6.SyntaxKind.MinusToken || node.operator === ts6.SyntaxKind.PlusToken) && ts6.isNumericLiteral(node.operand)) {
6245
- const value = Number(node.operand.text) * (node.operator === ts6.SyntaxKind.MinusToken ? -1 : 1);
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);
6246
6604
  return { defined: true, truthy: value !== 0 };
6247
6605
  }
6248
- if (ts6.isObjectLiteralExpression(node) || ts6.isArrayLiteralExpression(node) || ts6.isRegularExpressionLiteral(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node) || ts6.isClassExpression(node) || ts6.isNewExpression(node)) {
6606
+ if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
6249
6607
  return { defined: true, truthy: true };
6250
6608
  }
6251
6609
  return null;
6252
6610
  }
6253
6611
  function expectAssertion(node, sf) {
6254
- if (!ts6.isPropertyAccessExpression(node.expression)) return null;
6612
+ if (!ts7.isPropertyAccessExpression(node.expression)) return null;
6255
6613
  const matcher = node.expression.name.text;
6256
6614
  let receiver = node.expression.expression;
6257
6615
  let polarity = "";
6258
- if (ts6.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6616
+ if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6259
6617
  polarity = "not";
6260
6618
  receiver = receiver.expression;
6261
6619
  }
6262
- if (!ts6.isCallExpression(receiver) || !ts6.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6620
+ if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6263
6621
  return null;
6264
6622
  }
6265
6623
  const subject = compact(receiver.arguments[0].getText(sf));
@@ -6275,41 +6633,41 @@ function expectAssertion(node, sf) {
6275
6633
  };
6276
6634
  }
6277
6635
  function simpleCallName(node) {
6278
- return ts6.isIdentifier(node.expression) ? node.expression.text : null;
6636
+ return ts7.isIdentifier(node.expression) ? node.expression.text : null;
6279
6637
  }
6280
6638
  function callbackFor(node) {
6281
6639
  const cb = node.arguments[1];
6282
- return cb && (ts6.isArrowFunction(cb) || ts6.isFunctionExpression(cb)) ? cb : null;
6640
+ return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
6283
6641
  }
6284
6642
  function blocks(src, path) {
6285
6643
  const out2 = /* @__PURE__ */ new Map();
6286
6644
  if (langOf(path) !== "js") return out2;
6287
6645
  try {
6288
- const sf = ts6.createSourceFile(path, src, ts6.ScriptTarget.Latest, true, scriptKindForPath(path));
6646
+ const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
6289
6647
  const visit = (node, suites) => {
6290
- if (ts6.isCallExpression(node)) {
6648
+ if (ts7.isCallExpression(node)) {
6291
6649
  const call = simpleCallName(node);
6292
6650
  const title = literalTitle(node.arguments[0]);
6293
6651
  const cb = callbackFor(node);
6294
6652
  if (call && SUITE_CALLS.has(call) && title !== null && cb) {
6295
6653
  const nextSuites = [...suites, title];
6296
- ts6.forEachChild(cb, (child) => visit(child, nextSuites));
6654
+ ts7.forEachChild(cb, (child) => visit(child, nextSuites));
6297
6655
  return;
6298
6656
  }
6299
6657
  if (call && TEST_CALLS.has(call) && title !== null && cb) {
6300
6658
  const assertions = [];
6301
6659
  const collect2 = (child) => {
6302
- if (child !== cb && ts6.isCallExpression(child)) {
6660
+ if (child !== cb && ts7.isCallExpression(child)) {
6303
6661
  const nestedCall = simpleCallName(child);
6304
6662
  if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
6305
6663
  return;
6306
6664
  }
6307
6665
  }
6308
- if (ts6.isCallExpression(child)) {
6666
+ if (ts7.isCallExpression(child)) {
6309
6667
  const a = expectAssertion(child, sf);
6310
6668
  if (a) assertions.push(a);
6311
6669
  }
6312
- ts6.forEachChild(child, collect2);
6670
+ ts7.forEachChild(child, collect2);
6313
6671
  };
6314
6672
  collect2(cb);
6315
6673
  const identity2 = [...suites, title].join(" > ");
@@ -6319,7 +6677,7 @@ function blocks(src, path) {
6319
6677
  return;
6320
6678
  }
6321
6679
  }
6322
- ts6.forEachChild(node, (child) => visit(child, suites));
6680
+ ts7.forEachChild(node, (child) => visit(child, suites));
6323
6681
  };
6324
6682
  visit(sf, []);
6325
6683
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.17.0",
3
+ "version": "2.17.1",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",