tamperward 2.19.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -216,6 +216,10 @@ function defaultPolicy(version = 1) {
216
216
  // broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
217
217
  // block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
218
218
  "ts-any-launder": { severity: "warn" },
219
+ // net growth of ordinary `as T` / `<T>x` / `x!` assertions in non-test source (#383).
220
+ // Fires on a large share of legitimate TypeScript mainline diffs (measured in
221
+ // harness/fp-study/CAST-GROWTH-CORPUS.md), so it is a review prompt, not a gate.
222
+ "ts-cast-growth": { severity: "warn" },
219
223
  "lint-suppression": { severity: "block" },
220
224
  "coverage-lowering": { severity: "block" },
221
225
  "ci-tampering": { severity: "block" },
@@ -244,6 +248,45 @@ function defaultPolicy(version = 1) {
244
248
  return p;
245
249
  }
246
250
 
251
+ // src/narrow.ts
252
+ function isRecord(v) {
253
+ return typeof v === "object" && v !== null && !Array.isArray(v);
254
+ }
255
+ function errnoCode(e) {
256
+ const code2 = isRecord(e) ? e.code : void 0;
257
+ return typeof code2 === "string" ? code2 : void 0;
258
+ }
259
+ function errorMessage(e) {
260
+ return e instanceof Error ? e.message : String(e);
261
+ }
262
+ function textOf(v) {
263
+ if (typeof v === "string") return v;
264
+ if (Buffer.isBuffer(v)) return v.toString("utf8");
265
+ return "";
266
+ }
267
+ function execFailure(e) {
268
+ const r = isRecord(e) ? e : {};
269
+ return {
270
+ stdout: textOf(r.stdout),
271
+ stderr: textOf(r.stderr),
272
+ status: typeof r.status === "number" ? r.status : null,
273
+ code: errnoCode(e),
274
+ message: errorMessage(e)
275
+ };
276
+ }
277
+ function finiteNumber(v) {
278
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
279
+ }
280
+ function stringOrUndefined(v) {
281
+ return typeof v === "string" ? v : void 0;
282
+ }
283
+ function nullableString(v) {
284
+ return v === null || typeof v === "string" ? v : void 0;
285
+ }
286
+ function nullableNumber(v) {
287
+ return v === null || typeof v === "number" && Number.isFinite(v) ? v : void 0;
288
+ }
289
+
247
290
  // src/disk.ts
248
291
  var READ_CAP = 64 * 1024 * 1024;
249
292
  var NO_META = { mode: 0, size: 0, mtimeMs: 0 };
@@ -251,7 +294,7 @@ function errText(e) {
251
294
  return e instanceof Error ? e.message : String(e);
252
295
  }
253
296
  function errCode(e) {
254
- return String(e?.code ?? "");
297
+ return errnoCode(e) ?? "";
255
298
  }
256
299
  function irregularName(st) {
257
300
  if (st.isFIFO()) return "fifo";
@@ -341,7 +384,7 @@ function inspectResolved(abs2) {
341
384
  }
342
385
  return inspectPath(real);
343
386
  }
344
- function textOf(e) {
387
+ function textOf2(e) {
345
388
  return e.content == null ? null : e.content.toString("utf8");
346
389
  }
347
390
  function describe(e, shown) {
@@ -675,9 +718,9 @@ function git(args, cwd) {
675
718
  env: trustedGitEnv()
676
719
  });
677
720
  } catch (e) {
678
- const err3 = e;
679
- const detail = String(err3.stderr ?? "").trim();
680
- throw new Error(detail ? `git ${args[0]}: ${detail}` : err3.message ?? `git ${args[0]} failed`);
721
+ const failure = execFailure(e);
722
+ const detail = failure.stderr.trim();
723
+ throw new Error(detail ? `git ${args[0]}: ${detail}` : failure.message || `git ${args[0]} failed`);
681
724
  }
682
725
  }
683
726
  function blobAt(rev, path, cwd) {
@@ -688,7 +731,7 @@ function blobAt(rev, path, cwd) {
688
731
  }
689
732
  }
690
733
  function fromDisk(path, cwd) {
691
- return textOf(inspectRel(cwd ?? process.cwd(), path));
734
+ return textOf2(inspectRel(cwd ?? process.cwd(), path));
692
735
  }
693
736
  function enrich(c, beforeReader, afterReader) {
694
737
  if (c.kind !== "file") return c;
@@ -807,7 +850,7 @@ function ignoredAdds(opts = {}, keep, exclude) {
807
850
  }
808
851
  function diffWorktreeWithUntracked(opts = {}, keepIgnored) {
809
852
  const tracked = diffWorktree(opts);
810
- const seen = new Set(tracked.filter((c) => c.kind === "file").map((c) => c.path));
853
+ const seen = new Set(tracked.flatMap((c) => c.kind === "file" ? [c.path] : []));
811
854
  const untracked = untrackedAdds(opts, void 0, seen);
812
855
  for (const c of untracked) if (c.kind === "file") seen.add(c.path);
813
856
  const ignored = keepIgnored ? ignoredAdds(opts, keepIgnored, seen) : [];
@@ -832,7 +875,7 @@ function asStr(v) {
832
875
  return typeof v === "string" ? v : "";
833
876
  }
834
877
  function readDisk(path) {
835
- return textOf(inspectResolved(path));
878
+ return textOf2(inspectResolved(path));
836
879
  }
837
880
  function abs(path, cwd) {
838
881
  return resolve(cwd, path);
@@ -865,11 +908,11 @@ function synthFileChange(displayPath, before, after) {
865
908
  maxBuffer: 256 * 1024 * 1024
866
909
  });
867
910
  } catch (e) {
868
- const err3 = e;
869
- if (err3.status !== 1) {
870
- throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err3.code ?? err3.message ?? String(e)}`);
911
+ const failure = execFailure(e);
912
+ if (failure.status !== 1) {
913
+ throw new Error(`cannot diff the incoming edit to ${displayPath}: ${failure.code ?? failure.message}`);
871
914
  }
872
- raw = err3.stdout ? String(err3.stdout) : "";
915
+ raw = failure.stdout;
873
916
  }
874
917
  } finally {
875
918
  rmSync(dir, { recursive: true, force: true });
@@ -905,8 +948,8 @@ function changesFromClaudeHook(input, cwd) {
905
948
  let after = before;
906
949
  const edits = Array.isArray(ti.edits) ? ti.edits : [];
907
950
  for (const raw of edits) {
908
- const ed = raw;
909
- after = applyEdit(after, ed.old_string ?? "", ed.new_string ?? "");
951
+ const ed = isRecord(raw) ? raw : {};
952
+ after = applyEdit(after, asStr(ed.old_string), asStr(ed.new_string));
910
953
  }
911
954
  return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
912
955
  }
@@ -1110,7 +1153,7 @@ var noVerify = {
1110
1153
  };
1111
1154
 
1112
1155
  // src/detectors/ts-any-cast.ts
1113
- import ts from "typescript";
1156
+ import ts2 from "typescript";
1114
1157
 
1115
1158
  // src/diff/select.ts
1116
1159
  function addedLines(c) {
@@ -1190,38 +1233,168 @@ function insideStringLiteral(line, idx, lang) {
1190
1233
  return quote !== null;
1191
1234
  }
1192
1235
 
1236
+ // src/detectors/ts-cast-growth.ts
1237
+ import ts from "typescript";
1238
+ var RULE2 = "ts-cast-growth";
1239
+ var DECLARATION = /\.d\.(?:ts|mts|cts)$/;
1240
+ var OUT_OF_BUDGET_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|generated|__generated__)\/|\.(?:generated|gen)\.[cm]?[jt]sx?$/;
1241
+ var GENERATED_HEADER = /@generated\b|\bAUTO-?GENERATED\b|\bDO NOT EDIT\b/i;
1242
+ var HEADER_LINES = 20;
1243
+ function scriptKind(path) {
1244
+ const lower = path.toLowerCase();
1245
+ if (lower.endsWith(".tsx")) return ts.ScriptKind.TSX;
1246
+ if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) return ts.ScriptKind.TS;
1247
+ if (lower.endsWith(".jsx")) return ts.ScriptKind.JSX;
1248
+ return ts.ScriptKind.JS;
1249
+ }
1250
+ function outOfBudget(path, before) {
1251
+ if (DECLARATION.test(path) || OUT_OF_BUDGET_PATH.test(path)) return true;
1252
+ return before != null && GENERATED_HEADER.test(before.split("\n", HEADER_LINES).join("\n"));
1253
+ }
1254
+ function unparenthesized(e) {
1255
+ let x = e;
1256
+ while (ts.isParenthesizedExpression(x)) x = x.expression;
1257
+ return x;
1258
+ }
1259
+ function assertedType(node) {
1260
+ let t = node.type;
1261
+ while (ts.isParenthesizedTypeNode(t)) t = t.type;
1262
+ return t;
1263
+ }
1264
+ function isDoubleCast(node) {
1265
+ const inner = unparenthesized(node.expression);
1266
+ return (ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner)) && assertedType(inner).kind === ts.SyntaxKind.UnknownKeyword;
1267
+ }
1268
+ function surfaceOf(path, src) {
1269
+ let sf;
1270
+ try {
1271
+ sf = ts.createSourceFile(path, src, ts.ScriptTarget.Latest, true, scriptKind(path));
1272
+ } catch {
1273
+ return null;
1274
+ }
1275
+ const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
1276
+ if (diagnostics.length > 0) return null;
1277
+ const surface = { type: 0, nonNull: 0, assertions: [] };
1278
+ const lines = src.split("\n");
1279
+ const record = (kind, node) => {
1280
+ const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
1281
+ surface.assertions.push({ kind, line, text: node.getText(sf).replace(/\s+/g, " "), lineText: (lines[line - 1] ?? "").trim() });
1282
+ if (kind === "type") surface.type++;
1283
+ else surface.nonNull++;
1284
+ };
1285
+ const isRow4OrHonest = (node) => {
1286
+ const type = assertedType(node);
1287
+ if (type.kind === ts.SyntaxKind.AnyKeyword || type.kind === ts.SyntaxKind.UnknownKeyword) return true;
1288
+ return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
1289
+ };
1290
+ const visit = (node) => {
1291
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
1292
+ if (!isDoubleCast(node) && !isRow4OrHonest(node)) record("type", node);
1293
+ } else if (ts.isNonNullExpression(node)) {
1294
+ record("non-null", node);
1295
+ }
1296
+ ts.forEachChild(node, visit);
1297
+ };
1298
+ visit(sf);
1299
+ return surface;
1300
+ }
1301
+ function unmatchedAfter(before, after) {
1302
+ const n = before.length;
1303
+ const m = after.length;
1304
+ const same = (i2, j2) => before[i2].lineText === after[j2].lineText && before[i2].text === after[j2].text;
1305
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1306
+ for (let i2 = n - 1; i2 >= 0; i2--) {
1307
+ for (let j2 = m - 1; j2 >= 0; j2--) {
1308
+ lcs[i2][j2] = same(i2, j2) ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
1309
+ }
1310
+ }
1311
+ const out2 = [];
1312
+ let i = 0;
1313
+ let j = 0;
1314
+ while (j < m) {
1315
+ if (i < n && same(i, j)) {
1316
+ i++;
1317
+ j++;
1318
+ } else if (i < n && lcs[i + 1][j] >= lcs[i][j + 1]) {
1319
+ i++;
1320
+ } else {
1321
+ out2.push(after[j]);
1322
+ j++;
1323
+ }
1324
+ }
1325
+ return out2;
1326
+ }
1327
+ var tsCastGrowth = {
1328
+ id: RULE2,
1329
+ surface: ["file"],
1330
+ certainty: "mechanical",
1331
+ run(changes, policy) {
1332
+ const out2 = [];
1333
+ for (const c of changes) {
1334
+ if (c.kind !== "file" || c.op === "delete" || c.after == null) continue;
1335
+ if (!isCodeFile(c.path) || outOfBudget(c.path, c.before)) continue;
1336
+ if (protectedCategory(c.path, policy) === "tests") continue;
1337
+ const after = surfaceOf(c.path, c.after);
1338
+ const before = c.before == null ? { type: 0, nonNull: 0, assertions: [] } : surfaceOf(c.path, c.before);
1339
+ if (!after || !before) continue;
1340
+ const dType = after.type - before.type;
1341
+ const dNonNull = after.nonNull - before.nonNull;
1342
+ if (dType <= 0 && dNonNull <= 0) continue;
1343
+ const grownKinds = /* @__PURE__ */ new Set([...dType > 0 ? ["type"] : [], ...dNonNull > 0 ? ["non-null"] : []]);
1344
+ const addedLineNumbers2 = new Set(addedLines(c).flatMap((l) => l.newLine == null ? [] : [l.newLine]));
1345
+ const onAddedLine = after.assertions.find((a) => grownKinds.has(a.kind) && addedLineNumbers2.has(a.line));
1346
+ const first = onAddedLine ?? unmatchedAfter(before.assertions, after.assertions).find((a) => grownKinds.has(a.kind)) ?? after.assertions.find((a) => grownKinds.has(a.kind));
1347
+ const parts = [];
1348
+ if (dType > 0) parts.push(`+${dType} type assertion${dType === 1 ? "" : "s"} (\`as T\` / \`<T>x\`)`);
1349
+ if (dNonNull > 0) parts.push(`+${dNonNull} non-null assertion${dNonNull === 1 ? "" : "s"} (\`x!\`)`);
1350
+ out2.push(
1351
+ makeFinding(RULE2, policy, {
1352
+ file: c.path,
1353
+ ...first ? { line: first.line } : {},
1354
+ message: `The unsafe assertion surface grew: ${parts.join(", ")} net of any removed in this change (now ${after.type} type / ${after.nonNull} non-null in this file).`,
1355
+ evidence: first?.text ?? parts.join(", "),
1356
+ remediation: "Prove the value instead of asserting it: parse or validate at the boundary, use a type guard or discriminated union, or handle the nullable case. If the assertion is genuinely unavoidable, keep it at the narrowest boundary and say why in a comment.",
1357
+ defaultSeverity: "warn"
1358
+ })
1359
+ );
1360
+ }
1361
+ return out2;
1362
+ }
1363
+ };
1364
+
1193
1365
  // src/detectors/ts-any-cast.ts
1194
1366
  var BLOCK_RULE = "ts-any-cast";
1195
1367
  var WARN_RULE = "ts-any-launder";
1196
- var DOUBLE_CAST = /\bas\s+unknown\s+as\b/g;
1368
+ var DOUBLE_CAST = /\bas\s+\(*\s*unknown\s*\)*\s+as\b/g;
1197
1369
  var SUPPRESS = /@ts-(?:ignore|expect-error|nocheck)\b/g;
1198
1370
  var JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*any\s*\}\s*\*\/\s*\(/g;
1199
1371
  var JS_FILE = /\.(?:js|jsx|mjs|cjs)$/;
1200
1372
  var countMatches = (s, re) => (s.match(re) || []).length;
1201
1373
  function countAny(src) {
1202
- const r = { cast: 0, broad: 0 };
1374
+ const r = { cast: 0, broad: 0, double: 0 };
1203
1375
  try {
1204
- const sf = ts.createSourceFile("f.ts", src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1376
+ const sf = ts2.createSourceFile("f.ts", src, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
1205
1377
  const visit = (node) => {
1206
- if (node.kind === ts.SyntaxKind.AnyKeyword) {
1378
+ if ((ts2.isAsExpression(node) || ts2.isTypeAssertionExpression(node)) && isDoubleCast(node)) r.double++;
1379
+ if (node.kind === ts2.SyntaxKind.AnyKeyword) {
1207
1380
  let target = node;
1208
1381
  let p = target.parent;
1209
- while (p && ts.isParenthesizedTypeNode(p)) {
1382
+ while (p && ts2.isParenthesizedTypeNode(p)) {
1210
1383
  target = p;
1211
1384
  p = p.parent;
1212
1385
  }
1213
- const isCast = p && ts.isAsExpression(p) && p.type === target || p && ts.isTypeAssertionExpression(p) && p.type === target;
1386
+ const isCast = p && ts2.isAsExpression(p) && p.type === target || p && ts2.isTypeAssertionExpression(p) && p.type === target;
1214
1387
  if (isCast) r.cast++;
1215
1388
  else r.broad++;
1216
1389
  }
1217
- ts.forEachChild(node, visit);
1390
+ ts2.forEachChild(node, visit);
1218
1391
  };
1219
1392
  visit(sf);
1220
1393
  } catch {
1221
1394
  }
1222
1395
  return r;
1223
1396
  }
1224
- var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|\bas\s+unknown\s+as\b|@ts-(?:ignore|expect-error|nocheck)\b/;
1397
+ var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|\bas\s+\(*\s*unknown\s*\)*\s+as\b|@ts-(?:ignore|expect-error|nocheck)\b/;
1225
1398
  var BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
1226
1399
  var BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
1227
1400
  var WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
@@ -1240,7 +1413,7 @@ var tsAnyCast = {
1240
1413
  const b = countAny(before);
1241
1414
  const dCast = a.cast - b.cast;
1242
1415
  const dBroad = a.broad - b.broad;
1243
- const dDouble = countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST);
1416
+ const dDouble = Math.max(a.double - b.double, countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST));
1244
1417
  const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
1245
1418
  const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
1246
1419
  const blockReasons = [];
@@ -1304,7 +1477,7 @@ var tsAnyCast = {
1304
1477
  };
1305
1478
 
1306
1479
  // src/detectors/lint-suppression.ts
1307
- var RULE2 = "lint-suppression";
1480
+ var RULE3 = "lint-suppression";
1308
1481
  var PATTERNS = {
1309
1482
  js: [
1310
1483
  { re: /eslint-disable(?:-next-line|-line)?\b/, why: "eslint-disable suppresses lint rules" },
@@ -1352,7 +1525,7 @@ var PATTERNS = {
1352
1525
  ]
1353
1526
  };
1354
1527
  var lintSuppression = {
1355
- id: RULE2,
1528
+ id: RULE3,
1356
1529
  surface: ["file"],
1357
1530
  certainty: "mechanical",
1358
1531
  run(changes, policy) {
@@ -1368,7 +1541,7 @@ var lintSuppression = {
1368
1541
  const m = p.re.exec(l.content);
1369
1542
  if (!m || insideStringLiteral(l.content, m.index, lang)) continue;
1370
1543
  out2.push(
1371
- makeFinding(RULE2, policy, {
1544
+ makeFinding(RULE3, policy, {
1372
1545
  file: c.path,
1373
1546
  line: l.newLine ?? void 0,
1374
1547
  message: `Lint suppression added: ${p.why}.`,
@@ -1385,8 +1558,8 @@ var lintSuppression = {
1385
1558
  };
1386
1559
 
1387
1560
  // src/detectors/test-skip.ts
1388
- import ts2 from "typescript";
1389
- var RULE3 = "test-skip";
1561
+ import ts3 from "typescript";
1562
+ var RULE4 = "test-skip";
1390
1563
  var acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
1391
1564
  var JS_RUNNER = "\\b(?:it|test|describe|suite)";
1392
1565
  var JS_MOD = "concurrent|sequential|shuffle";
@@ -1489,11 +1662,11 @@ var JS_TEST_MODULES = /* @__PURE__ */ new Set([
1489
1662
  "bun:test",
1490
1663
  "@playwright/test"
1491
1664
  ]);
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;
1665
+ function scriptKind2(path) {
1666
+ if (/\.tsx$/i.test(path)) return ts3.ScriptKind.TSX;
1667
+ if (/\.jsx$/i.test(path)) return ts3.ScriptKind.JSX;
1668
+ if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts3.ScriptKind.JS;
1669
+ return ts3.ScriptKind.TS;
1497
1670
  }
1498
1671
  function addedLineNumbers(c) {
1499
1672
  const out2 = /* @__PURE__ */ new Set();
@@ -1505,32 +1678,32 @@ function addedLineNumbers(c) {
1505
1678
  return out2;
1506
1679
  }
1507
1680
  function moduleName(expr) {
1508
- return ts2.isStringLiteralLike(expr) ? expr.text : null;
1681
+ return ts3.isStringLiteralLike(expr) ? expr.text : null;
1509
1682
  }
1510
1683
  function requiredModule(expr) {
1511
- if (!ts2.isCallExpression(expr) || !ts2.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1684
+ if (!ts3.isCallExpression(expr) || !ts3.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1512
1685
  return moduleName(expr.arguments[0]);
1513
1686
  }
1514
1687
  function astContext(path, source) {
1515
1688
  const rootName = path.startsWith("/") ? path : "/tamperward/" + path;
1516
- const sf = ts2.createSourceFile(
1689
+ const sf = ts3.createSourceFile(
1517
1690
  rootName,
1518
1691
  source,
1519
- ts2.ScriptTarget.Latest,
1692
+ ts3.ScriptTarget.Latest,
1520
1693
  true,
1521
- scriptKind(path)
1694
+ scriptKind2(path)
1522
1695
  );
1523
- const diagnostics = sf.parseDiagnostics ?? [];
1696
+ const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
1524
1697
  if (diagnostics.length > 0) return null;
1525
1698
  const options = {
1526
1699
  allowJs: true,
1527
1700
  checkJs: false,
1528
1701
  noLib: true,
1529
1702
  noResolve: true,
1530
- target: ts2.ScriptTarget.Latest,
1531
- module: ts2.ModuleKind.ESNext
1703
+ target: ts3.ScriptTarget.Latest,
1704
+ module: ts3.ModuleKind.ESNext
1532
1705
  };
1533
- const host = ts2.createCompilerHost(options, true);
1706
+ const host = ts3.createCompilerHost(options, true);
1534
1707
  host.getSourceFile = (fileName) => fileName === rootName ? sf : void 0;
1535
1708
  host.fileExists = (fileName) => fileName === rootName;
1536
1709
  host.readFile = (fileName) => fileName === rootName ? source : void 0;
@@ -1541,7 +1714,7 @@ function astContext(path, source) {
1541
1714
  host.getCanonicalFileName = (fileName) => fileName;
1542
1715
  host.useCaseSensitiveFileNames = () => true;
1543
1716
  host.getNewLine = () => "\n";
1544
- const program = ts2.createProgram([rootName], options, host);
1717
+ const program = ts3.createProgram([rootName], options, host);
1545
1718
  return { sf, checker: program.getTypeChecker() };
1546
1719
  }
1547
1720
  function symbolAt(ctx, node) {
@@ -1555,13 +1728,13 @@ function importAliases(ctx) {
1555
1728
  if (sym) out2.set(sym, { causes });
1556
1729
  };
1557
1730
  for (const stmt of ctx.sf.statements) {
1558
- if (ts2.isImportDeclaration(stmt) && ts2.isStringLiteralLike(stmt.moduleSpecifier)) {
1731
+ if (ts3.isImportDeclaration(stmt) && ts3.isStringLiteralLike(stmt.moduleSpecifier)) {
1559
1732
  if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
1560
1733
  const clause = stmt.importClause;
1561
1734
  if (!clause) continue;
1562
1735
  if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
1563
1736
  const bindings = clause.namedBindings;
1564
- if (bindings && ts2.isNamedImports(bindings)) {
1737
+ if (bindings && ts3.isNamedImports(bindings)) {
1565
1738
  for (const el of bindings.elements) {
1566
1739
  const imported = el.propertyName?.text ?? el.name.text;
1567
1740
  if (JS_RUNNERS.has(imported)) add(el.name);
@@ -1569,17 +1742,17 @@ function importAliases(ctx) {
1569
1742
  }
1570
1743
  continue;
1571
1744
  }
1572
- if (!ts2.isVariableStatement(stmt)) continue;
1745
+ if (!ts3.isVariableStatement(stmt)) continue;
1573
1746
  for (const decl of stmt.declarationList.declarations) {
1574
- if (ts2.isObjectBindingPattern(decl.name) && decl.initializer) {
1747
+ if (ts3.isObjectBindingPattern(decl.name) && decl.initializer) {
1575
1748
  const mod = requiredModule(decl.initializer);
1576
1749
  if (!mod || !JS_TEST_MODULES.has(mod)) continue;
1577
1750
  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;
1751
+ if (!ts3.isIdentifier(el.name)) continue;
1752
+ const imported = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1580
1753
  if (JS_RUNNERS.has(imported)) add(el.name);
1581
1754
  }
1582
- } else if (ts2.isIdentifier(decl.name) && decl.initializer && ts2.isIdentifier(decl.initializer)) {
1755
+ } else if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isIdentifier(decl.initializer)) {
1583
1756
  simpleAliases.push({ local: decl.name, source: decl.initializer });
1584
1757
  }
1585
1758
  }
@@ -1605,11 +1778,11 @@ function importAliases(ctx) {
1605
1778
  function topLevelStaticStrings(ctx) {
1606
1779
  const pending = /* @__PURE__ */ new Map();
1607
1780
  for (const stmt of ctx.sf.statements) {
1608
- if (!ts2.isVariableStatement(stmt)) continue;
1609
- const isConst = (stmt.declarationList.flags & ts2.NodeFlags.Const) !== 0;
1781
+ if (!ts3.isVariableStatement(stmt)) continue;
1782
+ const isConst = (stmt.declarationList.flags & ts3.NodeFlags.Const) !== 0;
1610
1783
  if (!isConst) continue;
1611
1784
  for (const decl of stmt.declarationList.declarations) {
1612
- if (!ts2.isIdentifier(decl.name) || !decl.initializer) continue;
1785
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
1613
1786
  const sym = symbolAt(ctx, decl.name);
1614
1787
  if (sym) pending.set(sym, { decl: decl.name, expr: decl.initializer });
1615
1788
  }
@@ -1617,15 +1790,15 @@ function topLevelStaticStrings(ctx) {
1617
1790
  const resolved = /* @__PURE__ */ new Map();
1618
1791
  const resolving = /* @__PURE__ */ new Set();
1619
1792
  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) {
1793
+ if (ts3.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
1794
+ if (ts3.isParenthesizedExpression(expr)) return valueOf(expr.expression);
1795
+ if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
1623
1796
  const left = valueOf(expr.left);
1624
1797
  const right = valueOf(expr.right);
1625
1798
  if (!left || !right) return null;
1626
1799
  return { value: left.value + right.value, causes: [...left.causes, ...right.causes] };
1627
1800
  }
1628
- if (ts2.isIdentifier(expr)) {
1801
+ if (ts3.isIdentifier(expr)) {
1629
1802
  const sym = symbolAt(ctx, expr);
1630
1803
  if (!sym) return null;
1631
1804
  const cached = resolved.get(sym);
@@ -1652,15 +1825,15 @@ function topLevelStaticStrings(ctx) {
1652
1825
  return resolved;
1653
1826
  }
1654
1827
  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) {
1828
+ if (ts3.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
1829
+ if (ts3.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
1830
+ if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
1658
1831
  const left = staticPropertyName(expr.left, ctx, strings);
1659
1832
  const right = staticPropertyName(expr.right, ctx, strings);
1660
1833
  if (!left || !right) return null;
1661
1834
  return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
1662
1835
  }
1663
- if (ts2.isIdentifier(expr)) {
1836
+ if (ts3.isIdentifier(expr)) {
1664
1837
  const sym = symbolAt(ctx, expr);
1665
1838
  if (!sym) return null;
1666
1839
  const value = strings.get(sym);
@@ -1673,8 +1846,8 @@ function runnerChain(expr, ctx, runners, strings) {
1673
1846
  const causes = [];
1674
1847
  let cur = expr;
1675
1848
  let terminalNode = expr;
1676
- while (ts2.isPropertyAccessExpression(cur) || ts2.isElementAccessExpression(cur)) {
1677
- if (ts2.isPropertyAccessExpression(cur)) {
1849
+ while (ts3.isPropertyAccessExpression(cur) || ts3.isElementAccessExpression(cur)) {
1850
+ if (ts3.isPropertyAccessExpression(cur)) {
1678
1851
  props.unshift(cur.name.text);
1679
1852
  if (props.length === 1) terminalNode = cur.name;
1680
1853
  cur = cur.expression;
@@ -1688,7 +1861,7 @@ function runnerChain(expr, ctx, runners, strings) {
1688
1861
  if (props.length === 1) terminalNode = cur.argumentExpression;
1689
1862
  cur = cur.expression;
1690
1863
  }
1691
- if (!ts2.isIdentifier(cur)) return null;
1864
+ if (!ts3.isIdentifier(cur)) return null;
1692
1865
  const sym = symbolAt(ctx, cur);
1693
1866
  if (sym) {
1694
1867
  const binding = runners.get(sym);
@@ -1700,17 +1873,17 @@ function runnerChain(expr, ctx, runners, strings) {
1700
1873
  return { root: cur.text, props, terminalNode, causes };
1701
1874
  }
1702
1875
  function optionDisables(value) {
1703
- return !(value.kind === ts2.SyntaxKind.FalseKeyword || ts2.isNumericLiteral(value) && Number(value.text) === 0);
1876
+ return !(value.kind === ts3.SyntaxKind.FalseKeyword || ts3.isNumericLiteral(value) && Number(value.text) === 0);
1704
1877
  }
1705
1878
  function shorthandOptionDisables(prop, ctx) {
1706
1879
  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;
1880
+ const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts3.isVariableDeclaration(d));
1881
+ if (!decl || !ts3.isVariableDeclaration(decl) || !decl.initializer) return true;
1709
1882
  return optionDisables(decl.initializer);
1710
1883
  }
1711
1884
  function structuralNodeKey(node, sf) {
1712
1885
  const children = node.getChildren(sf);
1713
- const text = node.text;
1886
+ const text = "text" in node ? node.text : void 0;
1714
1887
  if (children.length === 0) {
1715
1888
  return typeof text === "string" ? `${node.kind}:${JSON.stringify(text)}` : String(node.kind);
1716
1889
  }
@@ -1722,7 +1895,7 @@ function semanticSkipHits(ctx) {
1722
1895
  const hits = [];
1723
1896
  const callOrdinals = /* @__PURE__ */ new Map();
1724
1897
  const visit = (node) => {
1725
- if (ts2.isCallExpression(node)) {
1898
+ if (ts3.isCallExpression(node)) {
1726
1899
  const callText = node.getText(ctx.sf).trim();
1727
1900
  const callIdentity = structuralNodeKey(node, ctx.sf);
1728
1901
  const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
@@ -1747,19 +1920,19 @@ function semanticSkipHits(ctx) {
1747
1920
  push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
1748
1921
  } else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
1749
1922
  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) {
1923
+ } else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts3.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
1751
1924
  push(terminalNode, [], "an empty .each table (no case ever runs)");
1752
1925
  }
1753
1926
  if (props.length === 0) {
1754
1927
  for (const arg of node.arguments) {
1755
- if (!ts2.isObjectLiteralExpression(arg)) continue;
1928
+ if (!ts3.isObjectLiteralExpression(arg)) continue;
1756
1929
  for (const prop of arg.properties) {
1757
- if (ts2.isPropertyAssignment(prop)) {
1930
+ if (ts3.isPropertyAssignment(prop)) {
1758
1931
  let name = null;
1759
1932
  let propertyCauses = [];
1760
- if (ts2.isIdentifier(prop.name) || ts2.isStringLiteralLike(prop.name)) {
1933
+ if (ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name)) {
1761
1934
  name = prop.name.text;
1762
- } else if (ts2.isComputedPropertyName(prop.name)) {
1935
+ } else if (ts3.isComputedPropertyName(prop.name)) {
1763
1936
  const resolved = staticPropertyName(prop.name.expression, ctx, strings);
1764
1937
  name = resolved?.name ?? null;
1765
1938
  propertyCauses = resolved?.causes ?? [];
@@ -1768,7 +1941,7 @@ function semanticSkipHits(ctx) {
1768
1941
  if (!optionDisables(prop.initializer)) continue;
1769
1942
  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
1943
  push(prop.name, propertyCauses, why);
1771
- } else if (ts2.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1944
+ } else if (ts3.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1772
1945
  if (!shorthandOptionDisables(prop, ctx)) continue;
1773
1946
  push(prop.name, [], `a { ${prop.name.text} } option that conditionally narrows the test run`);
1774
1947
  }
@@ -1777,7 +1950,7 @@ function semanticSkipHits(ctx) {
1777
1950
  }
1778
1951
  }
1779
1952
  }
1780
- ts2.forEachChild(node, visit);
1953
+ ts3.forEachChild(node, visit);
1781
1954
  };
1782
1955
  visit(ctx.sf);
1783
1956
  return hits;
@@ -1816,7 +1989,7 @@ function astSkipHits(c) {
1816
1989
  return { hits, authoritative: true };
1817
1990
  }
1818
1991
  var testSkip = {
1819
- id: RULE3,
1992
+ id: RULE4,
1820
1993
  surface: ["file"],
1821
1994
  certainty: "mechanical",
1822
1995
  run(changes, policy) {
@@ -1834,7 +2007,7 @@ var testSkip = {
1834
2007
  for (const hit of analysis.hits) {
1835
2008
  astHitLines.add(hit.line);
1836
2009
  out2.push(
1837
- makeFinding(RULE3, policy, {
2010
+ makeFinding(RULE4, policy, {
1838
2011
  file: c.path,
1839
2012
  line: hit.line,
1840
2013
  message: `Test skipped or narrowed: ${hit.why}.`,
@@ -1852,7 +2025,7 @@ var testSkip = {
1852
2025
  if (comment && !p.comment) continue;
1853
2026
  if (matchesOutsideString(p, l.content, lang)) {
1854
2027
  out2.push(
1855
- makeFinding(RULE3, policy, {
2028
+ makeFinding(RULE4, policy, {
1856
2029
  file: c.path,
1857
2030
  line: l.newLine ?? void 0,
1858
2031
  message: `Test skipped or narrowed: ${p.why}.`,
@@ -1871,7 +2044,7 @@ var testSkip = {
1871
2044
 
1872
2045
  // src/detectors/coverage-lowering.ts
1873
2046
  import picomatch2 from "picomatch";
1874
- import ts3 from "typescript";
2047
+ import ts4 from "typescript";
1875
2048
 
1876
2049
  // src/detectors/repo.ts
1877
2050
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -1994,34 +2167,34 @@ function containsProtected(token, policy, category, ctx, except) {
1994
2167
  }
1995
2168
 
1996
2169
  // src/detectors/coverage-lowering.ts
1997
- var RULE4 = "coverage-lowering";
2170
+ var RULE5 = "coverage-lowering";
1998
2171
  var METRICS = ["branches", "functions", "lines", "statements"];
1999
- var isMetric = (k) => METRICS.includes(k);
2172
+ var isMetric = (k) => METRICS.some((m) => m === k);
2000
2173
  var norm = (p) => p.replace(/^\.\//, "");
2001
2174
  function keyName(name) {
2002
- if (ts3.isIdentifier(name) || ts3.isStringLiteral(name) || ts3.isNumericLiteral(name)) return name.text;
2175
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
2003
2176
  return null;
2004
2177
  }
2005
2178
  function numericOf(e) {
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)) {
2179
+ if (ts4.isNumericLiteral(e)) return Number(e.text);
2180
+ if (ts4.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
2181
+ if (ts4.isPrefixUnaryExpression(e) && e.operator === ts4.SyntaxKind.MinusToken && ts4.isNumericLiteral(e.operand)) {
2009
2182
  return -Number(e.operand.text);
2010
2183
  }
2011
2184
  return void 0;
2012
2185
  }
2013
2186
  function metricsOf(expr, resolve10) {
2014
2187
  const obj = resolve10(expr);
2015
- if (!ts3.isObjectLiteralExpression(obj)) return void 0;
2188
+ if (!ts4.isObjectLiteralExpression(obj)) return void 0;
2016
2189
  const out2 = { values: {}, opaque: false };
2017
2190
  let any = false;
2018
2191
  for (const p of obj.properties) {
2019
- if (ts3.isSpreadAssignment(p)) {
2192
+ if (ts4.isSpreadAssignment(p)) {
2020
2193
  out2.opaque = true;
2021
2194
  any = true;
2022
2195
  continue;
2023
2196
  }
2024
- if (!ts3.isPropertyAssignment(p)) continue;
2197
+ if (!ts4.isPropertyAssignment(p)) continue;
2025
2198
  const k = keyName(p.name);
2026
2199
  if (k === null || !isMetric(k)) continue;
2027
2200
  any = true;
@@ -2033,34 +2206,34 @@ function metricsOf(expr, resolve10) {
2033
2206
  }
2034
2207
  function underKey(node, key2) {
2035
2208
  for (let p = node.parent; p; p = p.parent) {
2036
- if (ts3.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2209
+ if (ts4.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2037
2210
  }
2038
2211
  return false;
2039
2212
  }
2040
2213
  function ownerKey(node) {
2041
2214
  const obj = node.parent;
2042
- if (!obj || !ts3.isObjectLiteralExpression(obj)) return null;
2215
+ if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
2043
2216
  let q = obj.parent;
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;
2217
+ while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
2218
+ return q && ts4.isPropertyAssignment(q) ? keyName(q.name) : null;
2046
2219
  }
2047
2220
  function asExpression(src) {
2048
2221
  const t = src.trim();
2049
2222
  return t.startsWith("{") ? `(${t})` : src;
2050
2223
  }
2051
2224
  function sourceOf(src) {
2052
- return ts3.createSourceFile("cfg.ts", asExpression(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
2225
+ return ts4.createSourceFile("cfg.ts", asExpression(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
2053
2226
  }
2054
2227
  function resolverFor(sf) {
2055
2228
  const consts = /* @__PURE__ */ new Map();
2056
2229
  for (const st of sf.statements) {
2057
- if (!ts3.isVariableStatement(st)) continue;
2230
+ if (!ts4.isVariableStatement(st)) continue;
2058
2231
  for (const d of st.declarationList.declarations) {
2059
- if (ts3.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2232
+ if (ts4.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2060
2233
  }
2061
2234
  }
2062
2235
  return (e) => {
2063
- if (ts3.isIdentifier(e)) {
2236
+ if (ts4.isIdentifier(e)) {
2064
2237
  const bound = consts.get(e.text);
2065
2238
  return bound ?? e;
2066
2239
  }
@@ -2073,25 +2246,25 @@ function parseThresholds(src) {
2073
2246
  const sf = sourceOf(src);
2074
2247
  const resolve10 = resolverFor(sf);
2075
2248
  const visit = (node) => {
2076
- if (ts3.isPropertyAssignment(node)) {
2249
+ if (ts4.isPropertyAssignment(node)) {
2077
2250
  const key2 = keyName(node.name);
2078
2251
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
2079
2252
  res.present = true;
2080
2253
  const init = resolve10(node.initializer);
2081
- if (!ts3.isObjectLiteralExpression(init)) {
2254
+ if (!ts4.isObjectLiteralExpression(init)) {
2082
2255
  res.opaque = true;
2083
2256
  } else {
2084
2257
  const flat = metricsOf(init, resolve10);
2085
2258
  if (flat) res.global = merge(res.global, flat);
2086
2259
  for (const p of init.properties) {
2087
- if (ts3.isSpreadAssignment(p)) {
2260
+ if (ts4.isSpreadAssignment(p)) {
2088
2261
  res.opaque = true;
2089
2262
  continue;
2090
2263
  }
2091
- if (!ts3.isPropertyAssignment(p)) continue;
2264
+ if (!ts4.isPropertyAssignment(p)) continue;
2092
2265
  const k = keyName(p.name);
2093
2266
  if (k === null || isMetric(k)) continue;
2094
- if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts3.SyntaxKind.TrueKeyword) {
2267
+ if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts4.SyntaxKind.TrueKeyword) {
2095
2268
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
2096
2269
  continue;
2097
2270
  }
@@ -2103,7 +2276,7 @@ function parseThresholds(src) {
2103
2276
  }
2104
2277
  }
2105
2278
  }
2106
- ts3.forEachChild(node, visit);
2279
+ ts4.forEachChild(node, visit);
2107
2280
  };
2108
2281
  visit(sf);
2109
2282
  } catch {
@@ -2114,12 +2287,12 @@ function merge(a, b) {
2114
2287
  return { values: { ...a?.values ?? {}, ...b.values }, opaque: (a?.opaque ?? false) || b.opaque };
2115
2288
  }
2116
2289
  function literals(e) {
2117
- if (ts3.isStringLiteral(e)) return { items: [e.text], opaque: false };
2118
- if (!ts3.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2290
+ if (ts4.isStringLiteral(e)) return { items: [e.text], opaque: false };
2291
+ if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2119
2292
  const items = [];
2120
2293
  let opaque = false;
2121
2294
  for (const el of e.elements) {
2122
- if (ts3.isStringLiteral(el) || ts3.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2295
+ if (ts4.isStringLiteral(el) || ts4.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2123
2296
  else opaque = true;
2124
2297
  }
2125
2298
  return { items, opaque };
@@ -2130,7 +2303,7 @@ function parseLists(src) {
2130
2303
  const sf = sourceOf(src);
2131
2304
  const resolve10 = resolverFor(sf);
2132
2305
  const visit = (node) => {
2133
- if (ts3.isPropertyAssignment(node)) {
2306
+ if (ts4.isPropertyAssignment(node)) {
2134
2307
  const k = keyName(node.name);
2135
2308
  const vitestCoverage = ownerKey(node) === "coverage";
2136
2309
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
@@ -2144,7 +2317,7 @@ function parseLists(src) {
2144
2317
  if (opaque) res.opaqueIgnore = true;
2145
2318
  }
2146
2319
  }
2147
- ts3.forEachChild(node, visit);
2320
+ ts4.forEachChild(node, visit);
2148
2321
  };
2149
2322
  visit(sf);
2150
2323
  } catch {
@@ -2155,15 +2328,15 @@ function parseSwitches(src) {
2155
2328
  const res = {};
2156
2329
  try {
2157
2330
  const sf = sourceOf(src);
2158
- const bool = (e) => e.kind === ts3.SyntaxKind.TrueKeyword ? true : e.kind === ts3.SyntaxKind.FalseKeyword ? false : void 0;
2331
+ const bool = (e) => e.kind === ts4.SyntaxKind.TrueKeyword ? true : e.kind === ts4.SyntaxKind.FalseKeyword ? false : void 0;
2159
2332
  const visit = (node) => {
2160
- if (ts3.isPropertyAssignment(node)) {
2333
+ if (ts4.isPropertyAssignment(node)) {
2161
2334
  const k = keyName(node.name);
2162
2335
  const owner = ownerKey(node);
2163
2336
  if (k === "enabled" && owner === "coverage") res.enabled = bool(node.initializer);
2164
2337
  else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool(node.initializer);
2165
2338
  }
2166
- ts3.forEachChild(node, visit);
2339
+ ts4.forEachChild(node, visit);
2167
2340
  };
2168
2341
  visit(sf);
2169
2342
  } catch {
@@ -2176,11 +2349,11 @@ function sectionText(src, key2) {
2176
2349
  let out2 = "";
2177
2350
  const visit = (node) => {
2178
2351
  if (out2) return;
2179
- if (ts3.isPropertyAssignment(node) && keyName(node.name) === key2 && ts3.isObjectLiteralExpression(node.initializer)) {
2352
+ if (ts4.isPropertyAssignment(node) && keyName(node.name) === key2 && ts4.isObjectLiteralExpression(node.initializer)) {
2180
2353
  out2 = node.initializer.getText(sf);
2181
2354
  return;
2182
2355
  }
2183
- ts3.forEachChild(node, visit);
2356
+ ts4.forEachChild(node, visit);
2184
2357
  };
2185
2358
  visit(sf);
2186
2359
  return out2;
@@ -2517,7 +2690,7 @@ function gateMovedElsewhere(c, changes, policy) {
2517
2690
  });
2518
2691
  }
2519
2692
  var coverageLowering = {
2520
- id: RULE4,
2693
+ id: RULE5,
2521
2694
  surface: ["file"],
2522
2695
  certainty: "mechanical",
2523
2696
  run(changes, policy, _view, ctx) {
@@ -2539,7 +2712,7 @@ var coverageLowering = {
2539
2712
  reasons.push(...simpleNarrowings(c.path, c.before ?? "", c.after));
2540
2713
  for (const reason of reasons) {
2541
2714
  out2.push(
2542
- makeFinding(RULE4, policy, {
2715
+ makeFinding(RULE5, policy, {
2543
2716
  file: c.path,
2544
2717
  message: `Coverage gate weakened: ${reason}.`,
2545
2718
  evidence: reason,
@@ -2552,7 +2725,7 @@ var coverageLowering = {
2552
2725
  const gated = isJsConfig(c.path) ? parseThresholds(before).present : simpleWeakenings(c.path, before, "").length > 0 || SIMPLE_FILES.some((s) => s.file.test(c.path) && s.keys.some((k) => numbersOf(before, k.key).length));
2553
2726
  if (gated && !gateMovedElsewhere(c, changes, policy)) {
2554
2727
  out2.push(
2555
- makeFinding(RULE4, policy, {
2728
+ makeFinding(RULE5, policy, {
2556
2729
  file: c.path,
2557
2730
  message: "Coverage gate weakened: the coverage config was deleted.",
2558
2731
  evidence: c.path,
@@ -2566,7 +2739,7 @@ var coverageLowering = {
2566
2739
  const removedCoverage = removed.find((l) => /--coverage\b/.test(l.content));
2567
2740
  if (removedCoverage && !added.some((l) => /--coverage\b/.test(l.content))) {
2568
2741
  out2.push(
2569
- makeFinding(RULE4, policy, {
2742
+ makeFinding(RULE5, policy, {
2570
2743
  file: c.path,
2571
2744
  message: "The --coverage flag was removed from a test script.",
2572
2745
  evidence: removedCoverage.content.trim(),
@@ -2580,7 +2753,7 @@ var coverageLowering = {
2580
2753
  if (removed.some((r) => s.re.test(r.content))) continue;
2581
2754
  if (/passWithNoTests/.test(s.re.source) && dirHasSpec(c.path, policy, ctx) === false) continue;
2582
2755
  out2.push(
2583
- makeFinding(RULE4, policy, {
2756
+ makeFinding(RULE5, policy, {
2584
2757
  file: c.path,
2585
2758
  line: l.newLine ?? void 0,
2586
2759
  message: s.message,
@@ -2603,7 +2776,7 @@ var coverageLowering = {
2603
2776
  const ov = oldNums.get(m[1]);
2604
2777
  if (ov !== void 0 && Number(m[2]) < ov) {
2605
2778
  out2.push(
2606
- makeFinding(RULE4, policy, {
2779
+ makeFinding(RULE5, policy, {
2607
2780
  file: c.path,
2608
2781
  line: l.newLine ?? void 0,
2609
2782
  message: `Coverage threshold for ${m[1]} lowered ${ov} \u2192 ${m[2]}.`,
@@ -2626,7 +2799,7 @@ var coverageLowering = {
2626
2799
  };
2627
2800
 
2628
2801
  // src/detectors/ci-tampering.ts
2629
- var RULE5 = "ci-tampering";
2802
+ var RULE6 = "ci-tampering";
2630
2803
  var USES = /^\s*-\s*uses:/;
2631
2804
  var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
2632
2805
  var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
@@ -2890,7 +3063,7 @@ function isTruthy(raw) {
2890
3063
  return f !== void 0 && truthy(f);
2891
3064
  }
2892
3065
  var SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
2893
- var indentOf = (l) => l.match(/^\s*/)[0].length;
3066
+ var indentOf = (l) => l.length - l.trimStart().length;
2894
3067
  var STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
2895
3068
  function stepOf(lines, i) {
2896
3069
  const ind = indentOf(lines[i]);
@@ -3072,7 +3245,7 @@ function triggerNarrowings(before, after, opts) {
3072
3245
  }
3073
3246
  var REUSED_WORKFLOW = /^\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
3074
3247
  var ciTampering = {
3075
- id: RULE5,
3248
+ id: RULE6,
3076
3249
  surface: ["file"],
3077
3250
  certainty: "mechanical",
3078
3251
  run(changes, policy, _view, ctx) {
@@ -3084,7 +3257,7 @@ var ciTampering = {
3084
3257
  if (c.kind !== "file") continue;
3085
3258
  if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
3086
3259
  out2.push(
3087
- makeFinding(RULE5, policy, {
3260
+ makeFinding(RULE6, policy, {
3088
3261
  file: c.path,
3089
3262
  message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
3090
3263
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -3108,7 +3281,7 @@ var ciTampering = {
3108
3281
  }
3109
3282
  if (coe && isTruthy(coe[1])) {
3110
3283
  out2.push(
3111
- makeFinding(RULE5, policy, {
3284
+ makeFinding(RULE6, policy, {
3112
3285
  file: c.path,
3113
3286
  line: l.newLine ?? void 0,
3114
3287
  message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
@@ -3118,7 +3291,7 @@ var ciTampering = {
3118
3291
  );
3119
3292
  } else if (cond && isAlwaysFalse(cond[1])) {
3120
3293
  out2.push(
3121
- makeFinding(RULE5, policy, {
3294
+ makeFinding(RULE6, policy, {
3122
3295
  file: c.path,
3123
3296
  line: l.newLine ?? void 0,
3124
3297
  message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
@@ -3138,7 +3311,7 @@ var ciTampering = {
3138
3311
  const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
3139
3312
  if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
3140
3313
  out2.push(
3141
- makeFinding(RULE5, policy, {
3314
+ makeFinding(RULE6, policy, {
3142
3315
  file: c.path,
3143
3316
  line: idx + 1,
3144
3317
  message: `A check's run block was neutralised: ${n.what}.`,
@@ -3154,7 +3327,7 @@ var ciTampering = {
3154
3327
  const step = stepOf(afterLines, l.newLine - 1);
3155
3328
  if (step && !stepHasCheck(afterLines, step)) continue;
3156
3329
  out2.push(
3157
- makeFinding(RULE5, policy, {
3330
+ makeFinding(RULE6, policy, {
3158
3331
  file: c.path,
3159
3332
  line: l.newLine,
3160
3333
  message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
@@ -3163,11 +3336,11 @@ var ciTampering = {
3163
3336
  })
3164
3337
  );
3165
3338
  }
3166
- if (c.before != null) {
3339
+ if (c.before != null && c.after != null) {
3167
3340
  triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
3168
3341
  for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
3169
3342
  out2.push(
3170
- makeFinding(RULE5, policy, {
3343
+ makeFinding(RULE6, policy, {
3171
3344
  file: c.path,
3172
3345
  message: `The workflow's triggers were narrowed: ${reason}.`,
3173
3346
  evidence: reason,
@@ -3196,7 +3369,7 @@ var ciTampering = {
3196
3369
  if (s.state === "kept") continue;
3197
3370
  if (s.state === "neutralised") {
3198
3371
  out2.push(
3199
- makeFinding(RULE5, policy, {
3372
+ makeFinding(RULE6, policy, {
3200
3373
  file: c.path,
3201
3374
  message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
3202
3375
  evidence: `${l.content.trim()} \u2192 ${s.by}`,
@@ -3206,7 +3379,7 @@ var ciTampering = {
3206
3379
  continue;
3207
3380
  }
3208
3381
  out2.push(
3209
- makeFinding(RULE5, policy, {
3382
+ makeFinding(RULE6, policy, {
3210
3383
  file: c.path,
3211
3384
  message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
3212
3385
  evidence: l.content.trim(),
@@ -3235,7 +3408,7 @@ function shippedVersion() {
3235
3408
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
3236
3409
  try {
3237
3410
  const pkg = JSON.parse(readFileSync3(join5(here, rel), "utf8"));
3238
- if (pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
3411
+ if (isRecord(pkg) && pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
3239
3412
  return pkg.version;
3240
3413
  }
3241
3414
  } catch {
@@ -3319,11 +3492,25 @@ function resolvesToClaudeSettings(path, env = process.env) {
3319
3492
 
3320
3493
  // src/detectors/policy-diff.ts
3321
3494
  import { parse } from "yaml";
3495
+ function verifyBlock(v) {
3496
+ if (!isRecord(v)) return void 0;
3497
+ return {
3498
+ ...typeof v.command === "string" ? { command: v.command } : {},
3499
+ ...typeof v.budget === "number" ? { budget: v.budget } : {},
3500
+ ...Array.isArray(v.inputs) ? { inputs: v.inputs.filter((g) => typeof g === "string") } : {},
3501
+ ...typeof v.backend === "string" ? { backend: v.backend } : {},
3502
+ ...typeof v.image === "string" ? { image: v.image } : {}
3503
+ };
3504
+ }
3505
+ function ruleOverrides(v) {
3506
+ if (!isRecord(v)) return {};
3507
+ return Object.fromEntries(Object.entries(v).map(([name, cfg]) => [name, isRecord(cfg) ? cfg : {}]));
3508
+ }
3322
3509
  var globs2 = (list2) => Array.isArray(list2) ? list2.filter((g) => typeof g === "string").map(normalizeGlob) : [];
3323
3510
  function safeParse(src) {
3324
3511
  try {
3325
3512
  const v = parse(src);
3326
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
3513
+ if (isRecord(v)) return v;
3327
3514
  return null;
3328
3515
  } catch {
3329
3516
  return null;
@@ -3334,18 +3521,24 @@ function effective(raw) {
3334
3521
  const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
3335
3522
  const base = defaultPolicy(version);
3336
3523
  const rules = { ...base.rules };
3337
- for (const [name, cfg] of Object.entries(raw.rules ?? {})) {
3338
- const over = cfg && typeof cfg === "object" && !Array.isArray(cfg) ? cfg : {};
3339
- rules[name] = { ...rules[name] ?? {}, ...over, ...over.exclude ? { exclude: globs2(over.exclude) } : {} };
3524
+ for (const [name, over] of Object.entries(ruleOverrides(raw.rules))) {
3525
+ rules[name] = {
3526
+ ...rules[name] ?? {},
3527
+ ...over.severity !== void 0 ? { severity: over.severity } : {},
3528
+ ...over.enabled !== void 0 ? { enabled: over.enabled } : {},
3529
+ ...over.exclude !== void 0 ? { exclude: globs2(over.exclude) } : {}
3530
+ };
3340
3531
  }
3341
- const userProtected = raw.protected && typeof raw.protected === "object" ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
3532
+ const userProtected = isRecord(raw.protected) ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
3533
+ const signoff = isRecord(raw.signoff) ? raw.signoff : {};
3534
+ const requiredFor = [signoff.required_for, signoff.requiredFor].find(Array.isArray);
3342
3535
  return {
3343
3536
  version,
3344
3537
  rules,
3345
3538
  ignore: raw.ignore ? globs2(raw.ignore) : base.ignore ?? [],
3346
3539
  protected: mergeProtected(base.protected, userProtected),
3347
- requiredFor: raw.signoff?.required_for ?? raw.signoff?.requiredFor ?? base.signoff.requiredFor,
3348
- ledger: typeof raw.signoff?.ledger === "string" ? raw.signoff.ledger : base.signoff.ledger
3540
+ requiredFor: requiredFor ? requiredFor.filter((s) => typeof s === "string") : base.signoff.requiredFor,
3541
+ ledger: typeof signoff.ledger === "string" ? signoff.ledger : base.signoff.ledger
3349
3542
  };
3350
3543
  }
3351
3544
  function policyWeakening(before, after) {
@@ -3400,8 +3593,8 @@ function policyWeakening(before, after) {
3400
3593
  if (ae.ledger !== be.ledger) {
3401
3594
  reasons.push(`sign-off ledger moved (${be.ledger} \u2192 ${ae.ledger}) \u2014 the local layer would trust a different file`);
3402
3595
  }
3403
- const bv = b.verify;
3404
- const av = a.verify;
3596
+ const bv = verifyBlock(b.verify);
3597
+ const av = verifyBlock(a.verify);
3405
3598
  if (bv?.command) {
3406
3599
  if (!av?.command) {
3407
3600
  reasons.push("verify block removed \u2014 pristine-suite re-execution disabled");
@@ -4159,7 +4352,7 @@ function scriptWeakening(before, after, opts = {}) {
4159
4352
  const evidence = (trace?.line ?? before.find((l) => checkIdentity(stripComment(l).trim().replace(/^!\s+/, "").replace(PREFIX, "")) === id) ?? id).trim();
4160
4353
  out2.push({ reason: `the check invocation \`${id}\` was ${how}`, evidence });
4161
4354
  }
4162
- const floor = beforeInv.filter((i) => i.state === "live" && i.pin && PLAIN_SEMVER.test(i.pin)).map((i) => i.pin).sort((a, b) => compareVersions(a, b) ?? 0)[0];
4355
+ const floor = beforeInv.flatMap((i) => i.state === "live" && i.pin && PLAIN_SEMVER.test(i.pin) ? [i.pin] : []).sort((a, b) => compareVersions(a, b) ?? 0)[0];
4163
4356
  if (floor) {
4164
4357
  for (const inv of afterInv) {
4165
4358
  if (inv.state !== "live" || !inv.pin) continue;
@@ -4555,7 +4748,10 @@ function destinations(cmd, args, isDir) {
4555
4748
  positional.push(a);
4556
4749
  }
4557
4750
  const base = (p) => p.replace(/\/+$/, "").split("/").pop() ?? p;
4558
- if (target !== null) return positional.map((p) => `${target.replace(/\/+$/, "")}/${base(p)}`);
4751
+ if (target !== null) {
4752
+ const dir = target.replace(/\/+$/, "");
4753
+ return positional.map((p) => `${dir}/${base(p)}`);
4754
+ }
4559
4755
  if (positional.length < 2) return [];
4560
4756
  const dest = positional[positional.length - 1];
4561
4757
  const srcs = positional.slice(0, -1);
@@ -4569,16 +4765,18 @@ function expandShellPath(t, env = process.env) {
4569
4765
  }
4570
4766
  function hookTests(policy, ctx) {
4571
4767
  const cwd = ctx?.cwd;
4572
- const onDisk = cwd !== void 0 && existsSync2(cwd);
4573
- const root = onDisk ? canonicalPath(cwd) : cwd;
4768
+ const diskCwd = cwd !== void 0 && existsSync2(cwd) ? cwd : void 0;
4769
+ const onDisk = diskCwd !== void 0;
4770
+ const root = diskCwd !== void 0 ? canonicalPath(diskCwd) : cwd;
4574
4771
  const memo = /* @__PURE__ */ new Map();
4575
4772
  const rel = (t) => {
4576
4773
  const hit = memo.get(t);
4577
4774
  if (hit !== void 0) return hit;
4578
4775
  let p = expandShellPath(t.replace(/^["']|["']$/g, ""));
4579
4776
  const pathy = isAbsolute4(p) || p.includes("/") && !/^[-:]/.test(p);
4580
- if (pathy && (isAbsolute4(p) || onDisk)) {
4581
- p = canonicalPath(isAbsolute4(p) ? p : resolve2(cwd, p));
4777
+ const abs2 = isAbsolute4(p) ? p : diskCwd !== void 0 ? resolve2(diskCwd, p) : null;
4778
+ if (pathy && abs2 !== null) {
4779
+ p = canonicalPath(abs2);
4582
4780
  if (root !== void 0) {
4583
4781
  if (p === root) p = ".";
4584
4782
  else if (p.startsWith(root + "/")) p = p.slice(root.length + 1);
@@ -4743,7 +4941,7 @@ function xargsWritesHook(toks, fed, policy, ctx) {
4743
4941
  }
4744
4942
 
4745
4943
  // src/detectors/hook-tampering.ts
4746
- var RULE6 = "hook-tampering";
4944
+ var RULE7 = "hook-tampering";
4747
4945
  var ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
4748
4946
  var EVENTS = ["PreToolUse", "Stop"];
4749
4947
  var isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -4959,7 +5157,7 @@ function coverage(cs) {
4959
5157
  const live = cs.filter((c) => c.runs);
4960
5158
  if (live.length === 0) return "none";
4961
5159
  if (live.some((c) => c.tools === null)) return null;
4962
- return new Set(live.flatMap((c) => [...c.tools]));
5160
+ return new Set(live.flatMap((c) => c.tools ? [...c.tools] : []));
4963
5161
  }
4964
5162
  function duplicateJsonKeys(src) {
4965
5163
  const dups = [];
@@ -5072,7 +5270,7 @@ function settingsWeakening(before, after, local) {
5072
5270
  const schema = schemaProblem(e.group, e.h);
5073
5271
  if (schema) {
5074
5272
  reasons.push(`a ${e.event} hook entry carries a value the runtime's schema rejects (${schema}) \u2014 the runtime continues without the broken file, the gate included`);
5075
- } else if (e.event === "PreToolUse" || e.event === "Stop") {
5273
+ } else if ((e.event === "PreToolUse" || e.event === "Stop") && isObj(e.h)) {
5076
5274
  reasons.push(`a ${e.event} hook entry beside the gate ${siblingProblem(e.h)}`);
5077
5275
  }
5078
5276
  }
@@ -5090,7 +5288,7 @@ function claudeSettingsFindings(c, policy) {
5090
5288
  if (c.after == null) return null;
5091
5289
  const after = parseSettings(c.after);
5092
5290
  const before = c.before != null ? parseSettings(c.before) : null;
5093
- const finding = (reason) => makeFinding(RULE6, policy, {
5291
+ const finding = (reason) => makeFinding(RULE7, policy, {
5094
5292
  file: c.path,
5095
5293
  message: `The Claude Code hook wiring was weakened: ${reason}.`,
5096
5294
  evidence: reason,
@@ -5111,10 +5309,10 @@ function claudeSettingsFindings(c, policy) {
5111
5309
  function policyFindings(c, policy) {
5112
5310
  if (c.after == null) return null;
5113
5311
  const added = c.before == null;
5114
- const reasons = added ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
5312
+ const reasons = c.before == null ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
5115
5313
  if (reasons === null) return null;
5116
5314
  return reasons.map(
5117
- (reason) => makeFinding(RULE6, policy, {
5315
+ (reason) => makeFinding(RULE7, policy, {
5118
5316
  file: c.path,
5119
5317
  message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
5120
5318
  evidence: reason,
@@ -5177,7 +5375,7 @@ function configFindings(c, changes, policy, ctx) {
5177
5375
  reasons = after === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : isLefthook(c.path) ? lefthookWeakening(before, after) : preCommitWeakening(before, after);
5178
5376
  }
5179
5377
  return reasons.map(
5180
- (reason) => makeFinding(RULE6, policy, {
5378
+ (reason) => makeFinding(RULE7, policy, {
5181
5379
  file: c.path,
5182
5380
  message: `The hook configuration was weakened: ${reason}.`,
5183
5381
  evidence: reason,
@@ -5260,7 +5458,7 @@ function huskyRuntimeWrite(path, content, ctx) {
5260
5458
  const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
5261
5459
  const entry = inspectRel(join6(ctx.cwd, root), ".husky/pre-commit");
5262
5460
  if (entry.kind !== "file") return false;
5263
- const gate = textOf(entry);
5461
+ const gate = textOf2(entry);
5264
5462
  if (gate == null) return false;
5265
5463
  return invocations(gate.split("\n"), { errexit: true }).some(
5266
5464
  (i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
@@ -5269,7 +5467,7 @@ function huskyRuntimeWrite(path, content, ctx) {
5269
5467
  var isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
5270
5468
  var isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
5271
5469
  var hookTampering = {
5272
- id: RULE6,
5470
+ id: RULE7,
5273
5471
  surface: ["file", "command"],
5274
5472
  certainty: "mechanical",
5275
5473
  run(changes, policy, _view, ctx) {
@@ -5282,7 +5480,7 @@ var hookTampering = {
5282
5480
  if (isPackageJson(c.path) && c.before != null && c.after != null) {
5283
5481
  for (const reason of packageJsonWeakening(c.before, c.after)) {
5284
5482
  out2.push(
5285
- makeFinding(RULE6, policy, {
5483
+ makeFinding(RULE7, policy, {
5286
5484
  file: c.path,
5287
5485
  message: `The hook installation was weakened: ${reason}.`,
5288
5486
  evidence: reason,
@@ -5295,7 +5493,7 @@ var hookTampering = {
5295
5493
  }
5296
5494
  if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
5297
5495
  out2.push(
5298
- makeFinding(RULE6, policy, {
5496
+ makeFinding(RULE7, policy, {
5299
5497
  file: c.path,
5300
5498
  message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
5301
5499
  evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
@@ -5305,7 +5503,7 @@ var hookTampering = {
5305
5503
  }
5306
5504
  if (c.op === "delete") {
5307
5505
  out2.push(
5308
- makeFinding(RULE6, policy, {
5506
+ makeFinding(RULE7, policy, {
5309
5507
  file: c.path,
5310
5508
  message: "A protected hook/policy file was deleted.",
5311
5509
  evidence: c.path,
@@ -5314,7 +5512,7 @@ var hookTampering = {
5314
5512
  );
5315
5513
  } else if (c.op === "rename" && !renameKept(c)) {
5316
5514
  out2.push(
5317
- makeFinding(RULE6, policy, {
5515
+ makeFinding(RULE7, policy, {
5318
5516
  file: c.path,
5319
5517
  message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
5320
5518
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -5328,7 +5526,7 @@ var hookTampering = {
5328
5526
  if (c.before != null && c.after != null) {
5329
5527
  for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
5330
5528
  out2.push(
5331
- makeFinding(RULE6, policy, {
5529
+ makeFinding(RULE7, policy, {
5332
5530
  file: c.path,
5333
5531
  message: `CODEOWNERS was weakened: ${reason}.`,
5334
5532
  evidence: reason,
@@ -5344,12 +5542,12 @@ var hookTampering = {
5344
5542
  const husky = isHuskyScript(c.path);
5345
5543
  const opts = husky ? { errexit: true } : {};
5346
5544
  const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
5347
- if (beforePin !== null) {
5545
+ if (beforePin !== null && c.after != null) {
5348
5546
  const afterPin = initScriptPin(c.after);
5349
5547
  const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
5350
5548
  if (!kept) {
5351
5549
  out2.push(
5352
- makeFinding(RULE6, policy, {
5550
+ makeFinding(RULE7, policy, {
5353
5551
  file: c.path,
5354
5552
  message: "A protected hook script no longer matches the shape `tamperward init` writes.",
5355
5553
  evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
@@ -5372,7 +5570,7 @@ var hookTampering = {
5372
5570
  if (shebang || !live) {
5373
5571
  const why = shebang ?? (gate.length ? `its \`${gate[0].identity}\` is ${gate[0].state}${gate[0].why ? ` (${gate[0].why})` : ""}` : "it runs no `tamperward check`");
5374
5572
  out2.push(
5375
- makeFinding(RULE6, policy, {
5573
+ makeFinding(RULE7, policy, {
5376
5574
  file: c.path,
5377
5575
  message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
5378
5576
  evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or make the gate live in it`,
@@ -5381,7 +5579,7 @@ var hookTampering = {
5381
5579
  );
5382
5580
  } else if (earlyExit) {
5383
5581
  out2.push(
5384
- makeFinding(RULE6, policy, {
5582
+ makeFinding(RULE7, policy, {
5385
5583
  file: c.path,
5386
5584
  line: earlyExit.newLine ?? void 0,
5387
5585
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -5403,7 +5601,7 @@ var hookTampering = {
5403
5601
  deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
5404
5602
  ].filter((d) => d !== null);
5405
5603
  out2.push(
5406
- makeFinding(RULE6, policy, {
5604
+ makeFinding(RULE7, policy, {
5407
5605
  file: c.path,
5408
5606
  message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
5409
5607
  evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or restore it`,
@@ -5415,7 +5613,7 @@ var hookTampering = {
5415
5613
  }
5416
5614
  if (shebangChanged && c.after != null) {
5417
5615
  out2.push(
5418
- makeFinding(RULE6, policy, {
5616
+ makeFinding(RULE7, policy, {
5419
5617
  file: c.path,
5420
5618
  line: 1,
5421
5619
  message: `A protected hook's interpreter was changed: ${shebang}.`,
@@ -5426,7 +5624,7 @@ var hookTampering = {
5426
5624
  }
5427
5625
  if (earlyExit) {
5428
5626
  out2.push(
5429
- makeFinding(RULE6, policy, {
5627
+ makeFinding(RULE7, policy, {
5430
5628
  file: c.path,
5431
5629
  line: earlyExit.newLine ?? void 0,
5432
5630
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -5438,7 +5636,7 @@ var hookTampering = {
5438
5636
  const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
5439
5637
  if (deadGuard) {
5440
5638
  out2.push(
5441
- makeFinding(RULE6, policy, {
5639
+ makeFinding(RULE7, policy, {
5442
5640
  file: c.path,
5443
5641
  line: deadGuard.newLine ?? void 0,
5444
5642
  message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
@@ -5450,7 +5648,7 @@ var hookTampering = {
5450
5648
  const lost = scriptWeakening(beforeLines, afterLines, opts);
5451
5649
  if (lost.length) {
5452
5650
  out2.push(
5453
- makeFinding(RULE6, policy, {
5651
+ makeFinding(RULE7, policy, {
5454
5652
  file: c.path,
5455
5653
  message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
5456
5654
  evidence: lost[0].evidence,
@@ -5469,7 +5667,7 @@ var hookTampering = {
5469
5667
  const m = l.content.match(INLINE);
5470
5668
  if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
5471
5669
  out2.push(
5472
- makeFinding(RULE6, policy, {
5670
+ makeFinding(RULE7, policy, {
5473
5671
  file: c.path,
5474
5672
  line: l.newLine ?? void 0,
5475
5673
  message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
@@ -5479,7 +5677,7 @@ var hookTampering = {
5479
5677
  );
5480
5678
  } else if (/enabled:\s*false\b/.test(l.content)) {
5481
5679
  out2.push(
5482
- makeFinding(RULE6, policy, {
5680
+ makeFinding(RULE7, policy, {
5483
5681
  file: c.path,
5484
5682
  line: l.newLine ?? void 0,
5485
5683
  message: "A rule was disabled (enabled: false) in the policy.",
@@ -5497,7 +5695,7 @@ var hookTampering = {
5497
5695
  const hiding = updateIndexHiding(toks, policy);
5498
5696
  if (hiding) {
5499
5697
  out2.push(
5500
- makeFinding(RULE6, policy, {
5698
+ makeFinding(RULE7, policy, {
5501
5699
  file: repoRelative(hiding.file, ctx),
5502
5700
  message: `Hook tampering via shell: ${hiding.reason}.`,
5503
5701
  evidence: seg,
@@ -5513,7 +5711,7 @@ var hookTampering = {
5513
5711
  const file = target ? repoRelative(target, ctx) : void 0;
5514
5712
  const outside = file !== void 0 && (isAbsolute5(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
5515
5713
  out2.push(
5516
- makeFinding(RULE6, policy, {
5714
+ makeFinding(RULE7, policy, {
5517
5715
  ...file !== void 0 ? { file } : {},
5518
5716
  message: `Hook tampering via shell: ${why}${outside}.`,
5519
5717
  evidence: seg,
@@ -5530,11 +5728,11 @@ var hookTampering = {
5530
5728
  };
5531
5729
 
5532
5730
  // src/detectors/test-deletion.ts
5533
- import ts5 from "typescript";
5731
+ import ts6 from "typescript";
5534
5732
 
5535
5733
  // src/detectors/suite-config.ts
5536
5734
  import picomatch3 from "picomatch";
5537
- import ts4 from "typescript";
5735
+ import ts5 from "typescript";
5538
5736
  var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
5539
5737
  var JEST_DEFAULT_IGNORE = ["/node_modules/"];
5540
5738
  var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
@@ -5565,18 +5763,18 @@ var PYTEST_CANONICAL_SAMPLES = [
5565
5763
  "tests/conftest.py"
5566
5764
  ];
5567
5765
  function keyName2(name) {
5568
- if (ts4.isIdentifier(name) || ts4.isStringLiteral(name)) return name.text;
5766
+ if (ts5.isIdentifier(name) || ts5.isStringLiteral(name)) return name.text;
5569
5767
  return null;
5570
5768
  }
5571
- var isStr = (e) => ts4.isStringLiteral(e) || ts4.isNoSubstitutionTemplateLiteral(e);
5769
+ var isStr = (e) => ts5.isStringLiteral(e) || ts5.isNoSubstitutionTemplateLiteral(e);
5572
5770
  function literals2(e) {
5573
5771
  if (isStr(e)) return { items: [e.text], opaque: false };
5574
- if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5772
+ if (!ts5.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5575
5773
  const items = [];
5576
5774
  let opaque = false;
5577
5775
  for (const el of e.elements) {
5578
5776
  if (isStr(el)) items.push(el.text);
5579
- else if (ts4.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5777
+ else if (ts5.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5580
5778
  items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5581
5779
  } else opaque = true;
5582
5780
  }
@@ -5584,10 +5782,10 @@ function literals2(e) {
5584
5782
  }
5585
5783
  function ownerKey2(node) {
5586
5784
  const obj = node.parent;
5587
- if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
5785
+ if (!obj || !ts5.isObjectLiteralExpression(obj)) return null;
5588
5786
  let q = obj.parent;
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;
5787
+ while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
5788
+ return q && ts5.isPropertyAssignment(q) ? keyName2(q.name) : null;
5591
5789
  }
5592
5790
  function asExpression2(src) {
5593
5791
  const t = src.trim();
@@ -5622,13 +5820,12 @@ function effectivePytestFile(files) {
5622
5820
  function effectivePytestConfig(path, ctx) {
5623
5821
  if (path.includes("/")) return false;
5624
5822
  const base = path;
5625
- const ORDER = PYTEST_INI_ORDER;
5626
- const rank = ORDER.indexOf(base);
5823
+ const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
5627
5824
  if (rank < 0) return false;
5628
5825
  const files = ctx?.trackedFiles;
5629
5826
  if (!files) return true;
5630
5827
  const rootNames = new Set(files.filter((f) => !f.includes("/")));
5631
- return !ORDER.slice(0, rank).some((n) => rootNames.has(n));
5828
+ return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
5632
5829
  }
5633
5830
  function pytestBlock(src) {
5634
5831
  const lines = src.split(/\r?\n/);
@@ -5728,15 +5925,15 @@ function collect(root, runner) {
5728
5925
  projectList = e;
5729
5926
  };
5730
5927
  const visit = (node) => {
5731
- if (ts4.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5928
+ if (ts5.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5732
5929
  sel.present = true;
5733
5930
  sel.opaque = true;
5734
5931
  }
5735
- if (ts4.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts4.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5932
+ if (ts5.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts5.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5736
5933
  sel.present = true;
5737
5934
  sel.opaque = true;
5738
5935
  }
5739
- if (ts4.isPropertyAssignment(node)) {
5936
+ if (ts5.isPropertyAssignment(node)) {
5740
5937
  const k = keyName2(node.name);
5741
5938
  if (runner === "jest") {
5742
5939
  if (k === "testMatch") sel.include = take(k, node.initializer);
@@ -5766,7 +5963,7 @@ function collect(root, runner) {
5766
5963
  }
5767
5964
  }
5768
5965
  }
5769
- ts4.forEachChild(node, visit);
5966
+ ts5.forEachChild(node, visit);
5770
5967
  };
5771
5968
  visit(root);
5772
5969
  if (projectList) sel.projects = readProjects(projectList, sel, runner);
@@ -5775,30 +5972,30 @@ function collect(root, runner) {
5775
5972
  }
5776
5973
  function unwrapConfigCall(e) {
5777
5974
  let x = e;
5778
- while (ts4.isParenthesizedExpression(x) || ts4.isAsExpression(x) || x.kind === ts4.SyntaxKind.SatisfiesExpression) {
5975
+ while (ts5.isParenthesizedExpression(x) || ts5.isAsExpression(x) || ts5.isSatisfiesExpression(x)) {
5779
5976
  x = x.expression;
5780
5977
  }
5781
- if (ts4.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5978
+ if (ts5.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5782
5979
  return x;
5783
5980
  }
5784
5981
  var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
5785
5982
  function spreadsIntoConfig(node, runner) {
5786
5983
  const obj = node.parent;
5787
- if (!ts4.isObjectLiteralExpression(obj)) return false;
5788
- const keys = obj.properties.map((p) => ts4.isPropertyAssignment(p) ? keyName2(p.name) : null);
5984
+ if (!ts5.isObjectLiteralExpression(obj)) return false;
5985
+ const keys = obj.properties.map((p) => ts5.isPropertyAssignment(p) ? keyName2(p.name) : null);
5789
5986
  if (runner === "vitest") {
5790
5987
  if (keys.includes("test")) return true;
5791
5988
  let q = obj.parent;
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";
5989
+ while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
5990
+ return !!q && ts5.isPropertyAssignment(q) && keyName2(q.name) === "test";
5794
5991
  }
5795
5992
  return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
5796
5993
  }
5797
5994
  function extendsOf(el) {
5798
5995
  for (const p of el.properties) {
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;
5996
+ if (!ts5.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
5997
+ if (p.initializer.kind === ts5.SyntaxKind.TrueKeyword) return true;
5998
+ if (p.initializer.kind === ts5.SyntaxKind.FalseKeyword) return null;
5802
5999
  return "path";
5803
6000
  }
5804
6001
  return null;
@@ -5819,14 +6016,14 @@ function readProject(el, root, runner) {
5819
6016
  return p;
5820
6017
  }
5821
6018
  function readProjects(e, root, runner) {
5822
- if (!ts4.isArrayLiteralExpression(e)) {
6019
+ if (!ts5.isArrayLiteralExpression(e)) {
5823
6020
  root.opaque = true;
5824
6021
  return [];
5825
6022
  }
5826
6023
  const list2 = [];
5827
6024
  for (const el of e.elements) {
5828
6025
  const lit = unwrapConfigCall(el);
5829
- if (ts4.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
6026
+ if (ts5.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
5830
6027
  else root.opaque = true;
5831
6028
  }
5832
6029
  return list2;
@@ -5834,14 +6031,14 @@ function readProjects(e, root, runner) {
5834
6031
  function workspaceArray(sf) {
5835
6032
  for (const st of sf.statements) {
5836
6033
  let e;
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;
6034
+ if (ts5.isExportAssignment(st)) e = st.expression;
6035
+ else if (ts5.isExpressionStatement(st) && ts5.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
5839
6036
  if (!e) continue;
5840
- while (ts4.isParenthesizedExpression(e) || ts4.isAsExpression(e) || e.kind === ts4.SyntaxKind.SatisfiesExpression) {
6037
+ while (ts5.isParenthesizedExpression(e) || ts5.isAsExpression(e) || ts5.isSatisfiesExpression(e)) {
5841
6038
  e = e.expression;
5842
6039
  }
5843
- if (ts4.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
5844
- return ts4.isArrayLiteralExpression(e) ? e : null;
6040
+ if (ts5.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
6041
+ return ts5.isArrayLiteralExpression(e) ? e : null;
5845
6042
  }
5846
6043
  return null;
5847
6044
  }
@@ -5849,7 +6046,7 @@ function parseSelection(src, runner, path = "") {
5849
6046
  let sel = empty();
5850
6047
  if (runner === "pytest") return pytestSelection(src);
5851
6048
  try {
5852
- const sf = ts4.createSourceFile("cfg.ts", asExpression2(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
6049
+ const sf = ts5.createSourceFile("cfg.ts", asExpression2(src), ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
5853
6050
  if (runner === "vitest" && isWorkspaceFile(path)) {
5854
6051
  const arr = workspaceArray(sf);
5855
6052
  if (arr) {
@@ -5943,7 +6140,7 @@ function suiteNarrowings(before, after, path, samples) {
5943
6140
  }
5944
6141
 
5945
6142
  // src/detectors/test-deletion.ts
5946
- var RULE7 = "test-deletion";
6143
+ var RULE8 = "test-deletion";
5947
6144
  var JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
5948
6145
  var TEST_DEFS = {
5949
6146
  py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
@@ -5956,21 +6153,21 @@ var TEST_DEFS = {
5956
6153
  cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
5957
6154
  };
5958
6155
  function calleeName(expr) {
5959
- if (ts5.isIdentifier(expr)) return expr.text;
5960
- if (ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression)) {
6156
+ if (ts6.isIdentifier(expr)) return expr.text;
6157
+ if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression)) {
5961
6158
  return expr.expression.text;
5962
6159
  }
5963
6160
  return null;
5964
6161
  }
5965
6162
  var TABLE_METHOD = /^(?:each|for)$/;
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);
6163
+ var isEachOf = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
6164
+ var isDescribeEach = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
5968
6165
  function loopRows(iterable) {
5969
- if (ts5.isArrayLiteralExpression(iterable)) {
6166
+ if (ts6.isArrayLiteralExpression(iterable)) {
5970
6167
  let n = 0;
5971
6168
  let open = false;
5972
6169
  for (const el of iterable.elements) {
5973
- if (ts5.isSpreadElement(el)) open = true;
6170
+ if (ts6.isSpreadElement(el)) open = true;
5974
6171
  else n++;
5975
6172
  }
5976
6173
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -5982,20 +6179,20 @@ function templateRows(node) {
5982
6179
  return Math.max(rows, 1);
5983
6180
  }
5984
6181
  function hasSubstantiveBody(call) {
5985
- const fn = call.arguments.find((a) => ts5.isArrowFunction(a) || ts5.isFunctionExpression(a));
6182
+ const fn = call.arguments.find((a) => ts6.isArrowFunction(a) || ts6.isFunctionExpression(a));
5986
6183
  if (!fn) return true;
5987
6184
  const body = fn.body;
5988
- const text = ts5.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
6185
+ const text = ts6.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
5989
6186
  return text.split("\n").some((l) => isSignificantLine(l.trim(), "js"));
5990
6187
  }
5991
6188
  function eachRows(call) {
5992
6189
  const arg = call.arguments[0];
5993
6190
  if (!arg) return { n: 1, open: true };
5994
- if (ts5.isArrayLiteralExpression(arg)) {
6191
+ if (ts6.isArrayLiteralExpression(arg)) {
5995
6192
  let n = 0;
5996
6193
  let open = false;
5997
6194
  for (const el of arg.elements) {
5998
- if (ts5.isSpreadElement(el)) open = true;
6195
+ if (ts6.isSpreadElement(el)) open = true;
5999
6196
  else n++;
6000
6197
  }
6001
6198
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -6010,35 +6207,35 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
6010
6207
  for (const line of src.split("\n")) if (re.test(line)) n2++;
6011
6208
  return { min: n2, open: false };
6012
6209
  }
6013
- const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
6210
+ const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6014
6211
  let n = 0;
6015
6212
  let open = false;
6016
6213
  const visit = (node, mult) => {
6017
- if (ts5.isForOfStatement(node)) {
6214
+ if (ts6.isForOfStatement(node)) {
6018
6215
  const r = loopRows(node.expression);
6019
6216
  if (r.open) open = true;
6020
6217
  visit(node.statement, mult * r.n);
6021
6218
  return;
6022
6219
  }
6023
- if (ts5.isForStatement(node) || ts5.isForInStatement(node) || ts5.isWhileStatement(node) || ts5.isDoStatement(node)) {
6220
+ if (ts6.isForStatement(node) || ts6.isForInStatement(node) || ts6.isWhileStatement(node) || ts6.isDoStatement(node)) {
6024
6221
  open = true;
6025
6222
  }
6026
- if (ts5.isCallExpression(node)) {
6223
+ if (ts6.isCallExpression(node)) {
6027
6224
  const callee = node.expression;
6028
- if (ts5.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6225
+ if (ts6.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6029
6226
  const r = loopRows(callee.expression);
6030
6227
  if (r.open) open = true;
6031
6228
  for (const a of node.arguments) visit(a, mult * r.n);
6032
6229
  return;
6033
6230
  }
6034
6231
  const head = node.expression;
6035
- if (ts5.isCallExpression(head) && isDescribeEach(head.expression)) {
6232
+ if (ts6.isCallExpression(head) && isDescribeEach(head.expression)) {
6036
6233
  const r = eachRows(head);
6037
6234
  if (r.open) open = true;
6038
6235
  for (const a of node.arguments) visit(a, mult * r.n);
6039
6236
  return;
6040
6237
  }
6041
- if (ts5.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6238
+ if (ts6.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6042
6239
  for (const a of node.arguments) visit(a, mult * templateRows(head));
6043
6240
  return;
6044
6241
  }
@@ -6050,10 +6247,10 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
6050
6247
  const name = calleeName(node.expression);
6051
6248
  if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
6052
6249
  }
6053
- } else if (ts5.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6250
+ } else if (ts6.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6054
6251
  n += templateRows(node) * mult;
6055
6252
  }
6056
- ts5.forEachChild(node, (c) => visit(c, mult));
6253
+ ts6.forEachChild(node, (c) => visit(c, mult));
6057
6254
  };
6058
6255
  visit(sf, 1);
6059
6256
  return { min: n, open };
@@ -6090,7 +6287,8 @@ function scriptNarrowings(before, after, path) {
6090
6287
  if ((path.split("/").pop() ?? path) !== "package.json") return [];
6091
6288
  const scriptsOf = (src) => {
6092
6289
  try {
6093
- const s = JSON.parse(src ?? "{}").scripts ?? {};
6290
+ const pkg = JSON.parse(src ?? "{}");
6291
+ const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
6094
6292
  return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
6095
6293
  } catch {
6096
6294
  return {};
@@ -6135,7 +6333,7 @@ function leavesGlob(dir, dest, policy, ctx) {
6135
6333
  }
6136
6334
  var SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
6137
6335
  var testDeletion = {
6138
- id: RULE7,
6336
+ id: RULE8,
6139
6337
  surface: ["file", "command"],
6140
6338
  certainty: "mechanical",
6141
6339
  run(changes, policy, _view, ctx) {
@@ -6166,7 +6364,7 @@ var testDeletion = {
6166
6364
  if (c.op === "delete" && isTest) {
6167
6365
  if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
6168
6366
  out2.push(
6169
- makeFinding(RULE7, policy, {
6367
+ makeFinding(RULE8, policy, {
6170
6368
  file: c.path,
6171
6369
  message: "A test file was deleted.",
6172
6370
  evidence: c.path,
@@ -6175,7 +6373,7 @@ var testDeletion = {
6175
6373
  );
6176
6374
  } else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
6177
6375
  out2.push(
6178
- makeFinding(RULE7, policy, {
6376
+ makeFinding(RULE8, policy, {
6179
6377
  file: c.path,
6180
6378
  message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
6181
6379
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -6190,7 +6388,7 @@ var testDeletion = {
6190
6388
  const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
6191
6389
  if (isRelocation(removed, before.min - after.min)) continue;
6192
6390
  out2.push(
6193
- makeFinding(RULE7, policy, {
6391
+ makeFinding(RULE8, policy, {
6194
6392
  file: c.path,
6195
6393
  message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
6196
6394
  evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
@@ -6203,7 +6401,7 @@ var testDeletion = {
6203
6401
  const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
6204
6402
  for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
6205
6403
  out2.push(
6206
- makeFinding(RULE7, policy, {
6404
+ makeFinding(RULE8, policy, {
6207
6405
  file: c.path,
6208
6406
  message: `The runner no longer selects ${n.path}: ${n.reason}.`,
6209
6407
  evidence: `${n.path} dropped from the suite by ${c.path}`,
@@ -6213,7 +6411,7 @@ var testDeletion = {
6213
6411
  }
6214
6412
  for (const n of scriptNarrowings(c.before, c.after, c.path)) {
6215
6413
  out2.push(
6216
- makeFinding(RULE7, policy, {
6414
+ makeFinding(RULE8, policy, {
6217
6415
  file: c.path,
6218
6416
  message: `The test script now narrows the suite: ${n.flag} added to scripts.${n.script}.`,
6219
6417
  evidence: `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${JSON.stringify(n.after)}`,
@@ -6301,7 +6499,7 @@ var testDeletion = {
6301
6499
  }
6302
6500
  if (why) {
6303
6501
  out2.push(
6304
- makeFinding(RULE7, policy, {
6502
+ makeFinding(RULE8, policy, {
6305
6503
  message: `Test removed via shell: ${why}.`,
6306
6504
  evidence,
6307
6505
  remediation: "Fix the code under test. Mutating a spec from the shell is still deleting the test \u2014 and is blocked the same way."
@@ -6316,11 +6514,10 @@ var testDeletion = {
6316
6514
  }
6317
6515
  };
6318
6516
  function pytestPrecedenceFindings(changes, policy, ctx) {
6319
- const order = PYTEST_INI_ORDER;
6320
6517
  const touched = /* @__PURE__ */ new Map();
6321
6518
  for (const c of changes) {
6322
6519
  if (c.kind !== "file" || c.oldPath) continue;
6323
- if (order.includes(c.path)) touched.set(c.path, c);
6520
+ if (PYTEST_INI_ORDER.some((name) => name === c.path)) touched.set(c.path, c);
6324
6521
  }
6325
6522
  if (touched.size === 0) return [];
6326
6523
  const listing = trackedFiles(ctx);
@@ -6328,7 +6525,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6328
6525
  const rootNames = new Set(listing.filter((f) => !f.includes("/")));
6329
6526
  const base = /* @__PURE__ */ new Map();
6330
6527
  const head = /* @__PURE__ */ new Map();
6331
- for (const name of order) {
6528
+ for (const name of PYTEST_INI_ORDER) {
6332
6529
  const c = touched.get(name);
6333
6530
  if (c) {
6334
6531
  base.set(name, c.before);
@@ -6345,7 +6542,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6345
6542
  if (!was || !now || was.path === now.path) return [];
6346
6543
  const samples = runnerSamples(now.path, policy, ctx, "pytest");
6347
6544
  return suiteNarrowings(was.content, now.content, now.path, samples).map(
6348
- (n) => makeFinding(RULE7, policy, {
6545
+ (n) => makeFinding(RULE8, policy, {
6349
6546
  file: now.path,
6350
6547
  message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
6351
6548
  evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
@@ -6355,8 +6552,8 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6355
6552
  }
6356
6553
 
6357
6554
  // src/detectors/test-content-removal.ts
6358
- import ts6 from "typescript";
6359
- var RULE8 = "test-content-removal";
6555
+ import ts7 from "typescript";
6556
+ var RULE9 = "test-content-removal";
6360
6557
  var MIN_REMOVED_LINES = 3;
6361
6558
  var MIN_REMOVED_ROWS = 2;
6362
6559
  var ws = (s) => s.replace(/\s+/g, "");
@@ -6365,13 +6562,13 @@ function eachTables(src, path) {
6365
6562
  let open = false;
6366
6563
  if (langOf(path) !== "js" && langOf(path) !== null) return { rows, open };
6367
6564
  try {
6368
- const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6565
+ const sf = ts7.createSourceFile("spec.ts", src, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
6369
6566
  const visit = (node) => {
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)) {
6567
+ if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts7.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
6371
6568
  const arg = node.arguments[0];
6372
- if (arg && ts6.isArrayLiteralExpression(arg)) {
6569
+ if (arg && ts7.isArrayLiteralExpression(arg)) {
6373
6570
  for (const el of arg.elements) {
6374
- if (ts6.isSpreadElement(el)) {
6571
+ if (ts7.isSpreadElement(el)) {
6375
6572
  open = true;
6376
6573
  continue;
6377
6574
  }
@@ -6381,7 +6578,7 @@ function eachTables(src, path) {
6381
6578
  }
6382
6579
  } else open = true;
6383
6580
  }
6384
- ts6.forEachChild(node, visit);
6581
+ ts7.forEachChild(node, visit);
6385
6582
  };
6386
6583
  visit(sf);
6387
6584
  } catch {
@@ -6399,7 +6596,7 @@ function significantLinesOrdered(src, path) {
6399
6596
  return out2;
6400
6597
  }
6401
6598
  var testContentRemoval = {
6402
- id: RULE8,
6599
+ id: RULE9,
6403
6600
  surface: ["file"],
6404
6601
  certainty: "mechanical",
6405
6602
  run(changes, policy) {
@@ -6440,7 +6637,7 @@ var testContentRemoval = {
6440
6637
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
6441
6638
  if (goneRows.length >= MIN_REMOVED_ROWS) {
6442
6639
  out2.push(
6443
- makeFinding(RULE8, policy, {
6640
+ makeFinding(RULE9, policy, {
6444
6641
  file: c.path,
6445
6642
  message: `Rows removed from a test table: ${goneRows.length} case(s) gone from an each-table that spreads from elsewhere, and they reappear nowhere in the change.`,
6446
6643
  evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -6451,7 +6648,7 @@ var testContentRemoval = {
6451
6648
  }
6452
6649
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
6453
6650
  out2.push(
6454
- makeFinding(RULE8, policy, {
6651
+ makeFinding(RULE9, policy, {
6455
6652
  file: c.path,
6456
6653
  message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
6457
6654
  evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -6465,7 +6662,7 @@ var testContentRemoval = {
6465
6662
  };
6466
6663
 
6467
6664
  // src/detectors/snapshot-rewrite.ts
6468
- var RULE9 = "snapshot-rewrite";
6665
+ var RULE10 = "snapshot-rewrite";
6469
6666
  var RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
6470
6667
  var PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
6471
6668
  var UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
@@ -6482,12 +6679,12 @@ function namesProtectedSnapshot(seg, policy) {
6482
6679
  return null;
6483
6680
  }
6484
6681
  var snapshotRewrite = {
6485
- id: RULE9,
6682
+ id: RULE10,
6486
6683
  surface: ["command", "file"],
6487
6684
  certainty: "mechanical",
6488
6685
  run(changes, policy) {
6489
6686
  const out2 = [];
6490
- const warn = (input) => out2.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
6687
+ const warn = (input) => out2.push(makeFinding(RULE10, policy, { ...input, defaultSeverity: "warn" }));
6491
6688
  for (const c of changes) {
6492
6689
  if (c.kind === "command") {
6493
6690
  for (const seg of segments(c.raw)) {
@@ -6516,7 +6713,7 @@ var snapshotRewrite = {
6516
6713
  if (c.op === "add" || !isSnap && !wasSnap) continue;
6517
6714
  const what = c.op === "delete" ? "deleted" : c.op === "rename" && !isSnap ? `renamed out of the protected snapshot set (to ${c.path})` : "rewritten";
6518
6715
  warn({
6519
- file: c.op === "rename" && !isSnap ? c.oldPath : c.path,
6716
+ file: c.op === "rename" && !isSnap ? c.oldPath ?? c.path : c.path,
6520
6717
  message: `A recorded expected output was ${what}.`,
6521
6718
  evidence: c.op === "rename" ? `${c.oldPath} -> ${c.path}` : c.path,
6522
6719
  remediation: "If the intended output genuinely changed, a human should confirm the new expectation against the requirement; if the test was failing, fix the code instead of re-recording the expectation from it."
@@ -6527,7 +6724,7 @@ var snapshotRewrite = {
6527
6724
  };
6528
6725
 
6529
6726
  // src/detectors/snapshot-only.ts
6530
- var RULE10 = "snapshot-only-rewrite";
6727
+ var RULE11 = "snapshot-only-rewrite";
6531
6728
  var COMMIT_VIEWS = ["staged", "range"];
6532
6729
  function isSnapshotChange(c, policy) {
6533
6730
  if (c.kind !== "file") return false;
@@ -6536,7 +6733,7 @@ function isSnapshotChange(c, policy) {
6536
6733
  return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
6537
6734
  }
6538
6735
  var snapshotOnlyRewrite = {
6539
- id: RULE10,
6736
+ id: RULE11,
6540
6737
  surface: ["file"],
6541
6738
  certainty: "mechanical",
6542
6739
  run(changes, policy, view) {
@@ -6546,9 +6743,10 @@ var snapshotOnlyRewrite = {
6546
6743
  const snaps = files.filter((c) => isSnapshotChange(c, policy));
6547
6744
  if (snaps.length === 0 || snaps.length !== files.length) return [];
6548
6745
  const first = snaps[0];
6549
- const names = snaps.map((c) => c.kind === "file" ? c.path : "").filter(Boolean).slice(0, 3).join(", ");
6746
+ if (!first) return [];
6747
+ const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
6550
6748
  return [
6551
- makeFinding(RULE10, policy, {
6749
+ makeFinding(RULE11, policy, {
6552
6750
  file: first.path,
6553
6751
  message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
6554
6752
  evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
@@ -6560,8 +6758,8 @@ var snapshotOnlyRewrite = {
6560
6758
  };
6561
6759
 
6562
6760
  // src/detectors/assertion-weakening.ts
6563
- import ts7 from "typescript";
6564
- var RULE11 = "assertion-weakening";
6761
+ import ts8 from "typescript";
6762
+ var RULE12 = "assertion-weakening";
6565
6763
  var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
6566
6764
  var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
6567
6765
  var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
@@ -6569,55 +6767,55 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
6569
6767
  var compact = (s) => s.replace(/\s+/g, "");
6570
6768
  function literalTitle(node) {
6571
6769
  if (!node) return null;
6572
- if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
6770
+ if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) return node.text;
6573
6771
  return null;
6574
6772
  }
6575
6773
  function scriptKindForPath(path) {
6576
6774
  const lower = path.toLowerCase();
6577
- if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
6775
+ if (lower.endsWith(".tsx")) return ts8.ScriptKind.TSX;
6578
6776
  if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
6579
- return ts7.ScriptKind.TS;
6777
+ return ts8.ScriptKind.TS;
6580
6778
  }
6581
- if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
6582
- return ts7.ScriptKind.JS;
6779
+ if (lower.endsWith(".jsx")) return ts8.ScriptKind.JSX;
6780
+ return ts8.ScriptKind.JS;
6583
6781
  }
6584
6782
  function staticFacts(node) {
6585
6783
  if (!node) return null;
6586
- if (node.kind === ts7.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6587
- if (node.kind === ts7.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6588
- if (node.kind === ts7.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6589
- if (ts7.isIdentifier(node) && node.text === "undefined") {
6784
+ if (node.kind === ts8.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6785
+ if (node.kind === ts8.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6786
+ if (node.kind === ts8.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6787
+ if (ts8.isIdentifier(node) && node.text === "undefined") {
6590
6788
  return { defined: false, truthy: false };
6591
6789
  }
6592
- if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
6593
- if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
6790
+ if (ts8.isVoidExpression(node)) return { defined: false, truthy: false };
6791
+ if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) {
6594
6792
  return { defined: true, truthy: node.text.length > 0 };
6595
6793
  }
6596
- if (ts7.isNumericLiteral(node)) {
6794
+ if (ts8.isNumericLiteral(node)) {
6597
6795
  return { defined: true, truthy: Number(node.text) !== 0 };
6598
6796
  }
6599
- if (ts7.isBigIntLiteral(node)) {
6797
+ if (ts8.isBigIntLiteral(node)) {
6600
6798
  return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
6601
6799
  }
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);
6800
+ if (ts8.isPrefixUnaryExpression(node) && (node.operator === ts8.SyntaxKind.MinusToken || node.operator === ts8.SyntaxKind.PlusToken) && ts8.isNumericLiteral(node.operand)) {
6801
+ const value = Number(node.operand.text) * (node.operator === ts8.SyntaxKind.MinusToken ? -1 : 1);
6604
6802
  return { defined: true, truthy: value !== 0 };
6605
6803
  }
6606
- if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
6804
+ if (ts8.isObjectLiteralExpression(node) || ts8.isArrayLiteralExpression(node) || ts8.isRegularExpressionLiteral(node) || ts8.isArrowFunction(node) || ts8.isFunctionExpression(node) || ts8.isClassExpression(node) || ts8.isNewExpression(node)) {
6607
6805
  return { defined: true, truthy: true };
6608
6806
  }
6609
6807
  return null;
6610
6808
  }
6611
6809
  function expectAssertion(node, sf) {
6612
- if (!ts7.isPropertyAccessExpression(node.expression)) return null;
6810
+ if (!ts8.isPropertyAccessExpression(node.expression)) return null;
6613
6811
  const matcher = node.expression.name.text;
6614
6812
  let receiver = node.expression.expression;
6615
6813
  let polarity = "";
6616
- if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6814
+ if (ts8.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6617
6815
  polarity = "not";
6618
6816
  receiver = receiver.expression;
6619
6817
  }
6620
- if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6818
+ if (!ts8.isCallExpression(receiver) || !ts8.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6621
6819
  return null;
6622
6820
  }
6623
6821
  const subject = compact(receiver.arguments[0].getText(sf));
@@ -6633,41 +6831,41 @@ function expectAssertion(node, sf) {
6633
6831
  };
6634
6832
  }
6635
6833
  function simpleCallName(node) {
6636
- return ts7.isIdentifier(node.expression) ? node.expression.text : null;
6834
+ return ts8.isIdentifier(node.expression) ? node.expression.text : null;
6637
6835
  }
6638
6836
  function callbackFor(node) {
6639
6837
  const cb = node.arguments[1];
6640
- return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
6838
+ return cb && (ts8.isArrowFunction(cb) || ts8.isFunctionExpression(cb)) ? cb : null;
6641
6839
  }
6642
6840
  function blocks(src, path) {
6643
6841
  const out2 = /* @__PURE__ */ new Map();
6644
6842
  if (langOf(path) !== "js") return out2;
6645
6843
  try {
6646
- const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
6844
+ const sf = ts8.createSourceFile(path, src, ts8.ScriptTarget.Latest, true, scriptKindForPath(path));
6647
6845
  const visit = (node, suites) => {
6648
- if (ts7.isCallExpression(node)) {
6846
+ if (ts8.isCallExpression(node)) {
6649
6847
  const call = simpleCallName(node);
6650
6848
  const title = literalTitle(node.arguments[0]);
6651
6849
  const cb = callbackFor(node);
6652
6850
  if (call && SUITE_CALLS.has(call) && title !== null && cb) {
6653
6851
  const nextSuites = [...suites, title];
6654
- ts7.forEachChild(cb, (child) => visit(child, nextSuites));
6852
+ ts8.forEachChild(cb, (child) => visit(child, nextSuites));
6655
6853
  return;
6656
6854
  }
6657
6855
  if (call && TEST_CALLS.has(call) && title !== null && cb) {
6658
6856
  const assertions = [];
6659
6857
  const collect2 = (child) => {
6660
- if (child !== cb && ts7.isCallExpression(child)) {
6858
+ if (child !== cb && ts8.isCallExpression(child)) {
6661
6859
  const nestedCall = simpleCallName(child);
6662
6860
  if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
6663
6861
  return;
6664
6862
  }
6665
6863
  }
6666
- if (ts7.isCallExpression(child)) {
6864
+ if (ts8.isCallExpression(child)) {
6667
6865
  const a = expectAssertion(child, sf);
6668
6866
  if (a) assertions.push(a);
6669
6867
  }
6670
- ts7.forEachChild(child, collect2);
6868
+ ts8.forEachChild(child, collect2);
6671
6869
  };
6672
6870
  collect2(cb);
6673
6871
  const identity2 = [...suites, title].join(" > ");
@@ -6677,7 +6875,7 @@ function blocks(src, path) {
6677
6875
  return;
6678
6876
  }
6679
6877
  }
6680
- ts7.forEachChild(node, (child) => visit(child, suites));
6878
+ ts8.forEachChild(node, (child) => visit(child, suites));
6681
6879
  };
6682
6880
  visit(sf, []);
6683
6881
  } catch {
@@ -6726,7 +6924,7 @@ function findingForBlock(path, before, after, policy) {
6726
6924
  const weakened = directionalWeakening(before.assertions, after.assertions);
6727
6925
  if (weakened) {
6728
6926
  const message = weakened.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened.before.matcher} was replaced by weaker ${weakened.after.matcher} on the same subject.`;
6729
- return makeFinding(RULE11, policy, {
6927
+ return makeFinding(RULE12, policy, {
6730
6928
  file: path,
6731
6929
  message,
6732
6930
  evidence: `${weakened.before.text} -> ${weakened.after.text}`,
@@ -6735,7 +6933,7 @@ function findingForBlock(path, before, after, policy) {
6735
6933
  }
6736
6934
  const removed = pureRemoval(before.assertions, after.assertions);
6737
6935
  if (removed) {
6738
- return makeFinding(RULE11, policy, {
6936
+ return makeFinding(RULE12, policy, {
6739
6937
  file: path,
6740
6938
  message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
6741
6939
  evidence: removed.text,
@@ -6745,7 +6943,7 @@ function findingForBlock(path, before, after, policy) {
6745
6943
  return null;
6746
6944
  }
6747
6945
  var assertionWeakening = {
6748
- id: RULE11,
6946
+ id: RULE12,
6749
6947
  surface: ["file"],
6750
6948
  certainty: "heuristic",
6751
6949
  run(changes, policy) {
@@ -6771,6 +6969,7 @@ var assertionWeakening = {
6771
6969
  var allDetectors = [
6772
6970
  noVerify,
6773
6971
  tsAnyCast,
6972
+ tsCastGrowth,
6774
6973
  lintSuppression,
6775
6974
  testSkip,
6776
6975
  coverageLowering,
@@ -6812,14 +7011,15 @@ function evaluate(changes, policy, detectors = allDetectors, view, ctx) {
6812
7011
  process.stderr.write(`tamperward: detector "${d.id}" errored: ${String(e)}
6813
7012
  `);
6814
7013
  if (view === "staged" || view === "worktree" || view === "range") {
6815
- out2.push({
7014
+ const failed = {
6816
7015
  rule: "detector-error",
6817
7016
  severity: "block",
6818
7017
  message: `Detector "${d.id}" failed to run; the verdict is incomplete.`,
6819
7018
  evidence: String(e).slice(0, 200),
6820
7019
  remediation: "A rule that cannot run is not a rule that passed. Fix the input or the detector; do not read this as clean.",
6821
7020
  signoff: { required: true, command: `tamperward allow detector-error --reason "..."` }
6822
- });
7021
+ };
7022
+ out2.push(failed);
6823
7023
  }
6824
7024
  }
6825
7025
  }
@@ -6860,7 +7060,7 @@ function normalizeVersion(v, where = POLICY_FILE) {
6860
7060
  return v;
6861
7061
  }
6862
7062
  var SEVERITIES = ["block", "warn"];
6863
- var isSeverity = (v) => SEVERITIES.includes(v);
7063
+ var isSeverity = (v) => SEVERITIES.some((s) => s === v);
6864
7064
  var isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
6865
7065
  var isMapping = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6866
7066
  var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "protected", "rules", "ignore", "signoff", "verify"]);
@@ -6874,75 +7074,105 @@ function validate(r, where) {
6874
7074
  throw new PolicyError(`${where}: ${msg}`);
6875
7075
  }
6876
7076
  const show = (v) => JSON.stringify(v) ?? String(v);
7077
+ const out2 = {};
7078
+ if (r.version !== void 0) out2.version = r.version;
6877
7079
  const unknown = Object.keys(r).filter((k) => !TOP_LEVEL_KEYS.has(k));
6878
7080
  if (unknown.length) {
6879
7081
  bad(`unknown top-level key${unknown.length === 1 ? "" : "s"} ${unknown.map((k) => JSON.stringify(k)).join(", ")} (expected one of ${[...TOP_LEVEL_KEYS].join(", ")})`);
6880
7082
  }
6881
7083
  if (r.rules !== void 0) {
6882
7084
  if (!isMapping(r.rules)) bad(`rules must be a mapping of rule name to { severity, enabled, exclude }, got ${show(r.rules)}`);
7085
+ const rules = {};
6883
7086
  for (const [name, cfg] of Object.entries(r.rules)) {
6884
7087
  if (!isMapping(cfg)) bad(`rules.${name} must be a mapping like { severity: block }, got ${show(cfg)}`);
6885
- if (cfg.severity !== void 0 && !isSeverity(cfg.severity)) {
6886
- bad(`rules.${name}.severity must be "block" or "warn", got ${show(cfg.severity)}`);
7088
+ const { severity, enabled, exclude } = cfg;
7089
+ if (severity !== void 0 && !isSeverity(severity)) {
7090
+ bad(`rules.${name}.severity must be "block" or "warn", got ${show(severity)}`);
6887
7091
  }
6888
- if (cfg.enabled !== void 0 && typeof cfg.enabled !== "boolean") {
6889
- bad(`rules.${name}.enabled must be true or false, got ${show(cfg.enabled)}`);
7092
+ if (enabled !== void 0 && typeof enabled !== "boolean") {
7093
+ bad(`rules.${name}.enabled must be true or false, got ${show(enabled)}`);
6890
7094
  }
6891
- if (cfg.exclude !== void 0 && !isStringList(cfg.exclude)) {
6892
- bad(`rules.${name}.exclude must be a list of globs, got ${show(cfg.exclude)}`);
7095
+ if (exclude !== void 0 && !isStringList(exclude)) {
7096
+ bad(`rules.${name}.exclude must be a list of globs, got ${show(exclude)}`);
6893
7097
  }
7098
+ rules[name] = {
7099
+ ...severity !== void 0 ? { severity } : {},
7100
+ ...enabled !== void 0 ? { enabled } : {},
7101
+ ...exclude !== void 0 ? { exclude } : {}
7102
+ };
6894
7103
  }
7104
+ out2.rules = rules;
7105
+ }
7106
+ if (r.ignore !== void 0) {
7107
+ if (!isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
7108
+ out2.ignore = r.ignore;
6895
7109
  }
6896
- if (r.ignore !== void 0 && !isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
6897
7110
  if (r.protected !== void 0) {
6898
7111
  if (!isMapping(r.protected)) bad(`protected must be a mapping of category to a list of globs, got ${show(r.protected)}`);
7112
+ const categories = {};
6899
7113
  for (const [cat, globs4] of Object.entries(r.protected)) {
6900
7114
  if (!isStringList(globs4)) bad(`protected.${cat} must be a list of globs, got ${show(globs4)}`);
7115
+ categories[cat] = globs4;
6901
7116
  }
7117
+ out2.protected = categories;
6902
7118
  }
6903
7119
  if (r.signoff !== void 0) {
6904
7120
  if (!isMapping(r.signoff)) bad(`signoff must be a mapping, got ${show(r.signoff)}`);
7121
+ const signoff = {};
6905
7122
  for (const key2 of ["required_for", "requiredFor"]) {
6906
7123
  const v = r.signoff[key2];
6907
- if (v !== void 0 && !(Array.isArray(v) && v.every(isSeverity))) {
7124
+ if (v === void 0) continue;
7125
+ if (!(Array.isArray(v) && v.every(isSeverity))) {
6908
7126
  bad(`signoff.${key2} must be a list of "block" / "warn", got ${show(v)}`);
6909
7127
  }
7128
+ signoff[key2] = v;
6910
7129
  }
6911
7130
  const ledger = r.signoff.ledger;
6912
7131
  if (ledger !== void 0 && typeof ledger !== "string") bad(`signoff.ledger must be a path, got ${show(ledger)}`);
6913
- if (typeof ledger === "string" && !ledgerInsideRepo(ledger)) {
6914
- bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
7132
+ if (typeof ledger === "string") {
7133
+ if (!ledgerInsideRepo(ledger)) bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
7134
+ signoff.ledger = ledger;
6915
7135
  }
7136
+ out2.signoff = signoff;
6916
7137
  }
6917
7138
  if (r.verify !== void 0) {
6918
7139
  if (!isMapping(r.verify)) bad(`verify must be a mapping like { command: "npm test" }, got ${show(r.verify)}`);
6919
7140
  const v = r.verify;
6920
- if (v.command !== void 0 && typeof v.command !== "string") bad(`verify.command must be a string, got ${show(v.command)}`);
6921
- if (v.budget !== void 0 && !(typeof v.budget === "number" && Number.isFinite(v.budget) && v.budget > 0)) {
6922
- bad(`verify.budget must be a positive number of seconds, got ${show(v.budget)}`);
7141
+ const { command, budget, inputs, backend, image } = v;
7142
+ if (command !== void 0 && typeof command !== "string") bad(`verify.command must be a string, got ${show(command)}`);
7143
+ if (budget !== void 0 && !(typeof budget === "number" && Number.isFinite(budget) && budget > 0)) {
7144
+ bad(`verify.budget must be a positive number of seconds, got ${show(budget)}`);
6923
7145
  }
6924
- if (v.inputs !== void 0 && !isStringList(v.inputs)) bad(`verify.inputs must be a list of globs, got ${show(v.inputs)}`);
6925
- if (v.backend !== void 0 && v.backend !== "local" && v.backend !== "container") {
6926
- bad(`verify.backend must be "local" or "container", got ${show(v.backend)}`);
7146
+ if (inputs !== void 0 && !isStringList(inputs)) bad(`verify.inputs must be a list of globs, got ${show(inputs)}`);
7147
+ if (backend !== void 0 && backend !== "local" && backend !== "container") {
7148
+ bad(`verify.backend must be "local" or "container", got ${show(backend)}`);
6927
7149
  }
6928
- if (v.image !== void 0 && typeof v.image !== "string") {
6929
- bad(`verify.image must be an immutable container image reference, got ${show(v.image)}`);
7150
+ if (image !== void 0 && typeof image !== "string") {
7151
+ bad(`verify.image must be an immutable container image reference, got ${show(image)}`);
6930
7152
  }
6931
- const backend = v.backend ?? "local";
6932
- if (backend === "container") {
6933
- if (typeof v.image !== "string") bad('verify.image is required when verify.backend is "container"');
6934
- if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(v.image)) {
6935
- bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(v.image)}`);
7153
+ if ((backend ?? "local") === "container") {
7154
+ if (typeof image !== "string") bad('verify.image is required when verify.backend is "container"');
7155
+ if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(image)) {
7156
+ bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(image)}`);
6936
7157
  }
6937
- } else if (v.image !== void 0) {
7158
+ } else if (image !== void 0) {
6938
7159
  bad('verify.image is only valid when verify.backend is "container"');
6939
7160
  }
7161
+ out2.verify = {
7162
+ ...command !== void 0 ? { command } : {},
7163
+ ...budget !== void 0 ? { budget } : {},
7164
+ ...inputs !== void 0 ? { inputs } : {},
7165
+ ...backend !== void 0 ? { backend } : {},
7166
+ ...image !== void 0 ? { image } : {}
7167
+ };
6940
7168
  }
7169
+ return out2;
6941
7170
  }
6942
7171
  function parsePolicy(raw, where = POLICY_FILE) {
6943
- const r = raw ?? {};
6944
- const version = normalizeVersion(r.version, where);
6945
- validate(r, where);
7172
+ const doc = raw ?? {};
7173
+ if (!isMapping(doc)) throw new PolicyError(`${where} is not a policy mapping`);
7174
+ const version = normalizeVersion(doc.version, where);
7175
+ const r = validate(doc, where);
6946
7176
  const base = defaultPolicy(version);
6947
7177
  const globs4 = (list2) => list2?.map(normalizeGlob);
6948
7178
  const userProtected = r.protected ? Object.fromEntries(Object.entries(r.protected).map(([cat, list2]) => [cat, globs4(list2) ?? []])) : void 0;
@@ -6981,10 +7211,7 @@ function parseOrThrow(src, where) {
6981
7211
  try {
6982
7212
  raw = parse2(src);
6983
7213
  } catch (e) {
6984
- throw new PolicyError(`${where} is not valid YAML: ${e.message}`);
6985
- }
6986
- if (raw !== null && raw !== void 0 && (typeof raw !== "object" || Array.isArray(raw))) {
6987
- throw new PolicyError(`${where} is not a policy mapping`);
7214
+ throw new PolicyError(`${where} is not valid YAML: ${errorMessage(e)}`);
6988
7215
  }
6989
7216
  return parsePolicy(raw, where);
6990
7217
  }
@@ -7004,6 +7231,14 @@ import { createHash as createHash2 } from "node:crypto";
7004
7231
  import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "node:fs";
7005
7232
  import { dirname as dirname2, join as join8 } from "node:path";
7006
7233
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
7234
+ function ledgerEntryFrom(value) {
7235
+ if (!isRecord(value)) return null;
7236
+ const { rule, file, fingerprint: fingerprint2, reason, recordedAt, expiresAt } = value;
7237
+ if (typeof rule !== "string" || typeof fingerprint2 !== "string" || typeof reason !== "string") return null;
7238
+ if (typeof recordedAt !== "number" || typeof expiresAt !== "number") return null;
7239
+ if (file !== void 0 && typeof file !== "string") return null;
7240
+ return { rule, ...file !== void 0 ? { file } : {}, fingerprint: fingerprint2, reason, recordedAt, expiresAt };
7241
+ }
7007
7242
  function fingerprint(rule, file, evidence) {
7008
7243
  return createHash2("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
7009
7244
  }
@@ -7022,8 +7257,8 @@ function readLedger(cwd, policy) {
7022
7257
  for (const line of readFileSync6(p, "utf8").split("\n")) {
7023
7258
  if (!line.trim()) continue;
7024
7259
  try {
7025
- const e = JSON.parse(line);
7026
- if (e && typeof e.fingerprint === "string") out2.push(e);
7260
+ const e = ledgerEntryFrom(JSON.parse(line));
7261
+ if (e) out2.push(e);
7027
7262
  } catch {
7028
7263
  }
7029
7264
  }
@@ -7059,9 +7294,10 @@ function oobToken(want, oob, head) {
7059
7294
  }
7060
7295
  const [rule, sha2] = [t.slice(0, at), t.slice(at + 1)];
7061
7296
  if (rule !== want) continue;
7062
- const normalizedHead = head?.trim().toLowerCase();
7297
+ if (!head) return t;
7298
+ const normalizedHead = head.trim().toLowerCase();
7063
7299
  const normalizedSha = sha2.trim().toLowerCase();
7064
- if (!head || /^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
7300
+ if (/^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
7065
7301
  }
7066
7302
  return null;
7067
7303
  }
@@ -7293,7 +7529,7 @@ function isGitHubActions(env = process.env) {
7293
7529
  // src/cli/report.ts
7294
7530
  var FORMATS = ["auto", "text", "json", "github"];
7295
7531
  function isFormat(s) {
7296
- return FORMATS.includes(s);
7532
+ return FORMATS.some((f) => f === s);
7297
7533
  }
7298
7534
  function resolveFormat(input, env = process.env) {
7299
7535
  if (input.json) return "json";
@@ -7471,6 +7707,17 @@ import { createHash as createHash3 } from "node:crypto";
7471
7707
  import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
7472
7708
  import { isAbsolute as isAbsolute7, join as join10 } from "node:path";
7473
7709
  import { execFileSync as execFileSync5 } from "node:child_process";
7710
+ function ptreeFrom(value) {
7711
+ if (!isRecord(value)) return null;
7712
+ const out2 = {};
7713
+ for (const [path, entry] of Object.entries(value)) {
7714
+ if (!isRecord(entry)) return null;
7715
+ const { hash, mode, size, mtimeMs } = entry;
7716
+ if (typeof hash !== "string" || typeof mode !== "number" || typeof size !== "number" || typeof mtimeMs !== "number") return null;
7717
+ out2[path] = { hash, mode, size, mtimeMs };
7718
+ }
7719
+ return out2;
7720
+ }
7474
7721
  var UNSAFE2 = /[^A-Za-z0-9._-]/g;
7475
7722
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
7476
7723
  function gitDir2(cwd) {
@@ -7562,7 +7809,7 @@ function driftBetween(expected, current) {
7562
7809
  function loadTree(p) {
7563
7810
  if (!p || !existsSync7(p)) return null;
7564
7811
  try {
7565
- return JSON.parse(readFileSync8(p, "utf8"));
7812
+ return ptreeFrom(JSON.parse(readFileSync8(p, "utf8")));
7566
7813
  } catch {
7567
7814
  return null;
7568
7815
  }
@@ -7622,8 +7869,25 @@ function watcherHealthPath(log) {
7622
7869
  function readWatcherHealth(log) {
7623
7870
  try {
7624
7871
  const value = JSON.parse(readFileSync9(watcherHealthPath(log), "utf8"));
7625
- if (value.version !== 1 || typeof value.pid !== "number" || typeof value.started_at !== "string" || !["healthy", "degraded", "stopped"].includes(String(value.state))) return null;
7626
- return value;
7872
+ if (!isRecord(value)) return null;
7873
+ const { state, backend, pid, started_at } = value;
7874
+ if (value.version !== 1 || typeof pid !== "number" || typeof started_at !== "string") return null;
7875
+ if (state !== "healthy" && state !== "degraded" && state !== "stopped") return null;
7876
+ return {
7877
+ version: 1,
7878
+ state,
7879
+ backend: backend === "initializing" || backend === "recursive" ? backend : "fallback",
7880
+ pid,
7881
+ started_at,
7882
+ stopped_at: nullableString(value.stopped_at) ?? null,
7883
+ watched_dirs: finiteNumber(value.watched_dirs) ?? 0,
7884
+ last_append_at: nullableString(value.last_append_at) ?? null,
7885
+ event_count: finiteNumber(value.event_count) ?? 0,
7886
+ dropped_events: finiteNumber(value.dropped_events) ?? 0,
7887
+ error_count: finiteNumber(value.error_count) ?? 0,
7888
+ last_error: nullableString(value.last_error) ?? null,
7889
+ log: stringOrUndefined(value.log) ?? log
7890
+ };
7627
7891
  } catch {
7628
7892
  return null;
7629
7893
  }
@@ -7634,7 +7898,7 @@ function pidAlive(pid) {
7634
7898
  process.kill(pid, 0);
7635
7899
  return true;
7636
7900
  } catch (e) {
7637
- return e.code === "EPERM";
7901
+ return errnoCode(e) === "EPERM";
7638
7902
  }
7639
7903
  }
7640
7904
  function watcherTelemetry(log) {
@@ -7838,7 +8102,23 @@ function runWatch(args) {
7838
8102
 
7839
8103
  // src/detectors/fs-events.ts
7840
8104
  import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7841
- var RULE12 = "transient-protected-mutation";
8105
+ function fsEventFrom(line) {
8106
+ let value;
8107
+ try {
8108
+ value = JSON.parse(line);
8109
+ } catch {
8110
+ return null;
8111
+ }
8112
+ if (!isRecord(value)) return null;
8113
+ const { ts: ts9, path, kind } = value;
8114
+ if (typeof ts9 !== "string" || typeof path !== "string" || kind !== "change" && kind !== "rename") return null;
8115
+ const mode = nullableNumber(value.mode);
8116
+ const size = nullableNumber(value.size);
8117
+ const hash = nullableString(value.hash);
8118
+ if (mode === void 0 || size === void 0 || hash === void 0) return null;
8119
+ return { ts: ts9, path, kind, mode, size, hash };
8120
+ }
8121
+ var RULE13 = "transient-protected-mutation";
7842
8122
  var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
7843
8123
  var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
7844
8124
  var DEFAULT_EVENT_READ_IO = {
@@ -7909,11 +8189,9 @@ function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_E
7909
8189
  const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
7910
8190
  for (const line of complete.split("\n")) {
7911
8191
  if (!line.trim()) continue;
7912
- try {
7913
- events.push(JSON.parse(line));
7914
- } catch {
7915
- malformedLines++;
7916
- }
8192
+ const event = fsEventFrom(line);
8193
+ if (event) events.push(event);
8194
+ else malformedLines++;
7917
8195
  }
7918
8196
  const newOffset = start + lastNewline + 1;
7919
8197
  return {
@@ -8033,7 +8311,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
8033
8311
  };
8034
8312
  }
8035
8313
  function transientFindings(events, persistentPaths, policy, finalHash) {
8036
- if (!isEnabled(RULE12, policy)) return [];
8314
+ if (!isEnabled(RULE13, policy)) return [];
8037
8315
  const byPath = /* @__PURE__ */ new Map();
8038
8316
  for (const e of events) {
8039
8317
  if (!isProtected(e.path, policy)) continue;
@@ -8053,7 +8331,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
8053
8331
  const modeChurn = modes.size > 1;
8054
8332
  if (!contentChurn && !modeChurn) continue;
8055
8333
  const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
8056
- const f = makeFinding(RULE12, policy, {
8334
+ const f = makeFinding(RULE13, policy, {
8057
8335
  file: path,
8058
8336
  message: `Transient protected mutation: ${what}, leaving no net diff.`,
8059
8337
  evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
@@ -8087,10 +8365,16 @@ function parseInput(raw) {
8087
8365
  } catch (e) {
8088
8366
  throw new HookInputError(`the hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
8089
8367
  }
8090
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8368
+ if (!isRecord(parsed)) {
8091
8369
  throw new HookInputError(`the hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`);
8092
8370
  }
8093
- return parsed;
8371
+ return {
8372
+ ...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
8373
+ ...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
8374
+ ...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
8375
+ ...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {},
8376
+ ...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}
8377
+ };
8094
8378
  }
8095
8379
  function recordDenylog(blocks2) {
8096
8380
  const log = process.env.TAMPERWARD_DENYLOG;
@@ -8733,6 +9017,9 @@ function missingTools(matcher) {
8733
9017
  if (have === null) return [];
8734
9018
  return PRE_MATCHER.split("|").filter((t) => !have.has(t));
8735
9019
  }
9020
+ function isHooksShape(hooks) {
9021
+ return hooksShapeError(hooks) === null;
9022
+ }
8736
9023
  function hooksShapeError(hooks) {
8737
9024
  if (hooks === void 0 || hooks === null) return null;
8738
9025
  if (!isPlainObject(hooks)) return "hooks is not an object";
@@ -8755,7 +9042,7 @@ function verifierCandidates(cwd) {
8755
9042
  };
8756
9043
  try {
8757
9044
  const pkg = JSON.parse(readFileSync11(join12(cwd, "package.json"), "utf8"));
8758
- const test = pkg.scripts?.test;
9045
+ const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
8759
9046
  if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
8760
9047
  } catch {
8761
9048
  }
@@ -8854,14 +9141,14 @@ function planClaudeHooks(cwd) {
8854
9141
  detail: "exists but is not valid JSON \u2014 fix it, then re-run init (refusing to overwrite)"
8855
9142
  };
8856
9143
  }
8857
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
9144
+ if (!isRecord(parsed)) {
8858
9145
  return { item: "agent", path: rel, status: "error", detail: "exists but is not a JSON object \u2014 refusing to overwrite" };
8859
9146
  }
8860
- settings = parsed;
8861
- }
8862
- const shapeError = hooksShapeError(settings.hooks);
8863
- if (shapeError) {
8864
- return { item: "agent", path: rel, status: "error", detail: `${shapeError} \u2014 fix it, then re-run init (refusing to overwrite)` };
9147
+ const shapeError = hooksShapeError(parsed.hooks);
9148
+ if (shapeError || !isHooksShape(parsed.hooks)) {
9149
+ return { item: "agent", path: rel, status: "error", detail: `${shapeError ?? "hooks is not the shape Claude Code reads"} \u2014 fix it, then re-run init (refusing to overwrite)` };
9150
+ }
9151
+ settings = { ...parsed, hooks: parsed.hooks ?? void 0 };
8865
9152
  }
8866
9153
  const hooks = settings.hooks ??= {};
8867
9154
  const entriesWith = (arr, needle) => (arr ?? []).filter((m) => (m.hooks ?? []).some((h) => needle.test(String(h.command ?? ""))));
@@ -9085,7 +9372,7 @@ function planCodeowners(cwd) {
9085
9372
  const path = join12(cwd, existingRel ?? rel);
9086
9373
  const existing = existingRel ? readFileSync11(path, "utf8") : null;
9087
9374
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
9088
- if (missing.length === 0) {
9375
+ if (existingRel !== void 0 && missing.length === 0) {
9089
9376
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
9090
9377
  }
9091
9378
  const inferred = inferOwner(cwd);
@@ -9711,14 +9998,15 @@ function decodeUtf8Tail(tail) {
9711
9998
  while (start < tail.length && (tail[start] & 192) === 128) start++;
9712
9999
  return tail.subarray(start).toString("utf8");
9713
10000
  }
9714
- function streamFromRaw(raw) {
10001
+ function streamFromRaw(value) {
10002
+ const raw = isRecord(value) ? value : {};
9715
10003
  let tail = Buffer.alloc(0);
9716
10004
  try {
9717
- tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
10005
+ tail = Buffer.from(typeof raw.tail_b64 === "string" ? raw.tail_b64 : "", "base64");
9718
10006
  } catch {
9719
10007
  tail = Buffer.alloc(0);
9720
10008
  }
9721
- const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
10009
+ const captured = typeof raw.captured_bytes === "number" && Number.isFinite(raw.captured_bytes) ? Math.max(0, raw.captured_bytes) : tail.length;
9722
10010
  return {
9723
10011
  captured_bytes: captured,
9724
10012
  retained_bytes: tail.length,
@@ -9729,7 +10017,7 @@ function streamFromRaw(raw) {
9729
10017
  function scrubDiagnosticText(input) {
9730
10018
  let out2 = "";
9731
10019
  for (const ch of input) {
9732
- const cp = ch.codePointAt(0);
10020
+ const cp = ch.codePointAt(0) ?? 0;
9733
10021
  if (ch === "\n" || ch === " ") {
9734
10022
  out2 += ch;
9735
10023
  } else if (cp === 13) {
@@ -9932,7 +10220,7 @@ function parseCapturedSupervisorResult(stdoutText) {
9932
10220
  } catch {
9933
10221
  return null;
9934
10222
  }
9935
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
10223
+ if (!isRecord(raw)) return null;
9936
10224
  const diagnostics = {
9937
10225
  stdout: streamFromRaw(raw.stdout),
9938
10226
  stderr: streamFromRaw(raw.stderr)
@@ -9983,7 +10271,7 @@ function runCapturedProcessSync(executable, args, opts) {
9983
10271
  const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
9984
10272
  const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
9985
10273
  if (!parsed) {
9986
- const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
10274
+ const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
9987
10275
  return {
9988
10276
  exit: null,
9989
10277
  signal: supervisor.signal ? String(supervisor.signal) : null,
@@ -10078,16 +10366,16 @@ function dockerAvailable(enginePath, host) {
10078
10366
  }
10079
10367
  }
10080
10368
  function declaredImageVolumePaths(metadata) {
10081
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
10369
+ if (!isRecord(metadata)) {
10082
10370
  throw new Error("image metadata must be a Docker image-inspect object");
10083
10371
  }
10084
10372
  const config = metadata.Config;
10085
- if (!config || typeof config !== "object" || Array.isArray(config)) {
10373
+ if (!isRecord(config)) {
10086
10374
  throw new Error("image metadata Config must be an object");
10087
10375
  }
10088
10376
  const volumes = config.Volumes;
10089
10377
  if (volumes === void 0 || volumes === null) return [];
10090
- if (typeof volumes !== "object" || Array.isArray(volumes)) {
10378
+ if (!isRecord(volumes)) {
10091
10379
  throw new Error("image metadata Config.Volumes must be a mapping or null");
10092
10380
  }
10093
10381
  return Object.keys(volumes).sort();
@@ -10591,8 +10879,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10591
10879
  const pending = linkParts(target);
10592
10880
  const seen = /* @__PURE__ */ new Set();
10593
10881
  let hops = 0;
10594
- while (pending.length) {
10595
- const part = pending.shift();
10882
+ for (let part = pending.shift(); part !== void 0; part = pending.shift()) {
10596
10883
  if (part === "..") {
10597
10884
  if (!stack.length) throw linkEscape(label, target);
10598
10885
  stack.pop();
@@ -10604,7 +10891,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10604
10891
  stack = [];
10605
10892
  continue;
10606
10893
  }
10607
- const base = domain === "tree" ? root : dependencyRoot;
10894
+ const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
10608
10895
  const current = join16(base, ...stack);
10609
10896
  let st;
10610
10897
  try {
@@ -11623,7 +11910,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11623
11910
  }
11624
11911
  }
11625
11912
  );
11626
- const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
11913
+ const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
11627
11914
  const completedNormally = !supervisorTimedOut && !supervisor.error && supervisor.status === 0 && supervisor.signal == null;
11628
11915
  if (!completedNormally) {
11629
11916
  return {
@@ -11674,7 +11961,7 @@ function pidAlive2(pid) {
11674
11961
  process.kill(pid, 0);
11675
11962
  return true;
11676
11963
  } catch (e) {
11677
- return e.code === "EPERM";
11964
+ return errnoCode(e) === "EPERM";
11678
11965
  }
11679
11966
  }
11680
11967
  function supervisedObserverLog(cwd) {
@@ -11717,7 +12004,7 @@ function stopObserverProcess(observer) {
11717
12004
  waitMs(100);
11718
12005
  const beforeStop = watcherTelemetry(observer.log);
11719
12006
  const pid = observer.pid;
11720
- if (pidAlive2(pid)) {
12007
+ if (pid !== null && pidAlive2(pid)) {
11721
12008
  try {
11722
12009
  process.kill(pid, "SIGTERM");
11723
12010
  } catch {
@@ -12191,7 +12478,7 @@ function policyFor(opts, cwd) {
12191
12478
  return atBase ?? defaultPolicy();
12192
12479
  }
12193
12480
  function asMapping(value) {
12194
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
12481
+ return isRecord(value) ? value : null;
12195
12482
  }
12196
12483
  function wiringState(status, authority) {
12197
12484
  if (status === "ok") return "OK";
@@ -12512,8 +12799,8 @@ function githubApi(cwd, endpoint) {
12512
12799
  );
12513
12800
  return JSON.parse(stdout);
12514
12801
  } catch (e) {
12515
- const x = e;
12516
- const detail = x.stderr ? String(x.stderr).replace(/\s+/g, " ").trim() : x.message;
12802
+ const failure = execFailure(e);
12803
+ const detail = failure.stderr ? failure.stderr.replace(/\s+/g, " ").trim() : failure.message;
12517
12804
  throw new Error(
12518
12805
  "GitHub API " + endpoint + " failed: " + (detail || "unknown error")
12519
12806
  );
@@ -12824,7 +13111,7 @@ function summarizeTraceRuns(opts) {
12824
13111
  for (const item of run) {
12825
13112
  const rel = normalRepoPath(opts.root, item.path);
12826
13113
  const isTracked = rel !== null && opts.tracked.has(rel);
12827
- const keyPath = isTracked ? rel : rel === null ? resolve9(opts.root, item.path) : null;
13114
+ const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve9(opts.root, item.path);
12828
13115
  if (keyPath === null) continue;
12829
13116
  const target = isTracked ? repo : external;
12830
13117
  const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
@@ -12909,7 +13196,7 @@ function materializeBase(base, cwd, dest) {
12909
13196
  }
12910
13197
  function traceFiles(prefix) {
12911
13198
  const dir = resolve9(prefix, "..");
12912
- const base = prefix.split(sep3).at(-1);
13199
+ const base = prefix.split(sep3).at(-1) ?? prefix;
12913
13200
  return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join19(dir, name)).sort();
12914
13201
  }
12915
13202
  function rewriteTraceRoot(accesses, actualRoot) {