tamperward 2.23.6 → 2.23.8

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 (2) hide show
  1. package/dist/cli/index.js +527 -221
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2053,7 +2053,135 @@ var init_lint_suppression = __esm({
2053
2053
  }
2054
2054
  });
2055
2055
 
2056
+ // src/detectors/repo.ts
2057
+ import { execFileSync as execFileSync5 } from "node:child_process";
2058
+ import { readFileSync as readFileSync3 } from "node:fs";
2059
+ import { join as join6 } from "node:path";
2060
+ function trackedFiles(ctx) {
2061
+ if (!ctx) return null;
2062
+ if (ctx.trackedFiles) return ctx.trackedFiles;
2063
+ if (!ctx.cwd) return null;
2064
+ const hit = cache2.get(ctx.cwd);
2065
+ if (hit !== void 0) return hit;
2066
+ let files = null;
2067
+ try {
2068
+ files = execFileSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
2069
+ cwd: ctx.cwd,
2070
+ encoding: "utf8",
2071
+ maxBuffer: 64 * 1024 * 1024,
2072
+ stdio: ["ignore", "pipe", "pipe"]
2073
+ }).split("\0").filter(Boolean);
2074
+ } catch {
2075
+ files = null;
2076
+ }
2077
+ cache2.set(ctx.cwd, files);
2078
+ return files;
2079
+ }
2080
+ function trackedContent(path, ctx) {
2081
+ if (!ctx) return null;
2082
+ if (ctx.trackedContents) return ctx.trackedContents[path] ?? null;
2083
+ if (!ctx.cwd) return null;
2084
+ const key2 = ctx.cwd + "\0" + path;
2085
+ const hit = contentCache.get(key2);
2086
+ if (hit !== void 0) return hit;
2087
+ let src = null;
2088
+ try {
2089
+ src = readFileSync3(join6(ctx.cwd, path), "utf8");
2090
+ } catch {
2091
+ src = null;
2092
+ }
2093
+ contentCache.set(key2, src);
2094
+ return src;
2095
+ }
2096
+ function branchRefs(ctx) {
2097
+ if (!ctx?.cwd) return null;
2098
+ const hit = refsCache.get(ctx.cwd);
2099
+ if (hit !== void 0) return hit;
2100
+ let refs = null;
2101
+ try {
2102
+ const out3 = execFileSync5("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes"], {
2103
+ cwd: ctx.cwd,
2104
+ encoding: "utf8",
2105
+ stdio: ["ignore", "pipe", "pipe"]
2106
+ });
2107
+ refs = new Set(out3.split("\n").map((l) => l.trim()).filter(Boolean));
2108
+ } catch {
2109
+ refs = null;
2110
+ }
2111
+ refsCache.set(ctx.cwd, refs);
2112
+ return refs;
2113
+ }
2114
+ function branchExists(name, ctx) {
2115
+ const refs = branchRefs(ctx);
2116
+ if (!refs) return null;
2117
+ if (refs.has(name)) return true;
2118
+ for (const r of refs) if (r.endsWith("/" + name)) return true;
2119
+ return false;
2120
+ }
2121
+ function revIsHead(rev2, ctx) {
2122
+ if (!ctx?.cwd || rev2.startsWith("-")) return null;
2123
+ const parse = (r) => {
2124
+ try {
2125
+ return execFileSync5("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
2126
+ cwd: ctx.cwd,
2127
+ encoding: "utf8",
2128
+ stdio: ["ignore", "pipe", "pipe"]
2129
+ }).trim();
2130
+ } catch {
2131
+ return null;
2132
+ }
2133
+ };
2134
+ const head = parse("HEAD");
2135
+ const target = parse(rev2);
2136
+ if (!head || !target) return null;
2137
+ return head === target;
2138
+ }
2139
+ function defaultBranch(ctx) {
2140
+ if (!ctx?.cwd) return null;
2141
+ const hit = branchCache.get(ctx.cwd);
2142
+ if (hit !== void 0) return hit;
2143
+ let name = null;
2144
+ try {
2145
+ const ref = execFileSync5("git", ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"], {
2146
+ cwd: ctx.cwd,
2147
+ encoding: "utf8",
2148
+ stdio: ["ignore", "pipe", "pipe"]
2149
+ }).trim();
2150
+ name = ref.replace(/^origin\//, "") || null;
2151
+ } catch {
2152
+ name = null;
2153
+ }
2154
+ branchCache.set(ctx.cwd, name);
2155
+ return name;
2156
+ }
2157
+ function containsProtected(token, policy, category, ctx, except) {
2158
+ const dir = dirOf(token);
2159
+ if (!dir || dir === "." || dir === "/" || dir.startsWith("-")) return false;
2160
+ const counts = (p) => isProtected(p, policy, category) && !(except && isProtected(p, policy, except));
2161
+ const files = trackedFiles(ctx);
2162
+ if (files) {
2163
+ const prefix = dir + "/";
2164
+ return files.some((f) => f.startsWith(prefix) && counts(f));
2165
+ }
2166
+ if (counts(dir + "/probe")) return true;
2167
+ const base = dir.split("/").pop() ?? "";
2168
+ return category === "tests" && /^(?:test|tests|__tests__|spec|specs)$/i.test(base);
2169
+ }
2170
+ var cache2, contentCache, branchCache, refsCache, dirOf;
2171
+ var init_repo = __esm({
2172
+ "src/detectors/repo.ts"() {
2173
+ "use strict";
2174
+ init_policy();
2175
+ cache2 = /* @__PURE__ */ new Map();
2176
+ contentCache = /* @__PURE__ */ new Map();
2177
+ branchCache = /* @__PURE__ */ new Map();
2178
+ refsCache = /* @__PURE__ */ new Map();
2179
+ dirOf = (t) => t.replace(/^\.\//, "").replace(/\/+$/, "");
2180
+ }
2181
+ });
2182
+
2056
2183
  // src/detectors/test-skip.ts
2184
+ import { posix as posix2 } from "node:path";
2057
2185
  function matchesOutsideString(p, content, lang) {
2058
2186
  if (p.comment) return p.re.test(content);
2059
2187
  const re = p.re.global ? p.re : new RegExp(p.re.source, p.re.flags + "g");
@@ -2123,61 +2251,29 @@ function astContext(path, source) {
2123
2251
  function symbolAt(ctx, node) {
2124
2252
  return ctx.checker.getSymbolAtLocation(node) ?? null;
2125
2253
  }
2126
- function importAliases(ctx) {
2127
- const out3 = /* @__PURE__ */ new Map();
2128
- const simpleAliases = [];
2129
- const add = (id, causes = [id]) => {
2130
- const sym = symbolAt(ctx, id);
2131
- if (sym) out3.set(sym, { causes });
2132
- };
2133
- for (const stmt of ctx.sf.statements) {
2134
- if (ts.isImportDeclaration(stmt) && ts.isStringLiteralLike(stmt.moduleSpecifier)) {
2135
- if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
2136
- const clause = stmt.importClause;
2137
- if (!clause) continue;
2138
- if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
2139
- const bindings = clause.namedBindings;
2140
- if (bindings && ts.isNamedImports(bindings)) {
2141
- for (const el of bindings.elements) {
2142
- const imported = el.propertyName?.text ?? el.name.text;
2143
- if (JS_RUNNERS.has(imported)) add(el.name);
2144
- }
2145
- }
2146
- continue;
2147
- }
2148
- if (!ts.isVariableStatement(stmt)) continue;
2149
- for (const decl of stmt.declarationList.declarations) {
2150
- if (ts.isObjectBindingPattern(decl.name) && decl.initializer) {
2151
- const mod = requiredModule(decl.initializer);
2152
- if (!mod || !JS_TEST_MODULES.has(mod)) continue;
2153
- for (const el of decl.name.elements) {
2154
- if (!ts.isIdentifier(el.name)) continue;
2155
- const imported = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
2156
- if (JS_RUNNERS.has(imported)) add(el.name);
2157
- }
2158
- } else if (ts.isIdentifier(decl.name) && decl.initializer && ts.isIdentifier(decl.initializer)) {
2159
- simpleAliases.push({ local: decl.name, source: decl.initializer });
2160
- }
2161
- }
2162
- }
2163
- for (let pass = 0; pass < simpleAliases.length + 1; pass++) {
2164
- let changed = false;
2165
- for (const alias of simpleAliases) {
2166
- const localSym = symbolAt(ctx, alias.local);
2167
- if (!localSym || out3.has(localSym)) continue;
2168
- const sourceSym = symbolAt(ctx, alias.source);
2169
- const sourceBinding = sourceSym ? out3.get(sourceSym) : void 0;
2170
- const implicitGlobal = sourceSym == null && JS_RUNNERS.has(alias.source.text);
2171
- if (!sourceBinding && !implicitGlobal) continue;
2172
- out3.set(localSym, {
2173
- causes: [alias.local, ...sourceBinding?.causes ?? []]
2174
- });
2175
- changed = true;
2176
- }
2177
- if (!changed) break;
2254
+ function relativeCandidates(fromPath, spec) {
2255
+ const base = posix2.normalize(posix2.join(posix2.dirname(fromPath), spec));
2256
+ if (base.startsWith("../") || base.startsWith("/")) return [];
2257
+ const bases = [base];
2258
+ const swapped = base.replace(/\.([mc]?)js$/, ".$1ts");
2259
+ if (swapped !== base) bases.push(swapped);
2260
+ const out3 = [];
2261
+ for (const b of bases) {
2262
+ out3.push(b);
2263
+ for (const ext of MODULE_EXTENSIONS) out3.push(b + ext);
2264
+ for (const ext of MODULE_EXTENSIONS) out3.push(b + "/index" + ext);
2178
2265
  }
2179
2266
  return out3;
2180
2267
  }
2268
+ function isExported(stmt) {
2269
+ return ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
2270
+ }
2271
+ function cjsExportTarget(expr) {
2272
+ const isModuleExports = (e) => ts.isIdentifier(e) && e.text === "exports" || ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === "module" && e.name.text === "exports";
2273
+ if (isModuleExports(expr)) return "";
2274
+ if (ts.isPropertyAccessExpression(expr) && isModuleExports(expr.expression)) return expr.name.text;
2275
+ return null;
2276
+ }
2181
2277
  function topLevelStaticStrings(ctx) {
2182
2278
  const pending = /* @__PURE__ */ new Map();
2183
2279
  for (const stmt of ctx.sf.statements) {
@@ -2244,7 +2340,7 @@ function staticPropertyName(expr, ctx, strings) {
2244
2340
  }
2245
2341
  return null;
2246
2342
  }
2247
- function runnerChain(expr, ctx, runners, strings) {
2343
+ function runnerChain(expr, ctx, binder, strings) {
2248
2344
  const props = [];
2249
2345
  const causes = [];
2250
2346
  let cur = expr;
@@ -2264,16 +2360,29 @@ function runnerChain(expr, ctx, runners, strings) {
2264
2360
  if (props.length === 1) terminalNode = cur.argumentExpression;
2265
2361
  cur = cur.expression;
2266
2362
  }
2267
- if (!ts.isIdentifier(cur)) return null;
2268
- const sym = symbolAt(ctx, cur);
2269
- if (sym) {
2270
- const binding = runners.get(sym);
2271
- if (!binding) return null;
2272
- causes.push(...binding.causes);
2273
- } else if (!JS_RUNNERS.has(cur.text)) {
2274
- return null;
2363
+ let verdict2;
2364
+ let namespace = null;
2365
+ if (ts.isIdentifier(cur)) {
2366
+ const sym = symbolAt(ctx, cur);
2367
+ if (!sym) {
2368
+ verdict2 = JS_RUNNERS.has(cur.text) ? RUNNER : UNKNOWN;
2369
+ } else {
2370
+ verdict2 = binder.classify(sym);
2371
+ if (verdict2.kind !== "runner") namespace = binder.namespaceOf(sym);
2372
+ }
2373
+ } else {
2374
+ namespace = requiredModule(cur);
2375
+ verdict2 = namespace == null ? UNKNOWN : NON_RUNNER;
2376
+ }
2377
+ let root = ts.isIdentifier(cur) ? cur.text : "";
2378
+ if (namespace != null && props.length > 0) {
2379
+ verdict2 = withCause(binder.memberOf(namespace, props[0]), cur);
2380
+ if (verdict2.kind === "runner") root = props.shift() ?? root;
2275
2381
  }
2276
- return { root: cur.text, props, terminalNode, causes };
2382
+ if (verdict2.kind === "unknown") return "unknown";
2383
+ if (verdict2.kind === "non-runner") return null;
2384
+ causes.push(...verdict2.causes);
2385
+ return { root, props, terminalNode, causes };
2277
2386
  }
2278
2387
  function optionDisables(value) {
2279
2388
  return !(value.kind === ts.SyntaxKind.FalseKeyword || ts.isNumericLiteral(value) && Number(value.text) === 0);
@@ -2292,11 +2401,12 @@ function structuralNodeKey(node, sf) {
2292
2401
  }
2293
2402
  return `${node.kind}(${children.map((child) => structuralNodeKey(child, sf)).join(",")})`;
2294
2403
  }
2295
- function semanticSkipHits(ctx) {
2296
- const runners = importAliases(ctx);
2404
+ function semanticSkipHits(ctx, binder) {
2297
2405
  const strings = topLevelStaticStrings(ctx);
2298
2406
  const hits = [];
2407
+ const unowned = /* @__PURE__ */ new Set();
2299
2408
  const callOrdinals = /* @__PURE__ */ new Map();
2409
+ const lineOf = (pos) => ctx.sf.getLineAndCharacterOfPosition(pos).line + 1;
2300
2410
  const visit = (node) => {
2301
2411
  if (ts.isCallExpression(node)) {
2302
2412
  const callText = node.getText(ctx.sf).trim();
@@ -2304,8 +2414,13 @@ function semanticSkipHits(ctx) {
2304
2414
  const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
2305
2415
  callOrdinals.set(callIdentity, ordinal);
2306
2416
  const callKey = `${callIdentity}\0${ordinal}`;
2307
- const chain = runnerChain(node.expression, ctx, runners, strings);
2308
- if (chain) {
2417
+ const chain = runnerChain(node.expression, ctx, binder, strings);
2418
+ if (chain === "unknown") {
2419
+ const callee = node.expression;
2420
+ const first = lineOf(callee.getStart(ctx.sf));
2421
+ const last = lineOf(callee.getEnd());
2422
+ for (let line = first; line <= last; line++) unowned.add(line);
2423
+ } else if (chain) {
2309
2424
  const { props, terminalNode, causes } = chain;
2310
2425
  const terminal = props.at(-1);
2311
2426
  const push = (target, extraCauses, why) => {
@@ -2317,7 +2432,7 @@ function semanticSkipHits(ctx) {
2317
2432
  evidence: callText
2318
2433
  });
2319
2434
  };
2320
- if (terminal && (terminal === "skip" || terminal === "only" || terminal === "todo") && props.slice(0, -1).every((p) => JS_CHAIN_MODIFIERS.has(p))) {
2435
+ if (terminal && (terminal === "skip" || terminal === "only" || terminal === "todo") && props.slice(0, -1).every((p) => JS_CHAIN_MODIFIERS.has(p) || JS_RUNNERS.has(p))) {
2321
2436
  push(terminalNode, [], "a .skip/.only/.todo marker");
2322
2437
  } else if (terminal && ["skipIf", "runIf"].includes(terminal) && props.length === 1) {
2323
2438
  push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
@@ -2356,20 +2471,32 @@ function semanticSkipHits(ctx) {
2356
2471
  ts.forEachChild(node, visit);
2357
2472
  };
2358
2473
  visit(ctx.sf);
2359
- return hits;
2474
+ return { hits, unowned };
2475
+ }
2476
+ function moduleSource(changes, side, ctx) {
2477
+ const touched = /* @__PURE__ */ new Map();
2478
+ for (const c of changes) {
2479
+ if (c.kind !== "file") continue;
2480
+ touched.set(c.path, c[side]);
2481
+ if (side === "before" && c.oldPath != null) touched.set(c.oldPath, c.before);
2482
+ }
2483
+ return (path) => touched.has(path) ? touched.get(path) ?? null : trackedContent(path, ctx);
2360
2484
  }
2361
- function astSkipHits(c) {
2362
- if (c.after == null) return { hits: [], authoritative: false };
2485
+ function astSkipHits(c, resolvers) {
2486
+ if (c.after == null) return NO_AST;
2363
2487
  const added = addedLineNumbers(c);
2364
- if (added.size === 0) return { hits: [], authoritative: false };
2488
+ if (added.size === 0) return NO_AST;
2365
2489
  const afterCtx = astContext(c.path, c.after);
2366
- if (!afterCtx) return { hits: [], authoritative: false };
2490
+ if (!afterCtx) return NO_AST;
2367
2491
  const beforeCtx = c.before == null ? null : astContext(c.path, c.before);
2368
- const beforeKeys = beforeCtx == null ? null : new Set(semanticSkipHits(beforeCtx).map((hit) => hit.semanticKey));
2492
+ const beforeKeys = beforeCtx == null ? null : new Set(
2493
+ semanticSkipHits(beforeCtx, resolvers.before.binderFor(c.oldPath ?? c.path, beforeCtx)).hits.map((hit) => hit.semanticKey)
2494
+ );
2369
2495
  const lineOf = (node) => afterCtx.sf.getLineAndCharacterOfPosition(node.getStart(afterCtx.sf)).line + 1;
2370
2496
  const hits = [];
2371
2497
  const seen = /* @__PURE__ */ new Set();
2372
- for (const hit of semanticSkipHits(afterCtx)) {
2498
+ const analysis = semanticSkipHits(afterCtx, resolvers.after.binderFor(c.path, afterCtx));
2499
+ for (const hit of analysis.hits) {
2373
2500
  const directLine = lineOf(hit.terminalNode);
2374
2501
  let findingLine = added.has(directLine) ? directLine : null;
2375
2502
  if (findingLine == null) {
@@ -2389,21 +2516,22 @@ function astSkipHits(c) {
2389
2516
  seen.add(dedupe);
2390
2517
  hits.push({ line: findingLine, why: hit.why, evidence: hit.evidence });
2391
2518
  }
2392
- return { hits, authoritative: true };
2519
+ return { hits, authoritative: true, unowned: analysis.unowned };
2393
2520
  }
2394
- var RULE4, acc, JS_RUNNER, JS_MOD, PATTERNS2, JS_RUNNERS, JS_CHAIN_MODIFIERS, JS_TEST_MODULES, testSkip;
2521
+ var RULE4, acc, JS_RUNNER, JS_MOD, PATTERNS2, JS_RUNNERS, JS_CHAIN_MODIFIERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, RUNNER, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
2395
2522
  var init_test_skip = __esm({
2396
2523
  "src/detectors/test-skip.ts"() {
2397
2524
  "use strict";
2398
2525
  init_ts_lazy();
2399
2526
  init_select();
2400
2527
  init_policy();
2528
+ init_repo();
2401
2529
  init_files();
2402
2530
  init_finding();
2403
2531
  RULE4 = "test-skip";
2404
2532
  acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
2405
2533
  JS_RUNNER = "\\b(?:it|test|describe|suite)";
2406
- JS_MOD = "concurrent|sequential|shuffle";
2534
+ JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
2407
2535
  PATTERNS2 = {
2408
2536
  js: [
2409
2537
  // The skip/only/todo family on a runner, by dot-or-bracket access, with one optional
@@ -2482,7 +2610,8 @@ var init_test_skip = __esm({
2482
2610
  ]
2483
2611
  };
2484
2612
  JS_RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
2485
- JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle"]);
2613
+ JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle", "serial", "parallel"]);
2614
+ JS_DEFAULT_RUNNER_MODULES = /* @__PURE__ */ new Set(["node:test", "node:test/promises", "@playwright/test"]);
2486
2615
  JS_TEST_MODULES = /* @__PURE__ */ new Set([
2487
2616
  "vitest",
2488
2617
  "@jest/globals",
@@ -2492,12 +2621,272 @@ var init_test_skip = __esm({
2492
2621
  "bun:test",
2493
2622
  "@playwright/test"
2494
2623
  ]);
2624
+ RUNNER = { kind: "runner", causes: [] };
2625
+ NON_RUNNER = { kind: "non-runner" };
2626
+ UNKNOWN = { kind: "unknown" };
2627
+ withCause = (v, cause) => v.kind === "runner" ? { kind: "runner", causes: [cause, ...v.causes] } : v;
2628
+ MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
2629
+ RunnerResolver = class _RunnerResolver {
2630
+ constructor(source) {
2631
+ this.source = source;
2632
+ }
2633
+ binders = /* @__PURE__ */ new Map();
2634
+ static MAX_DEPTH = 4;
2635
+ binderFor(path, ctx) {
2636
+ const hit = this.binders.get(path);
2637
+ if (hit && hit.ctx === ctx) return hit;
2638
+ const binder = new ModuleBinder(ctx, path, this, 0);
2639
+ this.binders.set(path, binder);
2640
+ return binder;
2641
+ }
2642
+ /** The verdict for `name` exported by `spec` as imported from `fromPath`. */
2643
+ exportOf(fromPath, spec, name, depth) {
2644
+ if (JS_TEST_MODULES.has(spec)) {
2645
+ if (name === "default") return JS_DEFAULT_RUNNER_MODULES.has(spec) ? RUNNER : UNKNOWN;
2646
+ return JS_RUNNERS.has(name) ? RUNNER : NON_RUNNER;
2647
+ }
2648
+ if (!spec.startsWith("./") && !spec.startsWith("../")) return UNKNOWN;
2649
+ if (depth >= _RunnerResolver.MAX_DEPTH) return UNKNOWN;
2650
+ for (const candidate of relativeCandidates(fromPath, spec)) {
2651
+ if (this.binders.has(candidate)) {
2652
+ const cached = this.binders.get(candidate);
2653
+ return cached ? cached.exported(name) : UNKNOWN;
2654
+ }
2655
+ const src = this.source(candidate);
2656
+ if (src == null) continue;
2657
+ const ctx = astContext(candidate, src);
2658
+ const binder = ctx ? new ModuleBinder(ctx, candidate, this, depth + 1) : null;
2659
+ this.binders.set(candidate, binder);
2660
+ return binder ? binder.exported(name) : UNKNOWN;
2661
+ }
2662
+ return UNKNOWN;
2663
+ }
2664
+ };
2665
+ ModuleBinder = class {
2666
+ constructor(ctx, path, resolver, depth) {
2667
+ this.ctx = ctx;
2668
+ this.path = path;
2669
+ this.resolver = resolver;
2670
+ this.depth = depth;
2671
+ }
2672
+ memo = /* @__PURE__ */ new Map();
2673
+ resolving = /* @__PURE__ */ new Set();
2674
+ exports = null;
2675
+ starExports = [];
2676
+ opaqueCjs = false;
2677
+ moduleExport(spec, name) {
2678
+ return this.resolver.exportOf(this.path, spec, name, this.depth);
2679
+ }
2680
+ /** The verdict for member `name` of the module object `spec` denotes. */
2681
+ memberOf(spec, name) {
2682
+ return this.moduleExport(spec, name);
2683
+ }
2684
+ /** The module specifier a symbol stands for as a namespace object
2685
+ * (`import * as v from 'vitest'`, `const v = require('vitest')`), else null. */
2686
+ namespaceOf(sym) {
2687
+ const decl = sym.valueDeclaration ?? sym.declarations?.[0];
2688
+ if (!decl) return null;
2689
+ if (ts.isNamespaceImport(decl)) {
2690
+ const imp = decl.parent.parent;
2691
+ return ts.isImportDeclaration(imp) ? moduleName(imp.moduleSpecifier) : null;
2692
+ }
2693
+ if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) {
2694
+ return requiredModule(decl.initializer);
2695
+ }
2696
+ return null;
2697
+ }
2698
+ /** The verdict for a root identifier's symbol. */
2699
+ classify(sym) {
2700
+ const cached = this.memo.get(sym);
2701
+ if (cached) return cached;
2702
+ if (this.resolving.has(sym)) return UNKNOWN;
2703
+ this.resolving.add(sym);
2704
+ const verdict2 = this.classifyDeclaration(sym);
2705
+ this.resolving.delete(sym);
2706
+ this.memo.set(sym, verdict2);
2707
+ return verdict2;
2708
+ }
2709
+ classifyDeclaration(sym) {
2710
+ const decl = sym.valueDeclaration ?? sym.declarations?.[0];
2711
+ if (!decl) return UNKNOWN;
2712
+ if (ts.isImportSpecifier(decl)) {
2713
+ const imp = decl.parent.parent.parent;
2714
+ const spec = ts.isImportDeclaration(imp) ? moduleName(imp.moduleSpecifier) : null;
2715
+ if (spec == null) return UNKNOWN;
2716
+ return withCause(this.moduleExport(spec, (decl.propertyName ?? decl.name).text), decl.name);
2717
+ }
2718
+ if (ts.isImportClause(decl)) {
2719
+ const spec = ts.isImportDeclaration(decl.parent) ? moduleName(decl.parent.moduleSpecifier) : null;
2720
+ if (spec == null || !decl.name) return UNKNOWN;
2721
+ return withCause(this.moduleExport(spec, "default"), decl.name);
2722
+ }
2723
+ if (ts.isNamespaceImport(decl) || ts.isParameter(decl) || ts.isFunctionDeclaration(decl) || ts.isClassDeclaration(decl) || ts.isEnumDeclaration(decl) || ts.isModuleDeclaration(decl)) return NON_RUNNER;
2724
+ if (ts.isBindingElement(decl)) return this.classifyBindingElement(decl);
2725
+ if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) {
2726
+ if (!decl.initializer) return UNKNOWN;
2727
+ return withCause(this.classifyExpression(decl.initializer), decl.name);
2728
+ }
2729
+ return UNKNOWN;
2730
+ }
2731
+ /** `const { it } = require('vitest')`, `const { test } = fixtures`: the element
2732
+ * is what the module (or namespace) exports under its property name. */
2733
+ classifyBindingElement(decl) {
2734
+ const pattern = decl.parent;
2735
+ const owner = pattern.parent;
2736
+ if (ts.isParameter(owner) || !ts.isObjectBindingPattern(pattern) || !ts.isIdentifier(decl.name)) {
2737
+ return NON_RUNNER;
2738
+ }
2739
+ if (!ts.isVariableDeclaration(owner) || !owner.initializer) return UNKNOWN;
2740
+ const key2 = decl.propertyName ? ts.isIdentifier(decl.propertyName) || ts.isStringLiteralLike(decl.propertyName) ? decl.propertyName.text : null : decl.name.text;
2741
+ if (key2 == null) return UNKNOWN;
2742
+ const spec = this.namespaceSpec(owner.initializer);
2743
+ if (spec != null) return withCause(this.moduleExport(spec, key2), decl.name);
2744
+ if (ts.isIdentifier(owner.initializer)) {
2745
+ const inner = symbolAt(this.ctx, owner.initializer);
2746
+ const verdict2 = inner ? this.classify(inner) : UNKNOWN;
2747
+ return verdict2.kind === "unknown" ? UNKNOWN : NON_RUNNER;
2748
+ }
2749
+ return NON_RUNNER;
2750
+ }
2751
+ /** The module specifier `expr` denotes as a namespace object, else null. */
2752
+ namespaceSpec(expr) {
2753
+ const direct = requiredModule(expr);
2754
+ if (direct != null) return direct;
2755
+ if (!ts.isIdentifier(expr)) return null;
2756
+ const sym = symbolAt(this.ctx, expr);
2757
+ return sym ? this.namespaceOf(sym) : null;
2758
+ }
2759
+ /** The verdict for an expression used as a runner: an initialiser or a
2760
+ * re-exported value. */
2761
+ classifyExpression(expr) {
2762
+ if (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr) || ts.isTypeAssertionExpression(expr) || ts.isNonNullExpression(expr)) return this.classifyExpression(expr.expression);
2763
+ if (ts.isIdentifier(expr)) {
2764
+ const sym = symbolAt(this.ctx, expr);
2765
+ if (sym) return this.classify(sym);
2766
+ return JS_RUNNERS.has(expr.text) ? RUNNER : UNKNOWN;
2767
+ }
2768
+ if (ts.isCallExpression(expr)) {
2769
+ const spec = requiredModule(expr);
2770
+ if (spec != null) return this.moduleExport(spec, "default");
2771
+ const callee = expr.expression;
2772
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === "extend") {
2773
+ return this.classifyExpression(callee.expression);
2774
+ }
2775
+ return NON_RUNNER;
2776
+ }
2777
+ if (ts.isPropertyAccessExpression(expr)) {
2778
+ const spec = this.namespaceSpec(expr.expression);
2779
+ return spec != null ? this.moduleExport(spec, expr.name.text) : UNKNOWN;
2780
+ }
2781
+ if (ts.isElementAccessExpression(expr)) {
2782
+ const spec = this.namespaceSpec(expr.expression);
2783
+ const name = ts.isStringLiteralLike(expr.argumentExpression) ? expr.argumentExpression.text : null;
2784
+ return spec != null && name != null ? this.moduleExport(spec, name) : UNKNOWN;
2785
+ }
2786
+ if (ts.isLiteralExpression(expr) || ts.isTemplateExpression(expr) || ts.isObjectLiteralExpression(expr) || ts.isArrayLiteralExpression(expr) || ts.isFunctionExpression(expr) || ts.isArrowFunction(expr) || ts.isClassExpression(expr) || ts.isNewExpression(expr) || ts.isBinaryExpression(expr) || ts.isPrefixUnaryExpression(expr) || ts.isPostfixUnaryExpression(expr) || ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr) || expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword || expr.kind === ts.SyntaxKind.NullKeyword) return NON_RUNNER;
2787
+ return UNKNOWN;
2788
+ }
2789
+ /** The verdict for what this module exports under `name` (`default` for the
2790
+ * default export or a whole-module `module.exports = …`). */
2791
+ exported(name) {
2792
+ if (!this.exports) this.collectExports();
2793
+ const local = this.exports?.get(name);
2794
+ if (local) return local();
2795
+ for (const spec of this.starExports) {
2796
+ const v = this.moduleExport(spec, name);
2797
+ if (v.kind !== "non-runner") return v;
2798
+ }
2799
+ return this.opaqueCjs ? UNKNOWN : NON_RUNNER;
2800
+ }
2801
+ collectExports() {
2802
+ const exports = /* @__PURE__ */ new Map();
2803
+ this.exports = exports;
2804
+ const ctx = this.ctx;
2805
+ const bySymbol = (id) => () => {
2806
+ const sym = symbolAt(ctx, id);
2807
+ return sym ? this.classify(sym) : UNKNOWN;
2808
+ };
2809
+ for (const stmt of ctx.sf.statements) {
2810
+ if (ts.isExportDeclaration(stmt)) {
2811
+ const spec = stmt.moduleSpecifier ? moduleName(stmt.moduleSpecifier) : null;
2812
+ if (!stmt.exportClause) {
2813
+ if (spec != null) this.starExports.push(spec);
2814
+ else this.opaqueCjs = true;
2815
+ continue;
2816
+ }
2817
+ if (!ts.isNamedExports(stmt.exportClause)) continue;
2818
+ for (const el of stmt.exportClause.elements) {
2819
+ const exportedName = el.name.text;
2820
+ const localName = (el.propertyName ?? el.name).text;
2821
+ if (spec != null) {
2822
+ exports.set(exportedName, () => this.moduleExport(spec, localName));
2823
+ } else {
2824
+ exports.set(exportedName, () => {
2825
+ const sym = ctx.checker.getExportSpecifierLocalTargetSymbol(el);
2826
+ return sym ? this.classify(sym) : UNKNOWN;
2827
+ });
2828
+ }
2829
+ }
2830
+ continue;
2831
+ }
2832
+ if (ts.isExportAssignment(stmt)) {
2833
+ exports.set("default", () => this.classifyExpression(stmt.expression));
2834
+ continue;
2835
+ }
2836
+ if (isExported(stmt)) {
2837
+ if (ts.isVariableStatement(stmt)) {
2838
+ for (const decl of stmt.declarationList.declarations) {
2839
+ if (ts.isIdentifier(decl.name)) exports.set(decl.name.text, bySymbol(decl.name));
2840
+ else if (ts.isObjectBindingPattern(decl.name)) {
2841
+ for (const el of decl.name.elements) {
2842
+ if (ts.isIdentifier(el.name)) exports.set(el.name.text, bySymbol(el.name));
2843
+ }
2844
+ }
2845
+ }
2846
+ } else if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name) {
2847
+ exports.set(stmt.name.text, () => NON_RUNNER);
2848
+ }
2849
+ continue;
2850
+ }
2851
+ if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
2852
+ const target = cjsExportTarget(stmt.expression.left);
2853
+ if (target == null) continue;
2854
+ const value = stmt.expression.right;
2855
+ if (target !== "") {
2856
+ exports.set(target, () => this.classifyExpression(value));
2857
+ } else if (ts.isObjectLiteralExpression(value)) {
2858
+ for (const prop of value.properties) {
2859
+ if (ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name))) {
2860
+ const init = prop.initializer;
2861
+ exports.set(prop.name.text, () => this.classifyExpression(init));
2862
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
2863
+ exports.set(prop.name.text, () => {
2864
+ const sym = ctx.checker.getShorthandAssignmentValueSymbol(prop);
2865
+ return sym ? this.classify(sym) : UNKNOWN;
2866
+ });
2867
+ } else {
2868
+ this.opaqueCjs = true;
2869
+ }
2870
+ }
2871
+ } else {
2872
+ exports.set("default", () => this.classifyExpression(value));
2873
+ this.opaqueCjs = true;
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+ };
2879
+ NO_AST = { hits: [], authoritative: false, unowned: /* @__PURE__ */ new Set() };
2495
2880
  testSkip = {
2496
2881
  id: RULE4,
2497
2882
  surface: ["file"],
2498
2883
  certainty: "mechanical",
2499
- run(changes, policy) {
2884
+ run(changes, policy, _view, ctx) {
2500
2885
  const out3 = [];
2886
+ const resolvers = {
2887
+ before: new RunnerResolver(moduleSource(changes, "before", ctx)),
2888
+ after: new RunnerResolver(moduleSource(changes, "after", ctx))
2889
+ };
2501
2890
  for (const c of changes) {
2502
2891
  if (c.kind !== "file") continue;
2503
2892
  if (!isProtected(c.path, policy, "tests")) continue;
@@ -2505,9 +2894,11 @@ var init_test_skip = __esm({
2505
2894
  const patterns = PATTERNS2[lang ?? "js"];
2506
2895
  const astHitLines = /* @__PURE__ */ new Set();
2507
2896
  let astAuthoritative = false;
2897
+ let unowned = /* @__PURE__ */ new Set();
2508
2898
  if (lang === "js" && c.after != null) {
2509
- const analysis = astSkipHits(c);
2899
+ const analysis = astSkipHits(c, resolvers);
2510
2900
  astAuthoritative = analysis.authoritative;
2901
+ unowned = analysis.unowned;
2511
2902
  for (const hit of analysis.hits) {
2512
2903
  astHitLines.add(hit.line);
2513
2904
  out3.push(
@@ -2525,7 +2916,7 @@ var init_test_skip = __esm({
2525
2916
  if (l.newLine != null && astHitLines.has(l.newLine)) continue;
2526
2917
  const comment = isCommentLine(l.content.trim(), lang);
2527
2918
  for (const p of patterns) {
2528
- if (astAuthoritative && p.astOwned) continue;
2919
+ if (astAuthoritative && p.astOwned && !(l.newLine != null && unowned.has(l.newLine))) continue;
2529
2920
  if (comment && !p.comment) continue;
2530
2921
  if (matchesOutsideString(p, l.content, lang)) {
2531
2922
  out3.push(
@@ -2548,133 +2939,6 @@ var init_test_skip = __esm({
2548
2939
  }
2549
2940
  });
2550
2941
 
2551
- // src/detectors/repo.ts
2552
- import { execFileSync as execFileSync5 } from "node:child_process";
2553
- import { readFileSync as readFileSync3 } from "node:fs";
2554
- import { join as join6 } from "node:path";
2555
- function trackedFiles(ctx) {
2556
- if (!ctx) return null;
2557
- if (ctx.trackedFiles) return ctx.trackedFiles;
2558
- if (!ctx.cwd) return null;
2559
- const hit = cache2.get(ctx.cwd);
2560
- if (hit !== void 0) return hit;
2561
- let files = null;
2562
- try {
2563
- files = execFileSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
2564
- cwd: ctx.cwd,
2565
- encoding: "utf8",
2566
- maxBuffer: 64 * 1024 * 1024,
2567
- stdio: ["ignore", "pipe", "pipe"]
2568
- }).split("\0").filter(Boolean);
2569
- } catch {
2570
- files = null;
2571
- }
2572
- cache2.set(ctx.cwd, files);
2573
- return files;
2574
- }
2575
- function trackedContent(path, ctx) {
2576
- if (!ctx) return null;
2577
- if (ctx.trackedContents) return ctx.trackedContents[path] ?? null;
2578
- if (!ctx.cwd) return null;
2579
- const key2 = ctx.cwd + "\0" + path;
2580
- const hit = contentCache.get(key2);
2581
- if (hit !== void 0) return hit;
2582
- let src = null;
2583
- try {
2584
- src = readFileSync3(join6(ctx.cwd, path), "utf8");
2585
- } catch {
2586
- src = null;
2587
- }
2588
- contentCache.set(key2, src);
2589
- return src;
2590
- }
2591
- function branchRefs(ctx) {
2592
- if (!ctx?.cwd) return null;
2593
- const hit = refsCache.get(ctx.cwd);
2594
- if (hit !== void 0) return hit;
2595
- let refs = null;
2596
- try {
2597
- const out3 = execFileSync5("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes"], {
2598
- cwd: ctx.cwd,
2599
- encoding: "utf8",
2600
- stdio: ["ignore", "pipe", "pipe"]
2601
- });
2602
- refs = new Set(out3.split("\n").map((l) => l.trim()).filter(Boolean));
2603
- } catch {
2604
- refs = null;
2605
- }
2606
- refsCache.set(ctx.cwd, refs);
2607
- return refs;
2608
- }
2609
- function branchExists(name, ctx) {
2610
- const refs = branchRefs(ctx);
2611
- if (!refs) return null;
2612
- if (refs.has(name)) return true;
2613
- for (const r of refs) if (r.endsWith("/" + name)) return true;
2614
- return false;
2615
- }
2616
- function revIsHead(rev2, ctx) {
2617
- if (!ctx?.cwd || rev2.startsWith("-")) return null;
2618
- const parse = (r) => {
2619
- try {
2620
- return execFileSync5("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
2621
- cwd: ctx.cwd,
2622
- encoding: "utf8",
2623
- stdio: ["ignore", "pipe", "pipe"]
2624
- }).trim();
2625
- } catch {
2626
- return null;
2627
- }
2628
- };
2629
- const head = parse("HEAD");
2630
- const target = parse(rev2);
2631
- if (!head || !target) return null;
2632
- return head === target;
2633
- }
2634
- function defaultBranch(ctx) {
2635
- if (!ctx?.cwd) return null;
2636
- const hit = branchCache.get(ctx.cwd);
2637
- if (hit !== void 0) return hit;
2638
- let name = null;
2639
- try {
2640
- const ref = execFileSync5("git", ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"], {
2641
- cwd: ctx.cwd,
2642
- encoding: "utf8",
2643
- stdio: ["ignore", "pipe", "pipe"]
2644
- }).trim();
2645
- name = ref.replace(/^origin\//, "") || null;
2646
- } catch {
2647
- name = null;
2648
- }
2649
- branchCache.set(ctx.cwd, name);
2650
- return name;
2651
- }
2652
- function containsProtected(token, policy, category, ctx, except) {
2653
- const dir = dirOf(token);
2654
- if (!dir || dir === "." || dir === "/" || dir.startsWith("-")) return false;
2655
- const counts = (p) => isProtected(p, policy, category) && !(except && isProtected(p, policy, except));
2656
- const files = trackedFiles(ctx);
2657
- if (files) {
2658
- const prefix = dir + "/";
2659
- return files.some((f) => f.startsWith(prefix) && counts(f));
2660
- }
2661
- if (counts(dir + "/probe")) return true;
2662
- const base = dir.split("/").pop() ?? "";
2663
- return category === "tests" && /^(?:test|tests|__tests__|spec|specs)$/i.test(base);
2664
- }
2665
- var cache2, contentCache, branchCache, refsCache, dirOf;
2666
- var init_repo = __esm({
2667
- "src/detectors/repo.ts"() {
2668
- "use strict";
2669
- init_policy();
2670
- cache2 = /* @__PURE__ */ new Map();
2671
- contentCache = /* @__PURE__ */ new Map();
2672
- branchCache = /* @__PURE__ */ new Map();
2673
- refsCache = /* @__PURE__ */ new Map();
2674
- dirOf = (t) => t.replace(/^\.\//, "").replace(/\/+$/, "");
2675
- }
2676
- });
2677
-
2678
2942
  // src/detectors/coverage-lowering.ts
2679
2943
  function keyName(name) {
2680
2944
  if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
@@ -4105,8 +4369,8 @@ function stripComment(line) {
4105
4369
  }
4106
4370
  function checkIdentity(segment) {
4107
4371
  const core = segment.trim().replace(PREFIX, "").replace(/\s+/g, " ");
4108
- const viaRunner = RUNNER.test(core);
4109
- const rest = core.replace(RUNNER, "");
4372
+ const viaRunner = RUNNER2.test(core);
4373
+ const rest = core.replace(RUNNER2, "");
4110
4374
  const tool = rest.match(TOOL)?.[1] ?? (viaRunner ? rest.match(SCRIPT)?.[1] : void 0);
4111
4375
  if (!tool) return null;
4112
4376
  const toks = rest.replace(/^tamperward@\S+/, "tamperward").split(" ");
@@ -4122,7 +4386,7 @@ function checkIdentity(segment) {
4122
4386
  return tool;
4123
4387
  }
4124
4388
  function pinOfSegment(segment) {
4125
- const m = segment.trim().replace(PREFIX, "").replace(RUNNER, "").match(/^tamperward(?:@(\S+))?\b/);
4389
+ const m = segment.trim().replace(PREFIX, "").replace(RUNNER2, "").match(/^tamperward(?:@(\S+))?\b/);
4126
4390
  return m ? m[1] ?? "" : null;
4127
4391
  }
4128
4392
  function split(stmt) {
@@ -4985,7 +5249,7 @@ function installerOf(script) {
4985
5249
  const raw = seg.trim();
4986
5250
  if (!raw) continue;
4987
5251
  if (/^(?:HUSKY|LEFTHOOK)=0\b/.test(raw)) continue;
4988
- const core = raw.replace(PREFIX, "").replace(RUNNER, "").replace(/\s+/g, " ");
5252
+ const core = raw.replace(PREFIX, "").replace(RUNNER2, "").replace(/\s+/g, " ");
4989
5253
  const m = core.match(/^(?:node\s+)?(?:\S*\/)?(husky|lefthook|simple-git-hooks|pre-commit)(?:\.js)?(?:\s+(\S+))?/);
4990
5254
  if (!m) continue;
4991
5255
  const [, tool, sub] = m;
@@ -5283,7 +5547,7 @@ function xargsWritesHook(toks, fed, policy, ctx) {
5283
5547
  if ((cmd === "sed" || cmd === "perl") && args.some((a) => /^-[A-Za-z]*i|^--in-place/.test(a))) return `xargs ${cmd} -i rewrites a protected hook named earlier in the line`;
5284
5548
  return null;
5285
5549
  }
5286
- var isComment, RUNNER, TOOL, SCRIPT, SUBCOMMAND_TOOLS, PREFIX, MODE, CWD, unq, alwaysTrue, EXIT_ZERO, EXIT_ANY, FN_DEF, SENSITIVE_ENV, assignedNames, WAIT_PID, TRANSPARENT, strip, baseIdentity, isCodeowners, GATE_CRITICAL, isDoc, isLefthook, isLefthookLocal, isPreCommitConfig, isPackageJson, GATE, str, list, redirectingEnv, off, STAGE_ALIAS, GATE_STAGES, ALL_STAGES, normStage, INSTALL_SCRIPTS, GIT_HOOKS, ownerExec, SYMBOLIC, WRAPPERS, INTERPRETERS, INLINE_FLAG, EDITORS, SCRIPT_WORD, HOOK_BASENAMES, REDIRECT_TARGET;
5550
+ var isComment, RUNNER2, TOOL, SCRIPT, SUBCOMMAND_TOOLS, PREFIX, MODE, CWD, unq, alwaysTrue, EXIT_ZERO, EXIT_ANY, FN_DEF, SENSITIVE_ENV, assignedNames, WAIT_PID, TRANSPARENT, strip, baseIdentity, isCodeowners, GATE_CRITICAL, isDoc, isLefthook, isLefthookLocal, isPreCommitConfig, isPackageJson, GATE, str, list, redirectingEnv, off, STAGE_ALIAS, GATE_STAGES, ALL_STAGES, normStage, INSTALL_SCRIPTS, GIT_HOOKS, ownerExec, SYMBOLIC, WRAPPERS, INTERPRETERS, INLINE_FLAG, EDITORS, SCRIPT_WORD, HOOK_BASENAMES, REDIRECT_TARGET;
5287
5551
  var init_hook_wiring = __esm({
5288
5552
  "src/detectors/hook-wiring.ts"() {
5289
5553
  "use strict";
@@ -5292,7 +5556,7 @@ var init_hook_wiring = __esm({
5292
5556
  init_wiring();
5293
5557
  init_repo();
5294
5558
  isComment = (line) => /^\s*#/.test(line);
5295
- RUNNER = /^(?:npx|pnpm|yarn|bunx|bun|npm)\s+(?:(?:exec|dlx|x|run)\s+)?(?:(?:--yes|-y|--no-install|--silent|-s|--)\s+)*/;
5559
+ RUNNER2 = /^(?:npx|pnpm|yarn|bunx|bun|npm)\s+(?:(?:exec|dlx|x|run)\s+)?(?:(?:--yes|-y|--no-install|--silent|-s|--)\s+)*/;
5296
5560
  TOOL = /^(tamperward|jest|vitest|eslint|tsc|lint-staged|lefthook|husky|pytest|make|cargo|go|pre-commit|mypy|ruff|rubocop|prettier|gradlew?)\b/;
5297
5561
  SCRIPT = /^(test|tests|lint|typecheck|type-check|coverage|check)\b/;
5298
5562
  SUBCOMMAND_TOOLS = /* @__PURE__ */ new Set(["tamperward", "make", "cargo", "go", "lefthook", "husky", "pre-commit"]);
@@ -6197,6 +6461,30 @@ They WILL FAIL in v10.0.0
6197
6461
  });
6198
6462
 
6199
6463
  // src/detectors/suite-config.ts
6464
+ function runnerSkips(path) {
6465
+ const parts = path.split("/");
6466
+ if (/\.rs$/.test(path)) {
6467
+ const i = parts.indexOf("tests");
6468
+ if (i < 0 || parts.slice(0, i).includes("src")) return null;
6469
+ const rest = parts.slice(i + 1);
6470
+ if (rest.length === 1 || rest.length === 2 && rest[1] === "main.rs") return null;
6471
+ return "cargo builds only tests/*.rs and tests/*/main.rs as test targets";
6472
+ }
6473
+ if (/\.go$/.test(path)) {
6474
+ const hit = parts.find((p) => p === "testdata" || p.startsWith("_") || p.startsWith("."));
6475
+ if (hit === void 0) return null;
6476
+ return `go test skips ${hit === "testdata" ? "testdata/" : hit.startsWith("_") ? "_*" : ".*"} paths (${JSON.stringify(hit)})`;
6477
+ }
6478
+ if (/\.py$/.test(path)) {
6479
+ const hit = PYTEST_DEFAULT_IGNORE.find((g) => globs3([g])(path));
6480
+ return hit === void 0 ? null : `pytest's default norecursedirs matches it (${JSON.stringify(hit)})`;
6481
+ }
6482
+ if (/\.[cm]?[jt]sx?$/.test(path)) {
6483
+ const hit = JS_UNWALKED.find((g) => globs3([g])(path));
6484
+ return hit === void 0 ? null : `the runner's default exclude matches it (${JSON.stringify(hit)})`;
6485
+ }
6486
+ return null;
6487
+ }
6200
6488
  function keyName2(name) {
6201
6489
  if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
6202
6490
  return null;
@@ -6537,7 +6825,7 @@ function suiteNarrowings(before, after, path, samples) {
6537
6825
  }
6538
6826
  return out3;
6539
6827
  }
6540
- var JEST_DEFAULT_MATCH, JEST_DEFAULT_IGNORE, VITEST_DEFAULT_INCLUDE, PYTEST_DEFAULT_INCLUDE, PYTEST_DEFAULT_IGNORE, VITEST_DEFAULT_EXCLUDE, CANONICAL_SAMPLES, PYTEST_CANONICAL_SAMPLES, isStr, PYTEST_INI_ORDER, pathGlobs, isWorkspaceFile, empty, stripRootDir, JEST_SELECTION_KEYS, globs3, globList2, regexes, underDir;
6828
+ var JEST_DEFAULT_MATCH, JEST_DEFAULT_IGNORE, VITEST_DEFAULT_INCLUDE, PYTEST_DEFAULT_INCLUDE, PYTEST_DEFAULT_IGNORE, VITEST_DEFAULT_EXCLUDE, JS_UNWALKED, CANONICAL_SAMPLES, PYTEST_CANONICAL_SAMPLES, isStr, PYTEST_INI_ORDER, pathGlobs, isWorkspaceFile, empty, stripRootDir, JEST_SELECTION_KEYS, globs3, globList2, regexes, underDir;
6541
6829
  var init_suite_config = __esm({
6542
6830
  "src/detectors/suite-config.ts"() {
6543
6831
  "use strict";
@@ -6555,6 +6843,7 @@ var init_suite_config = __esm({
6555
6843
  "**/.{idea,git,cache,output,temp}/**",
6556
6844
  "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*"
6557
6845
  ];
6846
+ JS_UNWALKED = [...VITEST_DEFAULT_EXCLUDE, "**/.*/**"];
6558
6847
  CANONICAL_SAMPLES = [
6559
6848
  "src/a.test.ts",
6560
6849
  "src/a.spec.ts",
@@ -6786,6 +7075,9 @@ function leavesGlob(dir, dest, policy, ctx) {
6786
7075
  const to = dest.replace(/^\.\//, "").replace(/\/+$/, "") + "/";
6787
7076
  return files.some((f) => f.startsWith(src) && isProtected(f, policy, "tests") && !isProtected(to + f.slice(src.length), policy, "tests"));
6788
7077
  }
7078
+ function leavesWalk(from, to) {
7079
+ return runnerSkips(from) === null ? runnerSkips(to) : null;
7080
+ }
6789
7081
  function pytestPrecedenceFindings(changes, policy, ctx) {
6790
7082
  const touched = /* @__PURE__ */ new Map();
6791
7083
  for (const c of changes) {
@@ -6902,6 +7194,15 @@ var init_test_deletion = __esm({
6902
7194
  remediation: "Restoring the path. Renaming a spec out of the glob silently removes it from the suite."
6903
7195
  })
6904
7196
  );
7197
+ } else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
7198
+ out3.push(
7199
+ makeFinding(RULE8, policy, {
7200
+ file: c.path,
7201
+ message: `A test file was renamed into a path the runner does not walk (${c.oldPath} \u2192 ${c.path}): ${leavesWalk(c.oldPath, c.path)}.`,
7202
+ evidence: `${c.oldPath} \u2192 ${c.path}`,
7203
+ remediation: "Restoring the path. A spec the runner never opens is removed from the suite as surely as a deleted one."
7204
+ })
7205
+ );
6905
7206
  } else if ((c.op === "modify" || c.op === "rename") && isTest && c.before != null && c.after != null) {
6906
7207
  const before = countTests(c.before, c.path);
6907
7208
  const after = countTests(c.after, c.path);
@@ -7014,6 +7315,9 @@ var init_test_deletion = __esm({
7014
7315
  if (dest && testToks.length > 0 && !isSpec(dest)) {
7015
7316
  why = "mv renames a test file out of the test glob";
7016
7317
  evidence = `${testToks[0]} \u2192 ${dest}`;
7318
+ } else if (dest && testToks.length > 0 && testToks[0] !== dest && leavesWalk(testToks[0], dest)) {
7319
+ why = "mv renames a test file into a path the runner does not walk";
7320
+ evidence = `${testToks[0]} \u2192 ${dest}`;
7017
7321
  } else if (dest && testToks.length === 0 && dirToks[0] !== dest && leavesGlob(dirToks[0] ?? "", dest, policy, ctx)) {
7018
7322
  why = "mv renames a test directory so its specs leave the test glob";
7019
7323
  evidence = `${dirToks[0]} \u2192 ${dest}`;
@@ -7166,7 +7470,7 @@ function namesProtectedSnapshot(seg, policy) {
7166
7470
  }
7167
7471
  return null;
7168
7472
  }
7169
- var RULE10, RUNNER2, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
7473
+ var RULE10, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
7170
7474
  var init_snapshot_rewrite = __esm({
7171
7475
  "src/detectors/snapshot-rewrite.ts"() {
7172
7476
  "use strict";
@@ -7174,7 +7478,7 @@ var init_snapshot_rewrite = __esm({
7174
7478
  init_finding();
7175
7479
  init_command();
7176
7480
  RULE10 = "snapshot-rewrite";
7177
- RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
7481
+ RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
7178
7482
  PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
7179
7483
  UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
7180
7484
  UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
@@ -7194,7 +7498,7 @@ var init_snapshot_rewrite = __esm({
7194
7498
  for (const seg of segments(c.raw)) {
7195
7499
  let why = null;
7196
7500
  if (UPDATE_ANY.test(seg)) why = "the snapshot-update flag rewrites every failing snapshot to match current output";
7197
- else if ((RUNNER2.test(seg) || PM_TEST_SCRIPT.test(seg)) && UPDATE_WITH_RUNNER.test(seg)) why = "running the test runner in update mode rewrites failing snapshots to match current output";
7501
+ else if ((RUNNER3.test(seg) || PM_TEST_SCRIPT.test(seg)) && UPDATE_WITH_RUNNER.test(seg)) why = "running the test runner in update mode rewrites failing snapshots to match current output";
7198
7502
  else if (INSTA_ACCEPT.test(seg)) why = "cargo insta accept rewrites every pending snapshot to match current output";
7199
7503
  else if (UPDATE_EXPECT.test(seg)) why = "UPDATE_EXPECT=1 rewrites every expect-test expectation to match current output";
7200
7504
  else if (REGEN_SCRIPT.test(seg)) why = "a regeneration script rewrites the recorded expected output from current output";
@@ -7535,7 +7839,9 @@ function key(f) {
7535
7839
  function isSuppressed(c, policy) {
7536
7840
  if (c.kind !== "file") return false;
7537
7841
  if (isPolicyFile(c.path) || c.oldPath != null && isPolicyFile(c.oldPath)) return false;
7538
- return isIgnored(c.path, policy);
7842
+ if (!isIgnored(c.path, policy)) return false;
7843
+ if (c.oldPath == null) return true;
7844
+ return isIgnored(c.oldPath, policy) && !isProtected(c.oldPath, policy);
7539
7845
  }
7540
7846
  function activeChanges(changes, policy) {
7541
7847
  return changes.filter((c) => !isSuppressed(c, policy));
@@ -7600,7 +7906,7 @@ var init_engine = __esm({
7600
7906
 
7601
7907
  // src/policy-load.ts
7602
7908
  import { readFileSync as readFileSync5, existsSync as existsSync5 } from "node:fs";
7603
- import { isAbsolute as isAbsolute7, join as join8, posix as posix2 } from "node:path";
7909
+ import { isAbsolute as isAbsolute7, join as join8, posix as posix3 } from "node:path";
7604
7910
  function normalizeVersion(v, where = POLICY_FILE) {
7605
7911
  if (v === void 0 || v === null) return 1;
7606
7912
  if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
@@ -7610,7 +7916,7 @@ function normalizeVersion(v, where = POLICY_FILE) {
7610
7916
  }
7611
7917
  function ledgerInsideRepo(ledger) {
7612
7918
  if (!ledger.trim() || isAbsolute7(ledger) || /^[A-Za-z]:[\\/]/.test(ledger)) return false;
7613
- const norm2 = posix2.normalize(ledger.replace(/\\/g, "/"));
7919
+ const norm2 = posix3.normalize(ledger.replace(/\\/g, "/"));
7614
7920
  return norm2 !== ".." && !norm2.startsWith("../") && !norm2.startsWith("/");
7615
7921
  }
7616
7922
  function validate(r, where) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.6",
3
+ "version": "2.23.8",
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",