tamperward 2.19.0 → 2.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -27
  2. package/dist/cli/index.js +679 -373
  3. package/package.json +1 -1
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,39 +1233,188 @@ 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*>|@ts-(?:ignore|expect-error|nocheck)\b/;
1225
1398
  var BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
1399
+ function lineHasDoubleCast(line) {
1400
+ let sf;
1401
+ try {
1402
+ sf = ts2.createSourceFile("line.ts", line, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
1403
+ } catch {
1404
+ return false;
1405
+ }
1406
+ let found = false;
1407
+ const visit = (n) => {
1408
+ if (found) return;
1409
+ if ((ts2.isAsExpression(n) || ts2.isTypeAssertionExpression(n)) && isDoubleCast(n)) {
1410
+ found = true;
1411
+ return;
1412
+ }
1413
+ ts2.forEachChild(n, visit);
1414
+ };
1415
+ visit(sf);
1416
+ return found;
1417
+ }
1226
1418
  var BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
1227
1419
  var WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
1228
1420
  var tsAnyCast = {
@@ -1240,7 +1432,7 @@ var tsAnyCast = {
1240
1432
  const b = countAny(before);
1241
1433
  const dCast = a.cast - b.cast;
1242
1434
  const dBroad = a.broad - b.broad;
1243
- const dDouble = countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST);
1435
+ const dDouble = Math.max(a.double - b.double, countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST));
1244
1436
  const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
1245
1437
  const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
1246
1438
  const blockReasons = [];
@@ -1274,7 +1466,7 @@ var tsAnyCast = {
1274
1466
  }
1275
1467
  const jsdocCast = JS_FILE.test(c.path) ? new RegExp(JSDOC_ANY_CAST.source) : null;
1276
1468
  for (const l of addedLines(c)) {
1277
- if (NARROW_LINE.test(l.content) || jsdocCast && jsdocCast.test(l.content)) {
1469
+ if (NARROW_LINE.test(l.content) || lineHasDoubleCast(l.content) || jsdocCast && jsdocCast.test(l.content)) {
1278
1470
  out2.push(
1279
1471
  makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
1280
1472
  file: c.path,
@@ -1304,7 +1496,7 @@ var tsAnyCast = {
1304
1496
  };
1305
1497
 
1306
1498
  // src/detectors/lint-suppression.ts
1307
- var RULE2 = "lint-suppression";
1499
+ var RULE3 = "lint-suppression";
1308
1500
  var PATTERNS = {
1309
1501
  js: [
1310
1502
  { re: /eslint-disable(?:-next-line|-line)?\b/, why: "eslint-disable suppresses lint rules" },
@@ -1352,7 +1544,7 @@ var PATTERNS = {
1352
1544
  ]
1353
1545
  };
1354
1546
  var lintSuppression = {
1355
- id: RULE2,
1547
+ id: RULE3,
1356
1548
  surface: ["file"],
1357
1549
  certainty: "mechanical",
1358
1550
  run(changes, policy) {
@@ -1368,7 +1560,7 @@ var lintSuppression = {
1368
1560
  const m = p.re.exec(l.content);
1369
1561
  if (!m || insideStringLiteral(l.content, m.index, lang)) continue;
1370
1562
  out2.push(
1371
- makeFinding(RULE2, policy, {
1563
+ makeFinding(RULE3, policy, {
1372
1564
  file: c.path,
1373
1565
  line: l.newLine ?? void 0,
1374
1566
  message: `Lint suppression added: ${p.why}.`,
@@ -1385,8 +1577,8 @@ var lintSuppression = {
1385
1577
  };
1386
1578
 
1387
1579
  // src/detectors/test-skip.ts
1388
- import ts2 from "typescript";
1389
- var RULE3 = "test-skip";
1580
+ import ts3 from "typescript";
1581
+ var RULE4 = "test-skip";
1390
1582
  var acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
1391
1583
  var JS_RUNNER = "\\b(?:it|test|describe|suite)";
1392
1584
  var JS_MOD = "concurrent|sequential|shuffle";
@@ -1489,11 +1681,11 @@ var JS_TEST_MODULES = /* @__PURE__ */ new Set([
1489
1681
  "bun:test",
1490
1682
  "@playwright/test"
1491
1683
  ]);
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;
1684
+ function scriptKind2(path) {
1685
+ if (/\.tsx$/i.test(path)) return ts3.ScriptKind.TSX;
1686
+ if (/\.jsx$/i.test(path)) return ts3.ScriptKind.JSX;
1687
+ if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts3.ScriptKind.JS;
1688
+ return ts3.ScriptKind.TS;
1497
1689
  }
1498
1690
  function addedLineNumbers(c) {
1499
1691
  const out2 = /* @__PURE__ */ new Set();
@@ -1505,32 +1697,32 @@ function addedLineNumbers(c) {
1505
1697
  return out2;
1506
1698
  }
1507
1699
  function moduleName(expr) {
1508
- return ts2.isStringLiteralLike(expr) ? expr.text : null;
1700
+ return ts3.isStringLiteralLike(expr) ? expr.text : null;
1509
1701
  }
1510
1702
  function requiredModule(expr) {
1511
- if (!ts2.isCallExpression(expr) || !ts2.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1703
+ if (!ts3.isCallExpression(expr) || !ts3.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1512
1704
  return moduleName(expr.arguments[0]);
1513
1705
  }
1514
1706
  function astContext(path, source) {
1515
1707
  const rootName = path.startsWith("/") ? path : "/tamperward/" + path;
1516
- const sf = ts2.createSourceFile(
1708
+ const sf = ts3.createSourceFile(
1517
1709
  rootName,
1518
1710
  source,
1519
- ts2.ScriptTarget.Latest,
1711
+ ts3.ScriptTarget.Latest,
1520
1712
  true,
1521
- scriptKind(path)
1713
+ scriptKind2(path)
1522
1714
  );
1523
- const diagnostics = sf.parseDiagnostics ?? [];
1715
+ const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
1524
1716
  if (diagnostics.length > 0) return null;
1525
1717
  const options = {
1526
1718
  allowJs: true,
1527
1719
  checkJs: false,
1528
1720
  noLib: true,
1529
1721
  noResolve: true,
1530
- target: ts2.ScriptTarget.Latest,
1531
- module: ts2.ModuleKind.ESNext
1722
+ target: ts3.ScriptTarget.Latest,
1723
+ module: ts3.ModuleKind.ESNext
1532
1724
  };
1533
- const host = ts2.createCompilerHost(options, true);
1725
+ const host = ts3.createCompilerHost(options, true);
1534
1726
  host.getSourceFile = (fileName) => fileName === rootName ? sf : void 0;
1535
1727
  host.fileExists = (fileName) => fileName === rootName;
1536
1728
  host.readFile = (fileName) => fileName === rootName ? source : void 0;
@@ -1541,7 +1733,7 @@ function astContext(path, source) {
1541
1733
  host.getCanonicalFileName = (fileName) => fileName;
1542
1734
  host.useCaseSensitiveFileNames = () => true;
1543
1735
  host.getNewLine = () => "\n";
1544
- const program = ts2.createProgram([rootName], options, host);
1736
+ const program = ts3.createProgram([rootName], options, host);
1545
1737
  return { sf, checker: program.getTypeChecker() };
1546
1738
  }
1547
1739
  function symbolAt(ctx, node) {
@@ -1555,13 +1747,13 @@ function importAliases(ctx) {
1555
1747
  if (sym) out2.set(sym, { causes });
1556
1748
  };
1557
1749
  for (const stmt of ctx.sf.statements) {
1558
- if (ts2.isImportDeclaration(stmt) && ts2.isStringLiteralLike(stmt.moduleSpecifier)) {
1750
+ if (ts3.isImportDeclaration(stmt) && ts3.isStringLiteralLike(stmt.moduleSpecifier)) {
1559
1751
  if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
1560
1752
  const clause = stmt.importClause;
1561
1753
  if (!clause) continue;
1562
1754
  if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
1563
1755
  const bindings = clause.namedBindings;
1564
- if (bindings && ts2.isNamedImports(bindings)) {
1756
+ if (bindings && ts3.isNamedImports(bindings)) {
1565
1757
  for (const el of bindings.elements) {
1566
1758
  const imported = el.propertyName?.text ?? el.name.text;
1567
1759
  if (JS_RUNNERS.has(imported)) add(el.name);
@@ -1569,17 +1761,17 @@ function importAliases(ctx) {
1569
1761
  }
1570
1762
  continue;
1571
1763
  }
1572
- if (!ts2.isVariableStatement(stmt)) continue;
1764
+ if (!ts3.isVariableStatement(stmt)) continue;
1573
1765
  for (const decl of stmt.declarationList.declarations) {
1574
- if (ts2.isObjectBindingPattern(decl.name) && decl.initializer) {
1766
+ if (ts3.isObjectBindingPattern(decl.name) && decl.initializer) {
1575
1767
  const mod = requiredModule(decl.initializer);
1576
1768
  if (!mod || !JS_TEST_MODULES.has(mod)) continue;
1577
1769
  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;
1770
+ if (!ts3.isIdentifier(el.name)) continue;
1771
+ const imported = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1580
1772
  if (JS_RUNNERS.has(imported)) add(el.name);
1581
1773
  }
1582
- } else if (ts2.isIdentifier(decl.name) && decl.initializer && ts2.isIdentifier(decl.initializer)) {
1774
+ } else if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isIdentifier(decl.initializer)) {
1583
1775
  simpleAliases.push({ local: decl.name, source: decl.initializer });
1584
1776
  }
1585
1777
  }
@@ -1605,11 +1797,11 @@ function importAliases(ctx) {
1605
1797
  function topLevelStaticStrings(ctx) {
1606
1798
  const pending = /* @__PURE__ */ new Map();
1607
1799
  for (const stmt of ctx.sf.statements) {
1608
- if (!ts2.isVariableStatement(stmt)) continue;
1609
- const isConst = (stmt.declarationList.flags & ts2.NodeFlags.Const) !== 0;
1800
+ if (!ts3.isVariableStatement(stmt)) continue;
1801
+ const isConst = (stmt.declarationList.flags & ts3.NodeFlags.Const) !== 0;
1610
1802
  if (!isConst) continue;
1611
1803
  for (const decl of stmt.declarationList.declarations) {
1612
- if (!ts2.isIdentifier(decl.name) || !decl.initializer) continue;
1804
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
1613
1805
  const sym = symbolAt(ctx, decl.name);
1614
1806
  if (sym) pending.set(sym, { decl: decl.name, expr: decl.initializer });
1615
1807
  }
@@ -1617,15 +1809,15 @@ function topLevelStaticStrings(ctx) {
1617
1809
  const resolved = /* @__PURE__ */ new Map();
1618
1810
  const resolving = /* @__PURE__ */ new Set();
1619
1811
  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) {
1812
+ if (ts3.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
1813
+ if (ts3.isParenthesizedExpression(expr)) return valueOf(expr.expression);
1814
+ if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
1623
1815
  const left = valueOf(expr.left);
1624
1816
  const right = valueOf(expr.right);
1625
1817
  if (!left || !right) return null;
1626
1818
  return { value: left.value + right.value, causes: [...left.causes, ...right.causes] };
1627
1819
  }
1628
- if (ts2.isIdentifier(expr)) {
1820
+ if (ts3.isIdentifier(expr)) {
1629
1821
  const sym = symbolAt(ctx, expr);
1630
1822
  if (!sym) return null;
1631
1823
  const cached = resolved.get(sym);
@@ -1652,15 +1844,15 @@ function topLevelStaticStrings(ctx) {
1652
1844
  return resolved;
1653
1845
  }
1654
1846
  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) {
1847
+ if (ts3.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
1848
+ if (ts3.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
1849
+ if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
1658
1850
  const left = staticPropertyName(expr.left, ctx, strings);
1659
1851
  const right = staticPropertyName(expr.right, ctx, strings);
1660
1852
  if (!left || !right) return null;
1661
1853
  return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
1662
1854
  }
1663
- if (ts2.isIdentifier(expr)) {
1855
+ if (ts3.isIdentifier(expr)) {
1664
1856
  const sym = symbolAt(ctx, expr);
1665
1857
  if (!sym) return null;
1666
1858
  const value = strings.get(sym);
@@ -1673,8 +1865,8 @@ function runnerChain(expr, ctx, runners, strings) {
1673
1865
  const causes = [];
1674
1866
  let cur = expr;
1675
1867
  let terminalNode = expr;
1676
- while (ts2.isPropertyAccessExpression(cur) || ts2.isElementAccessExpression(cur)) {
1677
- if (ts2.isPropertyAccessExpression(cur)) {
1868
+ while (ts3.isPropertyAccessExpression(cur) || ts3.isElementAccessExpression(cur)) {
1869
+ if (ts3.isPropertyAccessExpression(cur)) {
1678
1870
  props.unshift(cur.name.text);
1679
1871
  if (props.length === 1) terminalNode = cur.name;
1680
1872
  cur = cur.expression;
@@ -1688,7 +1880,7 @@ function runnerChain(expr, ctx, runners, strings) {
1688
1880
  if (props.length === 1) terminalNode = cur.argumentExpression;
1689
1881
  cur = cur.expression;
1690
1882
  }
1691
- if (!ts2.isIdentifier(cur)) return null;
1883
+ if (!ts3.isIdentifier(cur)) return null;
1692
1884
  const sym = symbolAt(ctx, cur);
1693
1885
  if (sym) {
1694
1886
  const binding = runners.get(sym);
@@ -1700,17 +1892,17 @@ function runnerChain(expr, ctx, runners, strings) {
1700
1892
  return { root: cur.text, props, terminalNode, causes };
1701
1893
  }
1702
1894
  function optionDisables(value) {
1703
- return !(value.kind === ts2.SyntaxKind.FalseKeyword || ts2.isNumericLiteral(value) && Number(value.text) === 0);
1895
+ return !(value.kind === ts3.SyntaxKind.FalseKeyword || ts3.isNumericLiteral(value) && Number(value.text) === 0);
1704
1896
  }
1705
1897
  function shorthandOptionDisables(prop, ctx) {
1706
1898
  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;
1899
+ const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts3.isVariableDeclaration(d));
1900
+ if (!decl || !ts3.isVariableDeclaration(decl) || !decl.initializer) return true;
1709
1901
  return optionDisables(decl.initializer);
1710
1902
  }
1711
1903
  function structuralNodeKey(node, sf) {
1712
1904
  const children = node.getChildren(sf);
1713
- const text = node.text;
1905
+ const text = "text" in node ? node.text : void 0;
1714
1906
  if (children.length === 0) {
1715
1907
  return typeof text === "string" ? `${node.kind}:${JSON.stringify(text)}` : String(node.kind);
1716
1908
  }
@@ -1722,7 +1914,7 @@ function semanticSkipHits(ctx) {
1722
1914
  const hits = [];
1723
1915
  const callOrdinals = /* @__PURE__ */ new Map();
1724
1916
  const visit = (node) => {
1725
- if (ts2.isCallExpression(node)) {
1917
+ if (ts3.isCallExpression(node)) {
1726
1918
  const callText = node.getText(ctx.sf).trim();
1727
1919
  const callIdentity = structuralNodeKey(node, ctx.sf);
1728
1920
  const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
@@ -1747,19 +1939,19 @@ function semanticSkipHits(ctx) {
1747
1939
  push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
1748
1940
  } else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
1749
1941
  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) {
1942
+ } else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts3.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
1751
1943
  push(terminalNode, [], "an empty .each table (no case ever runs)");
1752
1944
  }
1753
1945
  if (props.length === 0) {
1754
1946
  for (const arg of node.arguments) {
1755
- if (!ts2.isObjectLiteralExpression(arg)) continue;
1947
+ if (!ts3.isObjectLiteralExpression(arg)) continue;
1756
1948
  for (const prop of arg.properties) {
1757
- if (ts2.isPropertyAssignment(prop)) {
1949
+ if (ts3.isPropertyAssignment(prop)) {
1758
1950
  let name = null;
1759
1951
  let propertyCauses = [];
1760
- if (ts2.isIdentifier(prop.name) || ts2.isStringLiteralLike(prop.name)) {
1952
+ if (ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name)) {
1761
1953
  name = prop.name.text;
1762
- } else if (ts2.isComputedPropertyName(prop.name)) {
1954
+ } else if (ts3.isComputedPropertyName(prop.name)) {
1763
1955
  const resolved = staticPropertyName(prop.name.expression, ctx, strings);
1764
1956
  name = resolved?.name ?? null;
1765
1957
  propertyCauses = resolved?.causes ?? [];
@@ -1768,7 +1960,7 @@ function semanticSkipHits(ctx) {
1768
1960
  if (!optionDisables(prop.initializer)) continue;
1769
1961
  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
1962
  push(prop.name, propertyCauses, why);
1771
- } else if (ts2.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1963
+ } else if (ts3.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1772
1964
  if (!shorthandOptionDisables(prop, ctx)) continue;
1773
1965
  push(prop.name, [], `a { ${prop.name.text} } option that conditionally narrows the test run`);
1774
1966
  }
@@ -1777,7 +1969,7 @@ function semanticSkipHits(ctx) {
1777
1969
  }
1778
1970
  }
1779
1971
  }
1780
- ts2.forEachChild(node, visit);
1972
+ ts3.forEachChild(node, visit);
1781
1973
  };
1782
1974
  visit(ctx.sf);
1783
1975
  return hits;
@@ -1816,7 +2008,7 @@ function astSkipHits(c) {
1816
2008
  return { hits, authoritative: true };
1817
2009
  }
1818
2010
  var testSkip = {
1819
- id: RULE3,
2011
+ id: RULE4,
1820
2012
  surface: ["file"],
1821
2013
  certainty: "mechanical",
1822
2014
  run(changes, policy) {
@@ -1834,7 +2026,7 @@ var testSkip = {
1834
2026
  for (const hit of analysis.hits) {
1835
2027
  astHitLines.add(hit.line);
1836
2028
  out2.push(
1837
- makeFinding(RULE3, policy, {
2029
+ makeFinding(RULE4, policy, {
1838
2030
  file: c.path,
1839
2031
  line: hit.line,
1840
2032
  message: `Test skipped or narrowed: ${hit.why}.`,
@@ -1852,7 +2044,7 @@ var testSkip = {
1852
2044
  if (comment && !p.comment) continue;
1853
2045
  if (matchesOutsideString(p, l.content, lang)) {
1854
2046
  out2.push(
1855
- makeFinding(RULE3, policy, {
2047
+ makeFinding(RULE4, policy, {
1856
2048
  file: c.path,
1857
2049
  line: l.newLine ?? void 0,
1858
2050
  message: `Test skipped or narrowed: ${p.why}.`,
@@ -1871,7 +2063,7 @@ var testSkip = {
1871
2063
 
1872
2064
  // src/detectors/coverage-lowering.ts
1873
2065
  import picomatch2 from "picomatch";
1874
- import ts3 from "typescript";
2066
+ import ts4 from "typescript";
1875
2067
 
1876
2068
  // src/detectors/repo.ts
1877
2069
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -1994,34 +2186,34 @@ function containsProtected(token, policy, category, ctx, except) {
1994
2186
  }
1995
2187
 
1996
2188
  // src/detectors/coverage-lowering.ts
1997
- var RULE4 = "coverage-lowering";
2189
+ var RULE5 = "coverage-lowering";
1998
2190
  var METRICS = ["branches", "functions", "lines", "statements"];
1999
- var isMetric = (k) => METRICS.includes(k);
2191
+ var isMetric = (k) => METRICS.some((m) => m === k);
2000
2192
  var norm = (p) => p.replace(/^\.\//, "");
2001
2193
  function keyName(name) {
2002
- if (ts3.isIdentifier(name) || ts3.isStringLiteral(name) || ts3.isNumericLiteral(name)) return name.text;
2194
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
2003
2195
  return null;
2004
2196
  }
2005
2197
  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)) {
2198
+ if (ts4.isNumericLiteral(e)) return Number(e.text);
2199
+ if (ts4.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
2200
+ if (ts4.isPrefixUnaryExpression(e) && e.operator === ts4.SyntaxKind.MinusToken && ts4.isNumericLiteral(e.operand)) {
2009
2201
  return -Number(e.operand.text);
2010
2202
  }
2011
2203
  return void 0;
2012
2204
  }
2013
2205
  function metricsOf(expr, resolve10) {
2014
2206
  const obj = resolve10(expr);
2015
- if (!ts3.isObjectLiteralExpression(obj)) return void 0;
2207
+ if (!ts4.isObjectLiteralExpression(obj)) return void 0;
2016
2208
  const out2 = { values: {}, opaque: false };
2017
2209
  let any = false;
2018
2210
  for (const p of obj.properties) {
2019
- if (ts3.isSpreadAssignment(p)) {
2211
+ if (ts4.isSpreadAssignment(p)) {
2020
2212
  out2.opaque = true;
2021
2213
  any = true;
2022
2214
  continue;
2023
2215
  }
2024
- if (!ts3.isPropertyAssignment(p)) continue;
2216
+ if (!ts4.isPropertyAssignment(p)) continue;
2025
2217
  const k = keyName(p.name);
2026
2218
  if (k === null || !isMetric(k)) continue;
2027
2219
  any = true;
@@ -2033,34 +2225,34 @@ function metricsOf(expr, resolve10) {
2033
2225
  }
2034
2226
  function underKey(node, key2) {
2035
2227
  for (let p = node.parent; p; p = p.parent) {
2036
- if (ts3.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2228
+ if (ts4.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2037
2229
  }
2038
2230
  return false;
2039
2231
  }
2040
2232
  function ownerKey(node) {
2041
2233
  const obj = node.parent;
2042
- if (!obj || !ts3.isObjectLiteralExpression(obj)) return null;
2234
+ if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
2043
2235
  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;
2236
+ while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
2237
+ return q && ts4.isPropertyAssignment(q) ? keyName(q.name) : null;
2046
2238
  }
2047
2239
  function asExpression(src) {
2048
2240
  const t = src.trim();
2049
2241
  return t.startsWith("{") ? `(${t})` : src;
2050
2242
  }
2051
2243
  function sourceOf(src) {
2052
- return ts3.createSourceFile("cfg.ts", asExpression(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
2244
+ return ts4.createSourceFile("cfg.ts", asExpression(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
2053
2245
  }
2054
2246
  function resolverFor(sf) {
2055
2247
  const consts = /* @__PURE__ */ new Map();
2056
2248
  for (const st of sf.statements) {
2057
- if (!ts3.isVariableStatement(st)) continue;
2249
+ if (!ts4.isVariableStatement(st)) continue;
2058
2250
  for (const d of st.declarationList.declarations) {
2059
- if (ts3.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2251
+ if (ts4.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2060
2252
  }
2061
2253
  }
2062
2254
  return (e) => {
2063
- if (ts3.isIdentifier(e)) {
2255
+ if (ts4.isIdentifier(e)) {
2064
2256
  const bound = consts.get(e.text);
2065
2257
  return bound ?? e;
2066
2258
  }
@@ -2073,25 +2265,25 @@ function parseThresholds(src) {
2073
2265
  const sf = sourceOf(src);
2074
2266
  const resolve10 = resolverFor(sf);
2075
2267
  const visit = (node) => {
2076
- if (ts3.isPropertyAssignment(node)) {
2268
+ if (ts4.isPropertyAssignment(node)) {
2077
2269
  const key2 = keyName(node.name);
2078
2270
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
2079
2271
  res.present = true;
2080
2272
  const init = resolve10(node.initializer);
2081
- if (!ts3.isObjectLiteralExpression(init)) {
2273
+ if (!ts4.isObjectLiteralExpression(init)) {
2082
2274
  res.opaque = true;
2083
2275
  } else {
2084
2276
  const flat = metricsOf(init, resolve10);
2085
2277
  if (flat) res.global = merge(res.global, flat);
2086
2278
  for (const p of init.properties) {
2087
- if (ts3.isSpreadAssignment(p)) {
2279
+ if (ts4.isSpreadAssignment(p)) {
2088
2280
  res.opaque = true;
2089
2281
  continue;
2090
2282
  }
2091
- if (!ts3.isPropertyAssignment(p)) continue;
2283
+ if (!ts4.isPropertyAssignment(p)) continue;
2092
2284
  const k = keyName(p.name);
2093
2285
  if (k === null || isMetric(k)) continue;
2094
- if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts3.SyntaxKind.TrueKeyword) {
2286
+ if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts4.SyntaxKind.TrueKeyword) {
2095
2287
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
2096
2288
  continue;
2097
2289
  }
@@ -2103,7 +2295,7 @@ function parseThresholds(src) {
2103
2295
  }
2104
2296
  }
2105
2297
  }
2106
- ts3.forEachChild(node, visit);
2298
+ ts4.forEachChild(node, visit);
2107
2299
  };
2108
2300
  visit(sf);
2109
2301
  } catch {
@@ -2114,12 +2306,12 @@ function merge(a, b) {
2114
2306
  return { values: { ...a?.values ?? {}, ...b.values }, opaque: (a?.opaque ?? false) || b.opaque };
2115
2307
  }
2116
2308
  function literals(e) {
2117
- if (ts3.isStringLiteral(e)) return { items: [e.text], opaque: false };
2118
- if (!ts3.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2309
+ if (ts4.isStringLiteral(e)) return { items: [e.text], opaque: false };
2310
+ if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2119
2311
  const items = [];
2120
2312
  let opaque = false;
2121
2313
  for (const el of e.elements) {
2122
- if (ts3.isStringLiteral(el) || ts3.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2314
+ if (ts4.isStringLiteral(el) || ts4.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2123
2315
  else opaque = true;
2124
2316
  }
2125
2317
  return { items, opaque };
@@ -2130,7 +2322,7 @@ function parseLists(src) {
2130
2322
  const sf = sourceOf(src);
2131
2323
  const resolve10 = resolverFor(sf);
2132
2324
  const visit = (node) => {
2133
- if (ts3.isPropertyAssignment(node)) {
2325
+ if (ts4.isPropertyAssignment(node)) {
2134
2326
  const k = keyName(node.name);
2135
2327
  const vitestCoverage = ownerKey(node) === "coverage";
2136
2328
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
@@ -2144,7 +2336,7 @@ function parseLists(src) {
2144
2336
  if (opaque) res.opaqueIgnore = true;
2145
2337
  }
2146
2338
  }
2147
- ts3.forEachChild(node, visit);
2339
+ ts4.forEachChild(node, visit);
2148
2340
  };
2149
2341
  visit(sf);
2150
2342
  } catch {
@@ -2155,15 +2347,15 @@ function parseSwitches(src) {
2155
2347
  const res = {};
2156
2348
  try {
2157
2349
  const sf = sourceOf(src);
2158
- const bool = (e) => e.kind === ts3.SyntaxKind.TrueKeyword ? true : e.kind === ts3.SyntaxKind.FalseKeyword ? false : void 0;
2350
+ const bool = (e) => e.kind === ts4.SyntaxKind.TrueKeyword ? true : e.kind === ts4.SyntaxKind.FalseKeyword ? false : void 0;
2159
2351
  const visit = (node) => {
2160
- if (ts3.isPropertyAssignment(node)) {
2352
+ if (ts4.isPropertyAssignment(node)) {
2161
2353
  const k = keyName(node.name);
2162
2354
  const owner = ownerKey(node);
2163
2355
  if (k === "enabled" && owner === "coverage") res.enabled = bool(node.initializer);
2164
2356
  else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool(node.initializer);
2165
2357
  }
2166
- ts3.forEachChild(node, visit);
2358
+ ts4.forEachChild(node, visit);
2167
2359
  };
2168
2360
  visit(sf);
2169
2361
  } catch {
@@ -2176,11 +2368,11 @@ function sectionText(src, key2) {
2176
2368
  let out2 = "";
2177
2369
  const visit = (node) => {
2178
2370
  if (out2) return;
2179
- if (ts3.isPropertyAssignment(node) && keyName(node.name) === key2 && ts3.isObjectLiteralExpression(node.initializer)) {
2371
+ if (ts4.isPropertyAssignment(node) && keyName(node.name) === key2 && ts4.isObjectLiteralExpression(node.initializer)) {
2180
2372
  out2 = node.initializer.getText(sf);
2181
2373
  return;
2182
2374
  }
2183
- ts3.forEachChild(node, visit);
2375
+ ts4.forEachChild(node, visit);
2184
2376
  };
2185
2377
  visit(sf);
2186
2378
  return out2;
@@ -2517,7 +2709,7 @@ function gateMovedElsewhere(c, changes, policy) {
2517
2709
  });
2518
2710
  }
2519
2711
  var coverageLowering = {
2520
- id: RULE4,
2712
+ id: RULE5,
2521
2713
  surface: ["file"],
2522
2714
  certainty: "mechanical",
2523
2715
  run(changes, policy, _view, ctx) {
@@ -2539,7 +2731,7 @@ var coverageLowering = {
2539
2731
  reasons.push(...simpleNarrowings(c.path, c.before ?? "", c.after));
2540
2732
  for (const reason of reasons) {
2541
2733
  out2.push(
2542
- makeFinding(RULE4, policy, {
2734
+ makeFinding(RULE5, policy, {
2543
2735
  file: c.path,
2544
2736
  message: `Coverage gate weakened: ${reason}.`,
2545
2737
  evidence: reason,
@@ -2552,7 +2744,7 @@ var coverageLowering = {
2552
2744
  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
2745
  if (gated && !gateMovedElsewhere(c, changes, policy)) {
2554
2746
  out2.push(
2555
- makeFinding(RULE4, policy, {
2747
+ makeFinding(RULE5, policy, {
2556
2748
  file: c.path,
2557
2749
  message: "Coverage gate weakened: the coverage config was deleted.",
2558
2750
  evidence: c.path,
@@ -2566,7 +2758,7 @@ var coverageLowering = {
2566
2758
  const removedCoverage = removed.find((l) => /--coverage\b/.test(l.content));
2567
2759
  if (removedCoverage && !added.some((l) => /--coverage\b/.test(l.content))) {
2568
2760
  out2.push(
2569
- makeFinding(RULE4, policy, {
2761
+ makeFinding(RULE5, policy, {
2570
2762
  file: c.path,
2571
2763
  message: "The --coverage flag was removed from a test script.",
2572
2764
  evidence: removedCoverage.content.trim(),
@@ -2580,7 +2772,7 @@ var coverageLowering = {
2580
2772
  if (removed.some((r) => s.re.test(r.content))) continue;
2581
2773
  if (/passWithNoTests/.test(s.re.source) && dirHasSpec(c.path, policy, ctx) === false) continue;
2582
2774
  out2.push(
2583
- makeFinding(RULE4, policy, {
2775
+ makeFinding(RULE5, policy, {
2584
2776
  file: c.path,
2585
2777
  line: l.newLine ?? void 0,
2586
2778
  message: s.message,
@@ -2603,7 +2795,7 @@ var coverageLowering = {
2603
2795
  const ov = oldNums.get(m[1]);
2604
2796
  if (ov !== void 0 && Number(m[2]) < ov) {
2605
2797
  out2.push(
2606
- makeFinding(RULE4, policy, {
2798
+ makeFinding(RULE5, policy, {
2607
2799
  file: c.path,
2608
2800
  line: l.newLine ?? void 0,
2609
2801
  message: `Coverage threshold for ${m[1]} lowered ${ov} \u2192 ${m[2]}.`,
@@ -2626,7 +2818,7 @@ var coverageLowering = {
2626
2818
  };
2627
2819
 
2628
2820
  // src/detectors/ci-tampering.ts
2629
- var RULE5 = "ci-tampering";
2821
+ var RULE6 = "ci-tampering";
2630
2822
  var USES = /^\s*-\s*uses:/;
2631
2823
  var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
2632
2824
  var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
@@ -2890,7 +3082,7 @@ function isTruthy(raw) {
2890
3082
  return f !== void 0 && truthy(f);
2891
3083
  }
2892
3084
  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;
3085
+ var indentOf = (l) => l.length - l.trimStart().length;
2894
3086
  var STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
2895
3087
  function stepOf(lines, i) {
2896
3088
  const ind = indentOf(lines[i]);
@@ -3072,7 +3264,7 @@ function triggerNarrowings(before, after, opts) {
3072
3264
  }
3073
3265
  var REUSED_WORKFLOW = /^\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
3074
3266
  var ciTampering = {
3075
- id: RULE5,
3267
+ id: RULE6,
3076
3268
  surface: ["file"],
3077
3269
  certainty: "mechanical",
3078
3270
  run(changes, policy, _view, ctx) {
@@ -3084,7 +3276,7 @@ var ciTampering = {
3084
3276
  if (c.kind !== "file") continue;
3085
3277
  if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
3086
3278
  out2.push(
3087
- makeFinding(RULE5, policy, {
3279
+ makeFinding(RULE6, policy, {
3088
3280
  file: c.path,
3089
3281
  message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
3090
3282
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -3108,7 +3300,7 @@ var ciTampering = {
3108
3300
  }
3109
3301
  if (coe && isTruthy(coe[1])) {
3110
3302
  out2.push(
3111
- makeFinding(RULE5, policy, {
3303
+ makeFinding(RULE6, policy, {
3112
3304
  file: c.path,
3113
3305
  line: l.newLine ?? void 0,
3114
3306
  message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
@@ -3118,7 +3310,7 @@ var ciTampering = {
3118
3310
  );
3119
3311
  } else if (cond && isAlwaysFalse(cond[1])) {
3120
3312
  out2.push(
3121
- makeFinding(RULE5, policy, {
3313
+ makeFinding(RULE6, policy, {
3122
3314
  file: c.path,
3123
3315
  line: l.newLine ?? void 0,
3124
3316
  message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
@@ -3138,7 +3330,7 @@ var ciTampering = {
3138
3330
  const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
3139
3331
  if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
3140
3332
  out2.push(
3141
- makeFinding(RULE5, policy, {
3333
+ makeFinding(RULE6, policy, {
3142
3334
  file: c.path,
3143
3335
  line: idx + 1,
3144
3336
  message: `A check's run block was neutralised: ${n.what}.`,
@@ -3154,7 +3346,7 @@ var ciTampering = {
3154
3346
  const step = stepOf(afterLines, l.newLine - 1);
3155
3347
  if (step && !stepHasCheck(afterLines, step)) continue;
3156
3348
  out2.push(
3157
- makeFinding(RULE5, policy, {
3349
+ makeFinding(RULE6, policy, {
3158
3350
  file: c.path,
3159
3351
  line: l.newLine,
3160
3352
  message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
@@ -3163,11 +3355,11 @@ var ciTampering = {
3163
3355
  })
3164
3356
  );
3165
3357
  }
3166
- if (c.before != null) {
3358
+ if (c.before != null && c.after != null) {
3167
3359
  triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
3168
3360
  for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
3169
3361
  out2.push(
3170
- makeFinding(RULE5, policy, {
3362
+ makeFinding(RULE6, policy, {
3171
3363
  file: c.path,
3172
3364
  message: `The workflow's triggers were narrowed: ${reason}.`,
3173
3365
  evidence: reason,
@@ -3196,7 +3388,7 @@ var ciTampering = {
3196
3388
  if (s.state === "kept") continue;
3197
3389
  if (s.state === "neutralised") {
3198
3390
  out2.push(
3199
- makeFinding(RULE5, policy, {
3391
+ makeFinding(RULE6, policy, {
3200
3392
  file: c.path,
3201
3393
  message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
3202
3394
  evidence: `${l.content.trim()} \u2192 ${s.by}`,
@@ -3206,7 +3398,7 @@ var ciTampering = {
3206
3398
  continue;
3207
3399
  }
3208
3400
  out2.push(
3209
- makeFinding(RULE5, policy, {
3401
+ makeFinding(RULE6, policy, {
3210
3402
  file: c.path,
3211
3403
  message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
3212
3404
  evidence: l.content.trim(),
@@ -3235,7 +3427,7 @@ function shippedVersion() {
3235
3427
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
3236
3428
  try {
3237
3429
  const pkg = JSON.parse(readFileSync3(join5(here, rel), "utf8"));
3238
- if (pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
3430
+ if (isRecord(pkg) && pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
3239
3431
  return pkg.version;
3240
3432
  }
3241
3433
  } catch {
@@ -3319,11 +3511,25 @@ function resolvesToClaudeSettings(path, env = process.env) {
3319
3511
 
3320
3512
  // src/detectors/policy-diff.ts
3321
3513
  import { parse } from "yaml";
3514
+ function verifyBlock(v) {
3515
+ if (!isRecord(v)) return void 0;
3516
+ return {
3517
+ ...typeof v.command === "string" ? { command: v.command } : {},
3518
+ ...typeof v.budget === "number" ? { budget: v.budget } : {},
3519
+ ...Array.isArray(v.inputs) ? { inputs: v.inputs.filter((g) => typeof g === "string") } : {},
3520
+ ...typeof v.backend === "string" ? { backend: v.backend } : {},
3521
+ ...typeof v.image === "string" ? { image: v.image } : {}
3522
+ };
3523
+ }
3524
+ function ruleOverrides(v) {
3525
+ if (!isRecord(v)) return {};
3526
+ return Object.fromEntries(Object.entries(v).map(([name, cfg]) => [name, isRecord(cfg) ? cfg : {}]));
3527
+ }
3322
3528
  var globs2 = (list2) => Array.isArray(list2) ? list2.filter((g) => typeof g === "string").map(normalizeGlob) : [];
3323
3529
  function safeParse(src) {
3324
3530
  try {
3325
3531
  const v = parse(src);
3326
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
3532
+ if (isRecord(v)) return v;
3327
3533
  return null;
3328
3534
  } catch {
3329
3535
  return null;
@@ -3334,18 +3540,24 @@ function effective(raw) {
3334
3540
  const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
3335
3541
  const base = defaultPolicy(version);
3336
3542
  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) } : {} };
3543
+ for (const [name, over] of Object.entries(ruleOverrides(raw.rules))) {
3544
+ rules[name] = {
3545
+ ...rules[name] ?? {},
3546
+ ...over.severity !== void 0 ? { severity: over.severity } : {},
3547
+ ...over.enabled !== void 0 ? { enabled: over.enabled } : {},
3548
+ ...over.exclude !== void 0 ? { exclude: globs2(over.exclude) } : {}
3549
+ };
3340
3550
  }
3341
- const userProtected = raw.protected && typeof raw.protected === "object" ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
3551
+ const userProtected = isRecord(raw.protected) ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
3552
+ const signoff = isRecord(raw.signoff) ? raw.signoff : {};
3553
+ const requiredFor = [signoff.required_for, signoff.requiredFor].find(Array.isArray);
3342
3554
  return {
3343
3555
  version,
3344
3556
  rules,
3345
3557
  ignore: raw.ignore ? globs2(raw.ignore) : base.ignore ?? [],
3346
3558
  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
3559
+ requiredFor: requiredFor ? requiredFor.filter((s) => typeof s === "string") : base.signoff.requiredFor,
3560
+ ledger: typeof signoff.ledger === "string" ? signoff.ledger : base.signoff.ledger
3349
3561
  };
3350
3562
  }
3351
3563
  function policyWeakening(before, after) {
@@ -3400,8 +3612,8 @@ function policyWeakening(before, after) {
3400
3612
  if (ae.ledger !== be.ledger) {
3401
3613
  reasons.push(`sign-off ledger moved (${be.ledger} \u2192 ${ae.ledger}) \u2014 the local layer would trust a different file`);
3402
3614
  }
3403
- const bv = b.verify;
3404
- const av = a.verify;
3615
+ const bv = verifyBlock(b.verify);
3616
+ const av = verifyBlock(a.verify);
3405
3617
  if (bv?.command) {
3406
3618
  if (!av?.command) {
3407
3619
  reasons.push("verify block removed \u2014 pristine-suite re-execution disabled");
@@ -4159,7 +4371,7 @@ function scriptWeakening(before, after, opts = {}) {
4159
4371
  const evidence = (trace?.line ?? before.find((l) => checkIdentity(stripComment(l).trim().replace(/^!\s+/, "").replace(PREFIX, "")) === id) ?? id).trim();
4160
4372
  out2.push({ reason: `the check invocation \`${id}\` was ${how}`, evidence });
4161
4373
  }
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];
4374
+ 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
4375
  if (floor) {
4164
4376
  for (const inv of afterInv) {
4165
4377
  if (inv.state !== "live" || !inv.pin) continue;
@@ -4555,7 +4767,10 @@ function destinations(cmd, args, isDir) {
4555
4767
  positional.push(a);
4556
4768
  }
4557
4769
  const base = (p) => p.replace(/\/+$/, "").split("/").pop() ?? p;
4558
- if (target !== null) return positional.map((p) => `${target.replace(/\/+$/, "")}/${base(p)}`);
4770
+ if (target !== null) {
4771
+ const dir = target.replace(/\/+$/, "");
4772
+ return positional.map((p) => `${dir}/${base(p)}`);
4773
+ }
4559
4774
  if (positional.length < 2) return [];
4560
4775
  const dest = positional[positional.length - 1];
4561
4776
  const srcs = positional.slice(0, -1);
@@ -4569,16 +4784,18 @@ function expandShellPath(t, env = process.env) {
4569
4784
  }
4570
4785
  function hookTests(policy, ctx) {
4571
4786
  const cwd = ctx?.cwd;
4572
- const onDisk = cwd !== void 0 && existsSync2(cwd);
4573
- const root = onDisk ? canonicalPath(cwd) : cwd;
4787
+ const diskCwd = cwd !== void 0 && existsSync2(cwd) ? cwd : void 0;
4788
+ const onDisk = diskCwd !== void 0;
4789
+ const root = diskCwd !== void 0 ? canonicalPath(diskCwd) : cwd;
4574
4790
  const memo = /* @__PURE__ */ new Map();
4575
4791
  const rel = (t) => {
4576
4792
  const hit = memo.get(t);
4577
4793
  if (hit !== void 0) return hit;
4578
4794
  let p = expandShellPath(t.replace(/^["']|["']$/g, ""));
4579
4795
  const pathy = isAbsolute4(p) || p.includes("/") && !/^[-:]/.test(p);
4580
- if (pathy && (isAbsolute4(p) || onDisk)) {
4581
- p = canonicalPath(isAbsolute4(p) ? p : resolve2(cwd, p));
4796
+ const abs2 = isAbsolute4(p) ? p : diskCwd !== void 0 ? resolve2(diskCwd, p) : null;
4797
+ if (pathy && abs2 !== null) {
4798
+ p = canonicalPath(abs2);
4582
4799
  if (root !== void 0) {
4583
4800
  if (p === root) p = ".";
4584
4801
  else if (p.startsWith(root + "/")) p = p.slice(root.length + 1);
@@ -4743,7 +4960,7 @@ function xargsWritesHook(toks, fed, policy, ctx) {
4743
4960
  }
4744
4961
 
4745
4962
  // src/detectors/hook-tampering.ts
4746
- var RULE6 = "hook-tampering";
4963
+ var RULE7 = "hook-tampering";
4747
4964
  var ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
4748
4965
  var EVENTS = ["PreToolUse", "Stop"];
4749
4966
  var isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -4959,7 +5176,7 @@ function coverage(cs) {
4959
5176
  const live = cs.filter((c) => c.runs);
4960
5177
  if (live.length === 0) return "none";
4961
5178
  if (live.some((c) => c.tools === null)) return null;
4962
- return new Set(live.flatMap((c) => [...c.tools]));
5179
+ return new Set(live.flatMap((c) => c.tools ? [...c.tools] : []));
4963
5180
  }
4964
5181
  function duplicateJsonKeys(src) {
4965
5182
  const dups = [];
@@ -5072,7 +5289,7 @@ function settingsWeakening(before, after, local) {
5072
5289
  const schema = schemaProblem(e.group, e.h);
5073
5290
  if (schema) {
5074
5291
  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") {
5292
+ } else if ((e.event === "PreToolUse" || e.event === "Stop") && isObj(e.h)) {
5076
5293
  reasons.push(`a ${e.event} hook entry beside the gate ${siblingProblem(e.h)}`);
5077
5294
  }
5078
5295
  }
@@ -5090,7 +5307,7 @@ function claudeSettingsFindings(c, policy) {
5090
5307
  if (c.after == null) return null;
5091
5308
  const after = parseSettings(c.after);
5092
5309
  const before = c.before != null ? parseSettings(c.before) : null;
5093
- const finding = (reason) => makeFinding(RULE6, policy, {
5310
+ const finding = (reason) => makeFinding(RULE7, policy, {
5094
5311
  file: c.path,
5095
5312
  message: `The Claude Code hook wiring was weakened: ${reason}.`,
5096
5313
  evidence: reason,
@@ -5111,10 +5328,10 @@ function claudeSettingsFindings(c, policy) {
5111
5328
  function policyFindings(c, policy) {
5112
5329
  if (c.after == null) return null;
5113
5330
  const added = c.before == null;
5114
- const reasons = added ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
5331
+ const reasons = c.before == null ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
5115
5332
  if (reasons === null) return null;
5116
5333
  return reasons.map(
5117
- (reason) => makeFinding(RULE6, policy, {
5334
+ (reason) => makeFinding(RULE7, policy, {
5118
5335
  file: c.path,
5119
5336
  message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
5120
5337
  evidence: reason,
@@ -5177,7 +5394,7 @@ function configFindings(c, changes, policy, ctx) {
5177
5394
  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
5395
  }
5179
5396
  return reasons.map(
5180
- (reason) => makeFinding(RULE6, policy, {
5397
+ (reason) => makeFinding(RULE7, policy, {
5181
5398
  file: c.path,
5182
5399
  message: `The hook configuration was weakened: ${reason}.`,
5183
5400
  evidence: reason,
@@ -5260,7 +5477,7 @@ function huskyRuntimeWrite(path, content, ctx) {
5260
5477
  const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
5261
5478
  const entry = inspectRel(join6(ctx.cwd, root), ".husky/pre-commit");
5262
5479
  if (entry.kind !== "file") return false;
5263
- const gate = textOf(entry);
5480
+ const gate = textOf2(entry);
5264
5481
  if (gate == null) return false;
5265
5482
  return invocations(gate.split("\n"), { errexit: true }).some(
5266
5483
  (i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
@@ -5269,7 +5486,7 @@ function huskyRuntimeWrite(path, content, ctx) {
5269
5486
  var isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
5270
5487
  var isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
5271
5488
  var hookTampering = {
5272
- id: RULE6,
5489
+ id: RULE7,
5273
5490
  surface: ["file", "command"],
5274
5491
  certainty: "mechanical",
5275
5492
  run(changes, policy, _view, ctx) {
@@ -5282,7 +5499,7 @@ var hookTampering = {
5282
5499
  if (isPackageJson(c.path) && c.before != null && c.after != null) {
5283
5500
  for (const reason of packageJsonWeakening(c.before, c.after)) {
5284
5501
  out2.push(
5285
- makeFinding(RULE6, policy, {
5502
+ makeFinding(RULE7, policy, {
5286
5503
  file: c.path,
5287
5504
  message: `The hook installation was weakened: ${reason}.`,
5288
5505
  evidence: reason,
@@ -5295,7 +5512,7 @@ var hookTampering = {
5295
5512
  }
5296
5513
  if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
5297
5514
  out2.push(
5298
- makeFinding(RULE6, policy, {
5515
+ makeFinding(RULE7, policy, {
5299
5516
  file: c.path,
5300
5517
  message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
5301
5518
  evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
@@ -5305,7 +5522,7 @@ var hookTampering = {
5305
5522
  }
5306
5523
  if (c.op === "delete") {
5307
5524
  out2.push(
5308
- makeFinding(RULE6, policy, {
5525
+ makeFinding(RULE7, policy, {
5309
5526
  file: c.path,
5310
5527
  message: "A protected hook/policy file was deleted.",
5311
5528
  evidence: c.path,
@@ -5314,7 +5531,7 @@ var hookTampering = {
5314
5531
  );
5315
5532
  } else if (c.op === "rename" && !renameKept(c)) {
5316
5533
  out2.push(
5317
- makeFinding(RULE6, policy, {
5534
+ makeFinding(RULE7, policy, {
5318
5535
  file: c.path,
5319
5536
  message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
5320
5537
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -5328,7 +5545,7 @@ var hookTampering = {
5328
5545
  if (c.before != null && c.after != null) {
5329
5546
  for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
5330
5547
  out2.push(
5331
- makeFinding(RULE6, policy, {
5548
+ makeFinding(RULE7, policy, {
5332
5549
  file: c.path,
5333
5550
  message: `CODEOWNERS was weakened: ${reason}.`,
5334
5551
  evidence: reason,
@@ -5344,12 +5561,12 @@ var hookTampering = {
5344
5561
  const husky = isHuskyScript(c.path);
5345
5562
  const opts = husky ? { errexit: true } : {};
5346
5563
  const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
5347
- if (beforePin !== null) {
5564
+ if (beforePin !== null && c.after != null) {
5348
5565
  const afterPin = initScriptPin(c.after);
5349
5566
  const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
5350
5567
  if (!kept) {
5351
5568
  out2.push(
5352
- makeFinding(RULE6, policy, {
5569
+ makeFinding(RULE7, policy, {
5353
5570
  file: c.path,
5354
5571
  message: "A protected hook script no longer matches the shape `tamperward init` writes.",
5355
5572
  evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
@@ -5372,7 +5589,7 @@ var hookTampering = {
5372
5589
  if (shebang || !live) {
5373
5590
  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
5591
  out2.push(
5375
- makeFinding(RULE6, policy, {
5592
+ makeFinding(RULE7, policy, {
5376
5593
  file: c.path,
5377
5594
  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
5595
  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 +5598,7 @@ var hookTampering = {
5381
5598
  );
5382
5599
  } else if (earlyExit) {
5383
5600
  out2.push(
5384
- makeFinding(RULE6, policy, {
5601
+ makeFinding(RULE7, policy, {
5385
5602
  file: c.path,
5386
5603
  line: earlyExit.newLine ?? void 0,
5387
5604
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -5403,7 +5620,7 @@ var hookTampering = {
5403
5620
  deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
5404
5621
  ].filter((d) => d !== null);
5405
5622
  out2.push(
5406
- makeFinding(RULE6, policy, {
5623
+ makeFinding(RULE7, policy, {
5407
5624
  file: c.path,
5408
5625
  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
5626
  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 +5632,7 @@ var hookTampering = {
5415
5632
  }
5416
5633
  if (shebangChanged && c.after != null) {
5417
5634
  out2.push(
5418
- makeFinding(RULE6, policy, {
5635
+ makeFinding(RULE7, policy, {
5419
5636
  file: c.path,
5420
5637
  line: 1,
5421
5638
  message: `A protected hook's interpreter was changed: ${shebang}.`,
@@ -5426,7 +5643,7 @@ var hookTampering = {
5426
5643
  }
5427
5644
  if (earlyExit) {
5428
5645
  out2.push(
5429
- makeFinding(RULE6, policy, {
5646
+ makeFinding(RULE7, policy, {
5430
5647
  file: c.path,
5431
5648
  line: earlyExit.newLine ?? void 0,
5432
5649
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -5438,7 +5655,7 @@ var hookTampering = {
5438
5655
  const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
5439
5656
  if (deadGuard) {
5440
5657
  out2.push(
5441
- makeFinding(RULE6, policy, {
5658
+ makeFinding(RULE7, policy, {
5442
5659
  file: c.path,
5443
5660
  line: deadGuard.newLine ?? void 0,
5444
5661
  message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
@@ -5450,7 +5667,7 @@ var hookTampering = {
5450
5667
  const lost = scriptWeakening(beforeLines, afterLines, opts);
5451
5668
  if (lost.length) {
5452
5669
  out2.push(
5453
- makeFinding(RULE6, policy, {
5670
+ makeFinding(RULE7, policy, {
5454
5671
  file: c.path,
5455
5672
  message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
5456
5673
  evidence: lost[0].evidence,
@@ -5469,7 +5686,7 @@ var hookTampering = {
5469
5686
  const m = l.content.match(INLINE);
5470
5687
  if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
5471
5688
  out2.push(
5472
- makeFinding(RULE6, policy, {
5689
+ makeFinding(RULE7, policy, {
5473
5690
  file: c.path,
5474
5691
  line: l.newLine ?? void 0,
5475
5692
  message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
@@ -5479,7 +5696,7 @@ var hookTampering = {
5479
5696
  );
5480
5697
  } else if (/enabled:\s*false\b/.test(l.content)) {
5481
5698
  out2.push(
5482
- makeFinding(RULE6, policy, {
5699
+ makeFinding(RULE7, policy, {
5483
5700
  file: c.path,
5484
5701
  line: l.newLine ?? void 0,
5485
5702
  message: "A rule was disabled (enabled: false) in the policy.",
@@ -5497,7 +5714,7 @@ var hookTampering = {
5497
5714
  const hiding = updateIndexHiding(toks, policy);
5498
5715
  if (hiding) {
5499
5716
  out2.push(
5500
- makeFinding(RULE6, policy, {
5717
+ makeFinding(RULE7, policy, {
5501
5718
  file: repoRelative(hiding.file, ctx),
5502
5719
  message: `Hook tampering via shell: ${hiding.reason}.`,
5503
5720
  evidence: seg,
@@ -5513,7 +5730,7 @@ var hookTampering = {
5513
5730
  const file = target ? repoRelative(target, ctx) : void 0;
5514
5731
  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
5732
  out2.push(
5516
- makeFinding(RULE6, policy, {
5733
+ makeFinding(RULE7, policy, {
5517
5734
  ...file !== void 0 ? { file } : {},
5518
5735
  message: `Hook tampering via shell: ${why}${outside}.`,
5519
5736
  evidence: seg,
@@ -5530,11 +5747,11 @@ var hookTampering = {
5530
5747
  };
5531
5748
 
5532
5749
  // src/detectors/test-deletion.ts
5533
- import ts5 from "typescript";
5750
+ import ts6 from "typescript";
5534
5751
 
5535
5752
  // src/detectors/suite-config.ts
5536
5753
  import picomatch3 from "picomatch";
5537
- import ts4 from "typescript";
5754
+ import ts5 from "typescript";
5538
5755
  var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
5539
5756
  var JEST_DEFAULT_IGNORE = ["/node_modules/"];
5540
5757
  var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
@@ -5565,18 +5782,18 @@ var PYTEST_CANONICAL_SAMPLES = [
5565
5782
  "tests/conftest.py"
5566
5783
  ];
5567
5784
  function keyName2(name) {
5568
- if (ts4.isIdentifier(name) || ts4.isStringLiteral(name)) return name.text;
5785
+ if (ts5.isIdentifier(name) || ts5.isStringLiteral(name)) return name.text;
5569
5786
  return null;
5570
5787
  }
5571
- var isStr = (e) => ts4.isStringLiteral(e) || ts4.isNoSubstitutionTemplateLiteral(e);
5788
+ var isStr = (e) => ts5.isStringLiteral(e) || ts5.isNoSubstitutionTemplateLiteral(e);
5572
5789
  function literals2(e) {
5573
5790
  if (isStr(e)) return { items: [e.text], opaque: false };
5574
- if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5791
+ if (!ts5.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5575
5792
  const items = [];
5576
5793
  let opaque = false;
5577
5794
  for (const el of e.elements) {
5578
5795
  if (isStr(el)) items.push(el.text);
5579
- else if (ts4.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5796
+ else if (ts5.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5580
5797
  items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5581
5798
  } else opaque = true;
5582
5799
  }
@@ -5584,10 +5801,10 @@ function literals2(e) {
5584
5801
  }
5585
5802
  function ownerKey2(node) {
5586
5803
  const obj = node.parent;
5587
- if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
5804
+ if (!obj || !ts5.isObjectLiteralExpression(obj)) return null;
5588
5805
  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;
5806
+ while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
5807
+ return q && ts5.isPropertyAssignment(q) ? keyName2(q.name) : null;
5591
5808
  }
5592
5809
  function asExpression2(src) {
5593
5810
  const t = src.trim();
@@ -5622,13 +5839,12 @@ function effectivePytestFile(files) {
5622
5839
  function effectivePytestConfig(path, ctx) {
5623
5840
  if (path.includes("/")) return false;
5624
5841
  const base = path;
5625
- const ORDER = PYTEST_INI_ORDER;
5626
- const rank = ORDER.indexOf(base);
5842
+ const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
5627
5843
  if (rank < 0) return false;
5628
5844
  const files = ctx?.trackedFiles;
5629
5845
  if (!files) return true;
5630
5846
  const rootNames = new Set(files.filter((f) => !f.includes("/")));
5631
- return !ORDER.slice(0, rank).some((n) => rootNames.has(n));
5847
+ return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
5632
5848
  }
5633
5849
  function pytestBlock(src) {
5634
5850
  const lines = src.split(/\r?\n/);
@@ -5728,15 +5944,15 @@ function collect(root, runner) {
5728
5944
  projectList = e;
5729
5945
  };
5730
5946
  const visit = (node) => {
5731
- if (ts4.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5947
+ if (ts5.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5732
5948
  sel.present = true;
5733
5949
  sel.opaque = true;
5734
5950
  }
5735
- if (ts4.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts4.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5951
+ if (ts5.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts5.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5736
5952
  sel.present = true;
5737
5953
  sel.opaque = true;
5738
5954
  }
5739
- if (ts4.isPropertyAssignment(node)) {
5955
+ if (ts5.isPropertyAssignment(node)) {
5740
5956
  const k = keyName2(node.name);
5741
5957
  if (runner === "jest") {
5742
5958
  if (k === "testMatch") sel.include = take(k, node.initializer);
@@ -5766,7 +5982,7 @@ function collect(root, runner) {
5766
5982
  }
5767
5983
  }
5768
5984
  }
5769
- ts4.forEachChild(node, visit);
5985
+ ts5.forEachChild(node, visit);
5770
5986
  };
5771
5987
  visit(root);
5772
5988
  if (projectList) sel.projects = readProjects(projectList, sel, runner);
@@ -5775,30 +5991,30 @@ function collect(root, runner) {
5775
5991
  }
5776
5992
  function unwrapConfigCall(e) {
5777
5993
  let x = e;
5778
- while (ts4.isParenthesizedExpression(x) || ts4.isAsExpression(x) || x.kind === ts4.SyntaxKind.SatisfiesExpression) {
5994
+ while (ts5.isParenthesizedExpression(x) || ts5.isAsExpression(x) || ts5.isSatisfiesExpression(x)) {
5779
5995
  x = x.expression;
5780
5996
  }
5781
- if (ts4.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5997
+ if (ts5.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5782
5998
  return x;
5783
5999
  }
5784
6000
  var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
5785
6001
  function spreadsIntoConfig(node, runner) {
5786
6002
  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);
6003
+ if (!ts5.isObjectLiteralExpression(obj)) return false;
6004
+ const keys = obj.properties.map((p) => ts5.isPropertyAssignment(p) ? keyName2(p.name) : null);
5789
6005
  if (runner === "vitest") {
5790
6006
  if (keys.includes("test")) return true;
5791
6007
  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";
6008
+ while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
6009
+ return !!q && ts5.isPropertyAssignment(q) && keyName2(q.name) === "test";
5794
6010
  }
5795
6011
  return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
5796
6012
  }
5797
6013
  function extendsOf(el) {
5798
6014
  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;
6015
+ if (!ts5.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
6016
+ if (p.initializer.kind === ts5.SyntaxKind.TrueKeyword) return true;
6017
+ if (p.initializer.kind === ts5.SyntaxKind.FalseKeyword) return null;
5802
6018
  return "path";
5803
6019
  }
5804
6020
  return null;
@@ -5819,14 +6035,14 @@ function readProject(el, root, runner) {
5819
6035
  return p;
5820
6036
  }
5821
6037
  function readProjects(e, root, runner) {
5822
- if (!ts4.isArrayLiteralExpression(e)) {
6038
+ if (!ts5.isArrayLiteralExpression(e)) {
5823
6039
  root.opaque = true;
5824
6040
  return [];
5825
6041
  }
5826
6042
  const list2 = [];
5827
6043
  for (const el of e.elements) {
5828
6044
  const lit = unwrapConfigCall(el);
5829
- if (ts4.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
6045
+ if (ts5.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
5830
6046
  else root.opaque = true;
5831
6047
  }
5832
6048
  return list2;
@@ -5834,14 +6050,14 @@ function readProjects(e, root, runner) {
5834
6050
  function workspaceArray(sf) {
5835
6051
  for (const st of sf.statements) {
5836
6052
  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;
6053
+ if (ts5.isExportAssignment(st)) e = st.expression;
6054
+ else if (ts5.isExpressionStatement(st) && ts5.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
5839
6055
  if (!e) continue;
5840
- while (ts4.isParenthesizedExpression(e) || ts4.isAsExpression(e) || e.kind === ts4.SyntaxKind.SatisfiesExpression) {
6056
+ while (ts5.isParenthesizedExpression(e) || ts5.isAsExpression(e) || ts5.isSatisfiesExpression(e)) {
5841
6057
  e = e.expression;
5842
6058
  }
5843
- if (ts4.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
5844
- return ts4.isArrayLiteralExpression(e) ? e : null;
6059
+ if (ts5.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
6060
+ return ts5.isArrayLiteralExpression(e) ? e : null;
5845
6061
  }
5846
6062
  return null;
5847
6063
  }
@@ -5849,7 +6065,7 @@ function parseSelection(src, runner, path = "") {
5849
6065
  let sel = empty();
5850
6066
  if (runner === "pytest") return pytestSelection(src);
5851
6067
  try {
5852
- const sf = ts4.createSourceFile("cfg.ts", asExpression2(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
6068
+ const sf = ts5.createSourceFile("cfg.ts", asExpression2(src), ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
5853
6069
  if (runner === "vitest" && isWorkspaceFile(path)) {
5854
6070
  const arr = workspaceArray(sf);
5855
6071
  if (arr) {
@@ -5943,7 +6159,7 @@ function suiteNarrowings(before, after, path, samples) {
5943
6159
  }
5944
6160
 
5945
6161
  // src/detectors/test-deletion.ts
5946
- var RULE7 = "test-deletion";
6162
+ var RULE8 = "test-deletion";
5947
6163
  var JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
5948
6164
  var TEST_DEFS = {
5949
6165
  py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
@@ -5956,21 +6172,21 @@ var TEST_DEFS = {
5956
6172
  cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
5957
6173
  };
5958
6174
  function calleeName(expr) {
5959
- if (ts5.isIdentifier(expr)) return expr.text;
5960
- if (ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression)) {
6175
+ if (ts6.isIdentifier(expr)) return expr.text;
6176
+ if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression)) {
5961
6177
  return expr.expression.text;
5962
6178
  }
5963
6179
  return null;
5964
6180
  }
5965
6181
  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);
6182
+ var isEachOf = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
6183
+ var isDescribeEach = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
5968
6184
  function loopRows(iterable) {
5969
- if (ts5.isArrayLiteralExpression(iterable)) {
6185
+ if (ts6.isArrayLiteralExpression(iterable)) {
5970
6186
  let n = 0;
5971
6187
  let open = false;
5972
6188
  for (const el of iterable.elements) {
5973
- if (ts5.isSpreadElement(el)) open = true;
6189
+ if (ts6.isSpreadElement(el)) open = true;
5974
6190
  else n++;
5975
6191
  }
5976
6192
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -5982,20 +6198,20 @@ function templateRows(node) {
5982
6198
  return Math.max(rows, 1);
5983
6199
  }
5984
6200
  function hasSubstantiveBody(call) {
5985
- const fn = call.arguments.find((a) => ts5.isArrowFunction(a) || ts5.isFunctionExpression(a));
6201
+ const fn = call.arguments.find((a) => ts6.isArrowFunction(a) || ts6.isFunctionExpression(a));
5986
6202
  if (!fn) return true;
5987
6203
  const body = fn.body;
5988
- const text = ts5.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
6204
+ const text = ts6.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
5989
6205
  return text.split("\n").some((l) => isSignificantLine(l.trim(), "js"));
5990
6206
  }
5991
6207
  function eachRows(call) {
5992
6208
  const arg = call.arguments[0];
5993
6209
  if (!arg) return { n: 1, open: true };
5994
- if (ts5.isArrayLiteralExpression(arg)) {
6210
+ if (ts6.isArrayLiteralExpression(arg)) {
5995
6211
  let n = 0;
5996
6212
  let open = false;
5997
6213
  for (const el of arg.elements) {
5998
- if (ts5.isSpreadElement(el)) open = true;
6214
+ if (ts6.isSpreadElement(el)) open = true;
5999
6215
  else n++;
6000
6216
  }
6001
6217
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -6010,35 +6226,35 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
6010
6226
  for (const line of src.split("\n")) if (re.test(line)) n2++;
6011
6227
  return { min: n2, open: false };
6012
6228
  }
6013
- const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
6229
+ const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6014
6230
  let n = 0;
6015
6231
  let open = false;
6016
6232
  const visit = (node, mult) => {
6017
- if (ts5.isForOfStatement(node)) {
6233
+ if (ts6.isForOfStatement(node)) {
6018
6234
  const r = loopRows(node.expression);
6019
6235
  if (r.open) open = true;
6020
6236
  visit(node.statement, mult * r.n);
6021
6237
  return;
6022
6238
  }
6023
- if (ts5.isForStatement(node) || ts5.isForInStatement(node) || ts5.isWhileStatement(node) || ts5.isDoStatement(node)) {
6239
+ if (ts6.isForStatement(node) || ts6.isForInStatement(node) || ts6.isWhileStatement(node) || ts6.isDoStatement(node)) {
6024
6240
  open = true;
6025
6241
  }
6026
- if (ts5.isCallExpression(node)) {
6242
+ if (ts6.isCallExpression(node)) {
6027
6243
  const callee = node.expression;
6028
- if (ts5.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6244
+ if (ts6.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6029
6245
  const r = loopRows(callee.expression);
6030
6246
  if (r.open) open = true;
6031
6247
  for (const a of node.arguments) visit(a, mult * r.n);
6032
6248
  return;
6033
6249
  }
6034
6250
  const head = node.expression;
6035
- if (ts5.isCallExpression(head) && isDescribeEach(head.expression)) {
6251
+ if (ts6.isCallExpression(head) && isDescribeEach(head.expression)) {
6036
6252
  const r = eachRows(head);
6037
6253
  if (r.open) open = true;
6038
6254
  for (const a of node.arguments) visit(a, mult * r.n);
6039
6255
  return;
6040
6256
  }
6041
- if (ts5.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6257
+ if (ts6.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6042
6258
  for (const a of node.arguments) visit(a, mult * templateRows(head));
6043
6259
  return;
6044
6260
  }
@@ -6050,10 +6266,10 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
6050
6266
  const name = calleeName(node.expression);
6051
6267
  if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
6052
6268
  }
6053
- } else if (ts5.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6269
+ } else if (ts6.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6054
6270
  n += templateRows(node) * mult;
6055
6271
  }
6056
- ts5.forEachChild(node, (c) => visit(c, mult));
6272
+ ts6.forEachChild(node, (c) => visit(c, mult));
6057
6273
  };
6058
6274
  visit(sf, 1);
6059
6275
  return { min: n, open };
@@ -6090,7 +6306,8 @@ function scriptNarrowings(before, after, path) {
6090
6306
  if ((path.split("/").pop() ?? path) !== "package.json") return [];
6091
6307
  const scriptsOf = (src) => {
6092
6308
  try {
6093
- const s = JSON.parse(src ?? "{}").scripts ?? {};
6309
+ const pkg = JSON.parse(src ?? "{}");
6310
+ const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
6094
6311
  return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
6095
6312
  } catch {
6096
6313
  return {};
@@ -6135,7 +6352,7 @@ function leavesGlob(dir, dest, policy, ctx) {
6135
6352
  }
6136
6353
  var SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
6137
6354
  var testDeletion = {
6138
- id: RULE7,
6355
+ id: RULE8,
6139
6356
  surface: ["file", "command"],
6140
6357
  certainty: "mechanical",
6141
6358
  run(changes, policy, _view, ctx) {
@@ -6166,7 +6383,7 @@ var testDeletion = {
6166
6383
  if (c.op === "delete" && isTest) {
6167
6384
  if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
6168
6385
  out2.push(
6169
- makeFinding(RULE7, policy, {
6386
+ makeFinding(RULE8, policy, {
6170
6387
  file: c.path,
6171
6388
  message: "A test file was deleted.",
6172
6389
  evidence: c.path,
@@ -6175,7 +6392,7 @@ var testDeletion = {
6175
6392
  );
6176
6393
  } else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
6177
6394
  out2.push(
6178
- makeFinding(RULE7, policy, {
6395
+ makeFinding(RULE8, policy, {
6179
6396
  file: c.path,
6180
6397
  message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
6181
6398
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -6190,7 +6407,7 @@ var testDeletion = {
6190
6407
  const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
6191
6408
  if (isRelocation(removed, before.min - after.min)) continue;
6192
6409
  out2.push(
6193
- makeFinding(RULE7, policy, {
6410
+ makeFinding(RULE8, policy, {
6194
6411
  file: c.path,
6195
6412
  message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
6196
6413
  evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
@@ -6203,7 +6420,7 @@ var testDeletion = {
6203
6420
  const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
6204
6421
  for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
6205
6422
  out2.push(
6206
- makeFinding(RULE7, policy, {
6423
+ makeFinding(RULE8, policy, {
6207
6424
  file: c.path,
6208
6425
  message: `The runner no longer selects ${n.path}: ${n.reason}.`,
6209
6426
  evidence: `${n.path} dropped from the suite by ${c.path}`,
@@ -6213,7 +6430,7 @@ var testDeletion = {
6213
6430
  }
6214
6431
  for (const n of scriptNarrowings(c.before, c.after, c.path)) {
6215
6432
  out2.push(
6216
- makeFinding(RULE7, policy, {
6433
+ makeFinding(RULE8, policy, {
6217
6434
  file: c.path,
6218
6435
  message: `The test script now narrows the suite: ${n.flag} added to scripts.${n.script}.`,
6219
6436
  evidence: `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${JSON.stringify(n.after)}`,
@@ -6301,7 +6518,7 @@ var testDeletion = {
6301
6518
  }
6302
6519
  if (why) {
6303
6520
  out2.push(
6304
- makeFinding(RULE7, policy, {
6521
+ makeFinding(RULE8, policy, {
6305
6522
  message: `Test removed via shell: ${why}.`,
6306
6523
  evidence,
6307
6524
  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 +6533,10 @@ var testDeletion = {
6316
6533
  }
6317
6534
  };
6318
6535
  function pytestPrecedenceFindings(changes, policy, ctx) {
6319
- const order = PYTEST_INI_ORDER;
6320
6536
  const touched = /* @__PURE__ */ new Map();
6321
6537
  for (const c of changes) {
6322
6538
  if (c.kind !== "file" || c.oldPath) continue;
6323
- if (order.includes(c.path)) touched.set(c.path, c);
6539
+ if (PYTEST_INI_ORDER.some((name) => name === c.path)) touched.set(c.path, c);
6324
6540
  }
6325
6541
  if (touched.size === 0) return [];
6326
6542
  const listing = trackedFiles(ctx);
@@ -6328,7 +6544,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6328
6544
  const rootNames = new Set(listing.filter((f) => !f.includes("/")));
6329
6545
  const base = /* @__PURE__ */ new Map();
6330
6546
  const head = /* @__PURE__ */ new Map();
6331
- for (const name of order) {
6547
+ for (const name of PYTEST_INI_ORDER) {
6332
6548
  const c = touched.get(name);
6333
6549
  if (c) {
6334
6550
  base.set(name, c.before);
@@ -6345,7 +6561,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6345
6561
  if (!was || !now || was.path === now.path) return [];
6346
6562
  const samples = runnerSamples(now.path, policy, ctx, "pytest");
6347
6563
  return suiteNarrowings(was.content, now.content, now.path, samples).map(
6348
- (n) => makeFinding(RULE7, policy, {
6564
+ (n) => makeFinding(RULE8, policy, {
6349
6565
  file: now.path,
6350
6566
  message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
6351
6567
  evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
@@ -6355,8 +6571,8 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6355
6571
  }
6356
6572
 
6357
6573
  // src/detectors/test-content-removal.ts
6358
- import ts6 from "typescript";
6359
- var RULE8 = "test-content-removal";
6574
+ import ts7 from "typescript";
6575
+ var RULE9 = "test-content-removal";
6360
6576
  var MIN_REMOVED_LINES = 3;
6361
6577
  var MIN_REMOVED_ROWS = 2;
6362
6578
  var ws = (s) => s.replace(/\s+/g, "");
@@ -6365,13 +6581,13 @@ function eachTables(src, path) {
6365
6581
  let open = false;
6366
6582
  if (langOf(path) !== "js" && langOf(path) !== null) return { rows, open };
6367
6583
  try {
6368
- const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6584
+ const sf = ts7.createSourceFile("spec.ts", src, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
6369
6585
  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)) {
6586
+ 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
6587
  const arg = node.arguments[0];
6372
- if (arg && ts6.isArrayLiteralExpression(arg)) {
6588
+ if (arg && ts7.isArrayLiteralExpression(arg)) {
6373
6589
  for (const el of arg.elements) {
6374
- if (ts6.isSpreadElement(el)) {
6590
+ if (ts7.isSpreadElement(el)) {
6375
6591
  open = true;
6376
6592
  continue;
6377
6593
  }
@@ -6381,7 +6597,7 @@ function eachTables(src, path) {
6381
6597
  }
6382
6598
  } else open = true;
6383
6599
  }
6384
- ts6.forEachChild(node, visit);
6600
+ ts7.forEachChild(node, visit);
6385
6601
  };
6386
6602
  visit(sf);
6387
6603
  } catch {
@@ -6399,7 +6615,7 @@ function significantLinesOrdered(src, path) {
6399
6615
  return out2;
6400
6616
  }
6401
6617
  var testContentRemoval = {
6402
- id: RULE8,
6618
+ id: RULE9,
6403
6619
  surface: ["file"],
6404
6620
  certainty: "mechanical",
6405
6621
  run(changes, policy) {
@@ -6440,7 +6656,7 @@ var testContentRemoval = {
6440
6656
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
6441
6657
  if (goneRows.length >= MIN_REMOVED_ROWS) {
6442
6658
  out2.push(
6443
- makeFinding(RULE8, policy, {
6659
+ makeFinding(RULE9, policy, {
6444
6660
  file: c.path,
6445
6661
  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
6662
  evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -6451,7 +6667,7 @@ var testContentRemoval = {
6451
6667
  }
6452
6668
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
6453
6669
  out2.push(
6454
- makeFinding(RULE8, policy, {
6670
+ makeFinding(RULE9, policy, {
6455
6671
  file: c.path,
6456
6672
  message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
6457
6673
  evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -6465,7 +6681,7 @@ var testContentRemoval = {
6465
6681
  };
6466
6682
 
6467
6683
  // src/detectors/snapshot-rewrite.ts
6468
- var RULE9 = "snapshot-rewrite";
6684
+ var RULE10 = "snapshot-rewrite";
6469
6685
  var RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
6470
6686
  var PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
6471
6687
  var UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
@@ -6482,12 +6698,12 @@ function namesProtectedSnapshot(seg, policy) {
6482
6698
  return null;
6483
6699
  }
6484
6700
  var snapshotRewrite = {
6485
- id: RULE9,
6701
+ id: RULE10,
6486
6702
  surface: ["command", "file"],
6487
6703
  certainty: "mechanical",
6488
6704
  run(changes, policy) {
6489
6705
  const out2 = [];
6490
- const warn = (input) => out2.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
6706
+ const warn = (input) => out2.push(makeFinding(RULE10, policy, { ...input, defaultSeverity: "warn" }));
6491
6707
  for (const c of changes) {
6492
6708
  if (c.kind === "command") {
6493
6709
  for (const seg of segments(c.raw)) {
@@ -6516,7 +6732,7 @@ var snapshotRewrite = {
6516
6732
  if (c.op === "add" || !isSnap && !wasSnap) continue;
6517
6733
  const what = c.op === "delete" ? "deleted" : c.op === "rename" && !isSnap ? `renamed out of the protected snapshot set (to ${c.path})` : "rewritten";
6518
6734
  warn({
6519
- file: c.op === "rename" && !isSnap ? c.oldPath : c.path,
6735
+ file: c.op === "rename" && !isSnap ? c.oldPath ?? c.path : c.path,
6520
6736
  message: `A recorded expected output was ${what}.`,
6521
6737
  evidence: c.op === "rename" ? `${c.oldPath} -> ${c.path}` : c.path,
6522
6738
  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 +6743,7 @@ var snapshotRewrite = {
6527
6743
  };
6528
6744
 
6529
6745
  // src/detectors/snapshot-only.ts
6530
- var RULE10 = "snapshot-only-rewrite";
6746
+ var RULE11 = "snapshot-only-rewrite";
6531
6747
  var COMMIT_VIEWS = ["staged", "range"];
6532
6748
  function isSnapshotChange(c, policy) {
6533
6749
  if (c.kind !== "file") return false;
@@ -6536,7 +6752,7 @@ function isSnapshotChange(c, policy) {
6536
6752
  return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
6537
6753
  }
6538
6754
  var snapshotOnlyRewrite = {
6539
- id: RULE10,
6755
+ id: RULE11,
6540
6756
  surface: ["file"],
6541
6757
  certainty: "mechanical",
6542
6758
  run(changes, policy, view) {
@@ -6546,9 +6762,10 @@ var snapshotOnlyRewrite = {
6546
6762
  const snaps = files.filter((c) => isSnapshotChange(c, policy));
6547
6763
  if (snaps.length === 0 || snaps.length !== files.length) return [];
6548
6764
  const first = snaps[0];
6549
- const names = snaps.map((c) => c.kind === "file" ? c.path : "").filter(Boolean).slice(0, 3).join(", ");
6765
+ if (!first) return [];
6766
+ const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
6550
6767
  return [
6551
- makeFinding(RULE10, policy, {
6768
+ makeFinding(RULE11, policy, {
6552
6769
  file: first.path,
6553
6770
  message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
6554
6771
  evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
@@ -6560,8 +6777,8 @@ var snapshotOnlyRewrite = {
6560
6777
  };
6561
6778
 
6562
6779
  // src/detectors/assertion-weakening.ts
6563
- import ts7 from "typescript";
6564
- var RULE11 = "assertion-weakening";
6780
+ import ts8 from "typescript";
6781
+ var RULE12 = "assertion-weakening";
6565
6782
  var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
6566
6783
  var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
6567
6784
  var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
@@ -6569,55 +6786,55 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
6569
6786
  var compact = (s) => s.replace(/\s+/g, "");
6570
6787
  function literalTitle(node) {
6571
6788
  if (!node) return null;
6572
- if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
6789
+ if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) return node.text;
6573
6790
  return null;
6574
6791
  }
6575
6792
  function scriptKindForPath(path) {
6576
6793
  const lower = path.toLowerCase();
6577
- if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
6794
+ if (lower.endsWith(".tsx")) return ts8.ScriptKind.TSX;
6578
6795
  if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
6579
- return ts7.ScriptKind.TS;
6796
+ return ts8.ScriptKind.TS;
6580
6797
  }
6581
- if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
6582
- return ts7.ScriptKind.JS;
6798
+ if (lower.endsWith(".jsx")) return ts8.ScriptKind.JSX;
6799
+ return ts8.ScriptKind.JS;
6583
6800
  }
6584
6801
  function staticFacts(node) {
6585
6802
  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") {
6803
+ if (node.kind === ts8.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6804
+ if (node.kind === ts8.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6805
+ if (node.kind === ts8.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6806
+ if (ts8.isIdentifier(node) && node.text === "undefined") {
6590
6807
  return { defined: false, truthy: false };
6591
6808
  }
6592
- if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
6593
- if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
6809
+ if (ts8.isVoidExpression(node)) return { defined: false, truthy: false };
6810
+ if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) {
6594
6811
  return { defined: true, truthy: node.text.length > 0 };
6595
6812
  }
6596
- if (ts7.isNumericLiteral(node)) {
6813
+ if (ts8.isNumericLiteral(node)) {
6597
6814
  return { defined: true, truthy: Number(node.text) !== 0 };
6598
6815
  }
6599
- if (ts7.isBigIntLiteral(node)) {
6816
+ if (ts8.isBigIntLiteral(node)) {
6600
6817
  return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
6601
6818
  }
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);
6819
+ if (ts8.isPrefixUnaryExpression(node) && (node.operator === ts8.SyntaxKind.MinusToken || node.operator === ts8.SyntaxKind.PlusToken) && ts8.isNumericLiteral(node.operand)) {
6820
+ const value = Number(node.operand.text) * (node.operator === ts8.SyntaxKind.MinusToken ? -1 : 1);
6604
6821
  return { defined: true, truthy: value !== 0 };
6605
6822
  }
6606
- if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
6823
+ if (ts8.isObjectLiteralExpression(node) || ts8.isArrayLiteralExpression(node) || ts8.isRegularExpressionLiteral(node) || ts8.isArrowFunction(node) || ts8.isFunctionExpression(node) || ts8.isClassExpression(node) || ts8.isNewExpression(node)) {
6607
6824
  return { defined: true, truthy: true };
6608
6825
  }
6609
6826
  return null;
6610
6827
  }
6611
6828
  function expectAssertion(node, sf) {
6612
- if (!ts7.isPropertyAccessExpression(node.expression)) return null;
6829
+ if (!ts8.isPropertyAccessExpression(node.expression)) return null;
6613
6830
  const matcher = node.expression.name.text;
6614
6831
  let receiver = node.expression.expression;
6615
6832
  let polarity = "";
6616
- if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6833
+ if (ts8.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6617
6834
  polarity = "not";
6618
6835
  receiver = receiver.expression;
6619
6836
  }
6620
- if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6837
+ if (!ts8.isCallExpression(receiver) || !ts8.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6621
6838
  return null;
6622
6839
  }
6623
6840
  const subject = compact(receiver.arguments[0].getText(sf));
@@ -6633,41 +6850,41 @@ function expectAssertion(node, sf) {
6633
6850
  };
6634
6851
  }
6635
6852
  function simpleCallName(node) {
6636
- return ts7.isIdentifier(node.expression) ? node.expression.text : null;
6853
+ return ts8.isIdentifier(node.expression) ? node.expression.text : null;
6637
6854
  }
6638
6855
  function callbackFor(node) {
6639
6856
  const cb = node.arguments[1];
6640
- return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
6857
+ return cb && (ts8.isArrowFunction(cb) || ts8.isFunctionExpression(cb)) ? cb : null;
6641
6858
  }
6642
6859
  function blocks(src, path) {
6643
6860
  const out2 = /* @__PURE__ */ new Map();
6644
6861
  if (langOf(path) !== "js") return out2;
6645
6862
  try {
6646
- const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
6863
+ const sf = ts8.createSourceFile(path, src, ts8.ScriptTarget.Latest, true, scriptKindForPath(path));
6647
6864
  const visit = (node, suites) => {
6648
- if (ts7.isCallExpression(node)) {
6865
+ if (ts8.isCallExpression(node)) {
6649
6866
  const call = simpleCallName(node);
6650
6867
  const title = literalTitle(node.arguments[0]);
6651
6868
  const cb = callbackFor(node);
6652
6869
  if (call && SUITE_CALLS.has(call) && title !== null && cb) {
6653
6870
  const nextSuites = [...suites, title];
6654
- ts7.forEachChild(cb, (child) => visit(child, nextSuites));
6871
+ ts8.forEachChild(cb, (child) => visit(child, nextSuites));
6655
6872
  return;
6656
6873
  }
6657
6874
  if (call && TEST_CALLS.has(call) && title !== null && cb) {
6658
6875
  const assertions = [];
6659
6876
  const collect2 = (child) => {
6660
- if (child !== cb && ts7.isCallExpression(child)) {
6877
+ if (child !== cb && ts8.isCallExpression(child)) {
6661
6878
  const nestedCall = simpleCallName(child);
6662
6879
  if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
6663
6880
  return;
6664
6881
  }
6665
6882
  }
6666
- if (ts7.isCallExpression(child)) {
6883
+ if (ts8.isCallExpression(child)) {
6667
6884
  const a = expectAssertion(child, sf);
6668
6885
  if (a) assertions.push(a);
6669
6886
  }
6670
- ts7.forEachChild(child, collect2);
6887
+ ts8.forEachChild(child, collect2);
6671
6888
  };
6672
6889
  collect2(cb);
6673
6890
  const identity2 = [...suites, title].join(" > ");
@@ -6677,7 +6894,7 @@ function blocks(src, path) {
6677
6894
  return;
6678
6895
  }
6679
6896
  }
6680
- ts7.forEachChild(node, (child) => visit(child, suites));
6897
+ ts8.forEachChild(node, (child) => visit(child, suites));
6681
6898
  };
6682
6899
  visit(sf, []);
6683
6900
  } catch {
@@ -6726,7 +6943,7 @@ function findingForBlock(path, before, after, policy) {
6726
6943
  const weakened = directionalWeakening(before.assertions, after.assertions);
6727
6944
  if (weakened) {
6728
6945
  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, {
6946
+ return makeFinding(RULE12, policy, {
6730
6947
  file: path,
6731
6948
  message,
6732
6949
  evidence: `${weakened.before.text} -> ${weakened.after.text}`,
@@ -6735,7 +6952,7 @@ function findingForBlock(path, before, after, policy) {
6735
6952
  }
6736
6953
  const removed = pureRemoval(before.assertions, after.assertions);
6737
6954
  if (removed) {
6738
- return makeFinding(RULE11, policy, {
6955
+ return makeFinding(RULE12, policy, {
6739
6956
  file: path,
6740
6957
  message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
6741
6958
  evidence: removed.text,
@@ -6745,7 +6962,7 @@ function findingForBlock(path, before, after, policy) {
6745
6962
  return null;
6746
6963
  }
6747
6964
  var assertionWeakening = {
6748
- id: RULE11,
6965
+ id: RULE12,
6749
6966
  surface: ["file"],
6750
6967
  certainty: "heuristic",
6751
6968
  run(changes, policy) {
@@ -6771,6 +6988,7 @@ var assertionWeakening = {
6771
6988
  var allDetectors = [
6772
6989
  noVerify,
6773
6990
  tsAnyCast,
6991
+ tsCastGrowth,
6774
6992
  lintSuppression,
6775
6993
  testSkip,
6776
6994
  coverageLowering,
@@ -6812,14 +7030,15 @@ function evaluate(changes, policy, detectors = allDetectors, view, ctx) {
6812
7030
  process.stderr.write(`tamperward: detector "${d.id}" errored: ${String(e)}
6813
7031
  `);
6814
7032
  if (view === "staged" || view === "worktree" || view === "range") {
6815
- out2.push({
7033
+ const failed = {
6816
7034
  rule: "detector-error",
6817
7035
  severity: "block",
6818
7036
  message: `Detector "${d.id}" failed to run; the verdict is incomplete.`,
6819
7037
  evidence: String(e).slice(0, 200),
6820
7038
  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
7039
  signoff: { required: true, command: `tamperward allow detector-error --reason "..."` }
6822
- });
7040
+ };
7041
+ out2.push(failed);
6823
7042
  }
6824
7043
  }
6825
7044
  }
@@ -6860,7 +7079,7 @@ function normalizeVersion(v, where = POLICY_FILE) {
6860
7079
  return v;
6861
7080
  }
6862
7081
  var SEVERITIES = ["block", "warn"];
6863
- var isSeverity = (v) => SEVERITIES.includes(v);
7082
+ var isSeverity = (v) => SEVERITIES.some((s) => s === v);
6864
7083
  var isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
6865
7084
  var isMapping = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6866
7085
  var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "protected", "rules", "ignore", "signoff", "verify"]);
@@ -6874,75 +7093,105 @@ function validate(r, where) {
6874
7093
  throw new PolicyError(`${where}: ${msg}`);
6875
7094
  }
6876
7095
  const show = (v) => JSON.stringify(v) ?? String(v);
7096
+ const out2 = {};
7097
+ if (r.version !== void 0) out2.version = r.version;
6877
7098
  const unknown = Object.keys(r).filter((k) => !TOP_LEVEL_KEYS.has(k));
6878
7099
  if (unknown.length) {
6879
7100
  bad(`unknown top-level key${unknown.length === 1 ? "" : "s"} ${unknown.map((k) => JSON.stringify(k)).join(", ")} (expected one of ${[...TOP_LEVEL_KEYS].join(", ")})`);
6880
7101
  }
6881
7102
  if (r.rules !== void 0) {
6882
7103
  if (!isMapping(r.rules)) bad(`rules must be a mapping of rule name to { severity, enabled, exclude }, got ${show(r.rules)}`);
7104
+ const rules = {};
6883
7105
  for (const [name, cfg] of Object.entries(r.rules)) {
6884
7106
  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)}`);
7107
+ const { severity, enabled, exclude } = cfg;
7108
+ if (severity !== void 0 && !isSeverity(severity)) {
7109
+ bad(`rules.${name}.severity must be "block" or "warn", got ${show(severity)}`);
6887
7110
  }
6888
- if (cfg.enabled !== void 0 && typeof cfg.enabled !== "boolean") {
6889
- bad(`rules.${name}.enabled must be true or false, got ${show(cfg.enabled)}`);
7111
+ if (enabled !== void 0 && typeof enabled !== "boolean") {
7112
+ bad(`rules.${name}.enabled must be true or false, got ${show(enabled)}`);
6890
7113
  }
6891
- if (cfg.exclude !== void 0 && !isStringList(cfg.exclude)) {
6892
- bad(`rules.${name}.exclude must be a list of globs, got ${show(cfg.exclude)}`);
7114
+ if (exclude !== void 0 && !isStringList(exclude)) {
7115
+ bad(`rules.${name}.exclude must be a list of globs, got ${show(exclude)}`);
6893
7116
  }
7117
+ rules[name] = {
7118
+ ...severity !== void 0 ? { severity } : {},
7119
+ ...enabled !== void 0 ? { enabled } : {},
7120
+ ...exclude !== void 0 ? { exclude } : {}
7121
+ };
6894
7122
  }
7123
+ out2.rules = rules;
7124
+ }
7125
+ if (r.ignore !== void 0) {
7126
+ if (!isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
7127
+ out2.ignore = r.ignore;
6895
7128
  }
6896
- if (r.ignore !== void 0 && !isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
6897
7129
  if (r.protected !== void 0) {
6898
7130
  if (!isMapping(r.protected)) bad(`protected must be a mapping of category to a list of globs, got ${show(r.protected)}`);
7131
+ const categories = {};
6899
7132
  for (const [cat, globs4] of Object.entries(r.protected)) {
6900
7133
  if (!isStringList(globs4)) bad(`protected.${cat} must be a list of globs, got ${show(globs4)}`);
7134
+ categories[cat] = globs4;
6901
7135
  }
7136
+ out2.protected = categories;
6902
7137
  }
6903
7138
  if (r.signoff !== void 0) {
6904
7139
  if (!isMapping(r.signoff)) bad(`signoff must be a mapping, got ${show(r.signoff)}`);
7140
+ const signoff = {};
6905
7141
  for (const key2 of ["required_for", "requiredFor"]) {
6906
7142
  const v = r.signoff[key2];
6907
- if (v !== void 0 && !(Array.isArray(v) && v.every(isSeverity))) {
7143
+ if (v === void 0) continue;
7144
+ if (!(Array.isArray(v) && v.every(isSeverity))) {
6908
7145
  bad(`signoff.${key2} must be a list of "block" / "warn", got ${show(v)}`);
6909
7146
  }
7147
+ signoff[key2] = v;
6910
7148
  }
6911
7149
  const ledger = r.signoff.ledger;
6912
7150
  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)}`);
7151
+ if (typeof ledger === "string") {
7152
+ if (!ledgerInsideRepo(ledger)) bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
7153
+ signoff.ledger = ledger;
6915
7154
  }
7155
+ out2.signoff = signoff;
6916
7156
  }
6917
7157
  if (r.verify !== void 0) {
6918
7158
  if (!isMapping(r.verify)) bad(`verify must be a mapping like { command: "npm test" }, got ${show(r.verify)}`);
6919
7159
  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)}`);
7160
+ const { command, budget, inputs, backend, image } = v;
7161
+ if (command !== void 0 && typeof command !== "string") bad(`verify.command must be a string, got ${show(command)}`);
7162
+ if (budget !== void 0 && !(typeof budget === "number" && Number.isFinite(budget) && budget > 0)) {
7163
+ bad(`verify.budget must be a positive number of seconds, got ${show(budget)}`);
6923
7164
  }
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)}`);
7165
+ if (inputs !== void 0 && !isStringList(inputs)) bad(`verify.inputs must be a list of globs, got ${show(inputs)}`);
7166
+ if (backend !== void 0 && backend !== "local" && backend !== "container") {
7167
+ bad(`verify.backend must be "local" or "container", got ${show(backend)}`);
6927
7168
  }
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)}`);
7169
+ if (image !== void 0 && typeof image !== "string") {
7170
+ bad(`verify.image must be an immutable container image reference, got ${show(image)}`);
6930
7171
  }
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)}`);
7172
+ if ((backend ?? "local") === "container") {
7173
+ if (typeof image !== "string") bad('verify.image is required when verify.backend is "container"');
7174
+ if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(image)) {
7175
+ bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(image)}`);
6936
7176
  }
6937
- } else if (v.image !== void 0) {
7177
+ } else if (image !== void 0) {
6938
7178
  bad('verify.image is only valid when verify.backend is "container"');
6939
7179
  }
7180
+ out2.verify = {
7181
+ ...command !== void 0 ? { command } : {},
7182
+ ...budget !== void 0 ? { budget } : {},
7183
+ ...inputs !== void 0 ? { inputs } : {},
7184
+ ...backend !== void 0 ? { backend } : {},
7185
+ ...image !== void 0 ? { image } : {}
7186
+ };
6940
7187
  }
7188
+ return out2;
6941
7189
  }
6942
7190
  function parsePolicy(raw, where = POLICY_FILE) {
6943
- const r = raw ?? {};
6944
- const version = normalizeVersion(r.version, where);
6945
- validate(r, where);
7191
+ const doc = raw ?? {};
7192
+ if (!isMapping(doc)) throw new PolicyError(`${where} is not a policy mapping`);
7193
+ const version = normalizeVersion(doc.version, where);
7194
+ const r = validate(doc, where);
6946
7195
  const base = defaultPolicy(version);
6947
7196
  const globs4 = (list2) => list2?.map(normalizeGlob);
6948
7197
  const userProtected = r.protected ? Object.fromEntries(Object.entries(r.protected).map(([cat, list2]) => [cat, globs4(list2) ?? []])) : void 0;
@@ -6981,10 +7230,7 @@ function parseOrThrow(src, where) {
6981
7230
  try {
6982
7231
  raw = parse2(src);
6983
7232
  } 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`);
7233
+ throw new PolicyError(`${where} is not valid YAML: ${errorMessage(e)}`);
6988
7234
  }
6989
7235
  return parsePolicy(raw, where);
6990
7236
  }
@@ -7004,6 +7250,14 @@ import { createHash as createHash2 } from "node:crypto";
7004
7250
  import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "node:fs";
7005
7251
  import { dirname as dirname2, join as join8 } from "node:path";
7006
7252
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
7253
+ function ledgerEntryFrom(value) {
7254
+ if (!isRecord(value)) return null;
7255
+ const { rule, file, fingerprint: fingerprint2, reason, recordedAt, expiresAt } = value;
7256
+ if (typeof rule !== "string" || typeof fingerprint2 !== "string" || typeof reason !== "string") return null;
7257
+ if (typeof recordedAt !== "number" || typeof expiresAt !== "number") return null;
7258
+ if (file !== void 0 && typeof file !== "string") return null;
7259
+ return { rule, ...file !== void 0 ? { file } : {}, fingerprint: fingerprint2, reason, recordedAt, expiresAt };
7260
+ }
7007
7261
  function fingerprint(rule, file, evidence) {
7008
7262
  return createHash2("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
7009
7263
  }
@@ -7022,8 +7276,8 @@ function readLedger(cwd, policy) {
7022
7276
  for (const line of readFileSync6(p, "utf8").split("\n")) {
7023
7277
  if (!line.trim()) continue;
7024
7278
  try {
7025
- const e = JSON.parse(line);
7026
- if (e && typeof e.fingerprint === "string") out2.push(e);
7279
+ const e = ledgerEntryFrom(JSON.parse(line));
7280
+ if (e) out2.push(e);
7027
7281
  } catch {
7028
7282
  }
7029
7283
  }
@@ -7059,9 +7313,10 @@ function oobToken(want, oob, head) {
7059
7313
  }
7060
7314
  const [rule, sha2] = [t.slice(0, at), t.slice(at + 1)];
7061
7315
  if (rule !== want) continue;
7062
- const normalizedHead = head?.trim().toLowerCase();
7316
+ if (!head) return t;
7317
+ const normalizedHead = head.trim().toLowerCase();
7063
7318
  const normalizedSha = sha2.trim().toLowerCase();
7064
- if (!head || /^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
7319
+ if (/^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
7065
7320
  }
7066
7321
  return null;
7067
7322
  }
@@ -7293,7 +7548,7 @@ function isGitHubActions(env = process.env) {
7293
7548
  // src/cli/report.ts
7294
7549
  var FORMATS = ["auto", "text", "json", "github"];
7295
7550
  function isFormat(s) {
7296
- return FORMATS.includes(s);
7551
+ return FORMATS.some((f) => f === s);
7297
7552
  }
7298
7553
  function resolveFormat(input, env = process.env) {
7299
7554
  if (input.json) return "json";
@@ -7471,6 +7726,17 @@ import { createHash as createHash3 } from "node:crypto";
7471
7726
  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
7727
  import { isAbsolute as isAbsolute7, join as join10 } from "node:path";
7473
7728
  import { execFileSync as execFileSync5 } from "node:child_process";
7729
+ function ptreeFrom(value) {
7730
+ if (!isRecord(value)) return null;
7731
+ const out2 = {};
7732
+ for (const [path, entry] of Object.entries(value)) {
7733
+ if (!isRecord(entry)) return null;
7734
+ const { hash, mode, size, mtimeMs } = entry;
7735
+ if (typeof hash !== "string" || typeof mode !== "number" || typeof size !== "number" || typeof mtimeMs !== "number") return null;
7736
+ out2[path] = { hash, mode, size, mtimeMs };
7737
+ }
7738
+ return out2;
7739
+ }
7474
7740
  var UNSAFE2 = /[^A-Za-z0-9._-]/g;
7475
7741
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
7476
7742
  function gitDir2(cwd) {
@@ -7562,7 +7828,7 @@ function driftBetween(expected, current) {
7562
7828
  function loadTree(p) {
7563
7829
  if (!p || !existsSync7(p)) return null;
7564
7830
  try {
7565
- return JSON.parse(readFileSync8(p, "utf8"));
7831
+ return ptreeFrom(JSON.parse(readFileSync8(p, "utf8")));
7566
7832
  } catch {
7567
7833
  return null;
7568
7834
  }
@@ -7622,8 +7888,25 @@ function watcherHealthPath(log) {
7622
7888
  function readWatcherHealth(log) {
7623
7889
  try {
7624
7890
  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;
7891
+ if (!isRecord(value)) return null;
7892
+ const { state, backend, pid, started_at } = value;
7893
+ if (value.version !== 1 || typeof pid !== "number" || typeof started_at !== "string") return null;
7894
+ if (state !== "healthy" && state !== "degraded" && state !== "stopped") return null;
7895
+ return {
7896
+ version: 1,
7897
+ state,
7898
+ backend: backend === "initializing" || backend === "recursive" ? backend : "fallback",
7899
+ pid,
7900
+ started_at,
7901
+ stopped_at: nullableString(value.stopped_at) ?? null,
7902
+ watched_dirs: finiteNumber(value.watched_dirs) ?? 0,
7903
+ last_append_at: nullableString(value.last_append_at) ?? null,
7904
+ event_count: finiteNumber(value.event_count) ?? 0,
7905
+ dropped_events: finiteNumber(value.dropped_events) ?? 0,
7906
+ error_count: finiteNumber(value.error_count) ?? 0,
7907
+ last_error: nullableString(value.last_error) ?? null,
7908
+ log: stringOrUndefined(value.log) ?? log
7909
+ };
7627
7910
  } catch {
7628
7911
  return null;
7629
7912
  }
@@ -7634,7 +7917,7 @@ function pidAlive(pid) {
7634
7917
  process.kill(pid, 0);
7635
7918
  return true;
7636
7919
  } catch (e) {
7637
- return e.code === "EPERM";
7920
+ return errnoCode(e) === "EPERM";
7638
7921
  }
7639
7922
  }
7640
7923
  function watcherTelemetry(log) {
@@ -7838,7 +8121,23 @@ function runWatch(args) {
7838
8121
 
7839
8122
  // src/detectors/fs-events.ts
7840
8123
  import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7841
- var RULE12 = "transient-protected-mutation";
8124
+ function fsEventFrom(line) {
8125
+ let value;
8126
+ try {
8127
+ value = JSON.parse(line);
8128
+ } catch {
8129
+ return null;
8130
+ }
8131
+ if (!isRecord(value)) return null;
8132
+ const { ts: ts9, path, kind } = value;
8133
+ if (typeof ts9 !== "string" || typeof path !== "string" || kind !== "change" && kind !== "rename") return null;
8134
+ const mode = nullableNumber(value.mode);
8135
+ const size = nullableNumber(value.size);
8136
+ const hash = nullableString(value.hash);
8137
+ if (mode === void 0 || size === void 0 || hash === void 0) return null;
8138
+ return { ts: ts9, path, kind, mode, size, hash };
8139
+ }
8140
+ var RULE13 = "transient-protected-mutation";
7842
8141
  var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
7843
8142
  var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
7844
8143
  var DEFAULT_EVENT_READ_IO = {
@@ -7909,11 +8208,9 @@ function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_E
7909
8208
  const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
7910
8209
  for (const line of complete.split("\n")) {
7911
8210
  if (!line.trim()) continue;
7912
- try {
7913
- events.push(JSON.parse(line));
7914
- } catch {
7915
- malformedLines++;
7916
- }
8211
+ const event = fsEventFrom(line);
8212
+ if (event) events.push(event);
8213
+ else malformedLines++;
7917
8214
  }
7918
8215
  const newOffset = start + lastNewline + 1;
7919
8216
  return {
@@ -8033,7 +8330,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
8033
8330
  };
8034
8331
  }
8035
8332
  function transientFindings(events, persistentPaths, policy, finalHash) {
8036
- if (!isEnabled(RULE12, policy)) return [];
8333
+ if (!isEnabled(RULE13, policy)) return [];
8037
8334
  const byPath = /* @__PURE__ */ new Map();
8038
8335
  for (const e of events) {
8039
8336
  if (!isProtected(e.path, policy)) continue;
@@ -8053,7 +8350,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
8053
8350
  const modeChurn = modes.size > 1;
8054
8351
  if (!contentChurn && !modeChurn) continue;
8055
8352
  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, {
8353
+ const f = makeFinding(RULE13, policy, {
8057
8354
  file: path,
8058
8355
  message: `Transient protected mutation: ${what}, leaving no net diff.`,
8059
8356
  evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
@@ -8087,10 +8384,16 @@ function parseInput(raw) {
8087
8384
  } catch (e) {
8088
8385
  throw new HookInputError(`the hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
8089
8386
  }
8090
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8387
+ if (!isRecord(parsed)) {
8091
8388
  throw new HookInputError(`the hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`);
8092
8389
  }
8093
- return parsed;
8390
+ return {
8391
+ ...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
8392
+ ...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
8393
+ ...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
8394
+ ...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {},
8395
+ ...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}
8396
+ };
8094
8397
  }
8095
8398
  function recordDenylog(blocks2) {
8096
8399
  const log = process.env.TAMPERWARD_DENYLOG;
@@ -8733,6 +9036,9 @@ function missingTools(matcher) {
8733
9036
  if (have === null) return [];
8734
9037
  return PRE_MATCHER.split("|").filter((t) => !have.has(t));
8735
9038
  }
9039
+ function isHooksShape(hooks) {
9040
+ return hooksShapeError(hooks) === null;
9041
+ }
8736
9042
  function hooksShapeError(hooks) {
8737
9043
  if (hooks === void 0 || hooks === null) return null;
8738
9044
  if (!isPlainObject(hooks)) return "hooks is not an object";
@@ -8755,7 +9061,7 @@ function verifierCandidates(cwd) {
8755
9061
  };
8756
9062
  try {
8757
9063
  const pkg = JSON.parse(readFileSync11(join12(cwd, "package.json"), "utf8"));
8758
- const test = pkg.scripts?.test;
9064
+ const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
8759
9065
  if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
8760
9066
  } catch {
8761
9067
  }
@@ -8854,14 +9160,14 @@ function planClaudeHooks(cwd) {
8854
9160
  detail: "exists but is not valid JSON \u2014 fix it, then re-run init (refusing to overwrite)"
8855
9161
  };
8856
9162
  }
8857
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
9163
+ if (!isRecord(parsed)) {
8858
9164
  return { item: "agent", path: rel, status: "error", detail: "exists but is not a JSON object \u2014 refusing to overwrite" };
8859
9165
  }
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)` };
9166
+ const shapeError = hooksShapeError(parsed.hooks);
9167
+ if (shapeError || !isHooksShape(parsed.hooks)) {
9168
+ 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)` };
9169
+ }
9170
+ settings = { ...parsed, hooks: parsed.hooks ?? void 0 };
8865
9171
  }
8866
9172
  const hooks = settings.hooks ??= {};
8867
9173
  const entriesWith = (arr, needle) => (arr ?? []).filter((m) => (m.hooks ?? []).some((h) => needle.test(String(h.command ?? ""))));
@@ -9085,7 +9391,7 @@ function planCodeowners(cwd) {
9085
9391
  const path = join12(cwd, existingRel ?? rel);
9086
9392
  const existing = existingRel ? readFileSync11(path, "utf8") : null;
9087
9393
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
9088
- if (missing.length === 0) {
9394
+ if (existingRel !== void 0 && missing.length === 0) {
9089
9395
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
9090
9396
  }
9091
9397
  const inferred = inferOwner(cwd);
@@ -9711,14 +10017,15 @@ function decodeUtf8Tail(tail) {
9711
10017
  while (start < tail.length && (tail[start] & 192) === 128) start++;
9712
10018
  return tail.subarray(start).toString("utf8");
9713
10019
  }
9714
- function streamFromRaw(raw) {
10020
+ function streamFromRaw(value) {
10021
+ const raw = isRecord(value) ? value : {};
9715
10022
  let tail = Buffer.alloc(0);
9716
10023
  try {
9717
- tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
10024
+ tail = Buffer.from(typeof raw.tail_b64 === "string" ? raw.tail_b64 : "", "base64");
9718
10025
  } catch {
9719
10026
  tail = Buffer.alloc(0);
9720
10027
  }
9721
- const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
10028
+ const captured = typeof raw.captured_bytes === "number" && Number.isFinite(raw.captured_bytes) ? Math.max(0, raw.captured_bytes) : tail.length;
9722
10029
  return {
9723
10030
  captured_bytes: captured,
9724
10031
  retained_bytes: tail.length,
@@ -9729,7 +10036,7 @@ function streamFromRaw(raw) {
9729
10036
  function scrubDiagnosticText(input) {
9730
10037
  let out2 = "";
9731
10038
  for (const ch of input) {
9732
- const cp = ch.codePointAt(0);
10039
+ const cp = ch.codePointAt(0) ?? 0;
9733
10040
  if (ch === "\n" || ch === " ") {
9734
10041
  out2 += ch;
9735
10042
  } else if (cp === 13) {
@@ -9932,7 +10239,7 @@ function parseCapturedSupervisorResult(stdoutText) {
9932
10239
  } catch {
9933
10240
  return null;
9934
10241
  }
9935
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
10242
+ if (!isRecord(raw)) return null;
9936
10243
  const diagnostics = {
9937
10244
  stdout: streamFromRaw(raw.stdout),
9938
10245
  stderr: streamFromRaw(raw.stderr)
@@ -9983,7 +10290,7 @@ function runCapturedProcessSync(executable, args, opts) {
9983
10290
  const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
9984
10291
  const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
9985
10292
  if (!parsed) {
9986
- const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
10293
+ const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
9987
10294
  return {
9988
10295
  exit: null,
9989
10296
  signal: supervisor.signal ? String(supervisor.signal) : null,
@@ -10078,16 +10385,16 @@ function dockerAvailable(enginePath, host) {
10078
10385
  }
10079
10386
  }
10080
10387
  function declaredImageVolumePaths(metadata) {
10081
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
10388
+ if (!isRecord(metadata)) {
10082
10389
  throw new Error("image metadata must be a Docker image-inspect object");
10083
10390
  }
10084
10391
  const config = metadata.Config;
10085
- if (!config || typeof config !== "object" || Array.isArray(config)) {
10392
+ if (!isRecord(config)) {
10086
10393
  throw new Error("image metadata Config must be an object");
10087
10394
  }
10088
10395
  const volumes = config.Volumes;
10089
10396
  if (volumes === void 0 || volumes === null) return [];
10090
- if (typeof volumes !== "object" || Array.isArray(volumes)) {
10397
+ if (!isRecord(volumes)) {
10091
10398
  throw new Error("image metadata Config.Volumes must be a mapping or null");
10092
10399
  }
10093
10400
  return Object.keys(volumes).sort();
@@ -10591,8 +10898,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10591
10898
  const pending = linkParts(target);
10592
10899
  const seen = /* @__PURE__ */ new Set();
10593
10900
  let hops = 0;
10594
- while (pending.length) {
10595
- const part = pending.shift();
10901
+ for (let part = pending.shift(); part !== void 0; part = pending.shift()) {
10596
10902
  if (part === "..") {
10597
10903
  if (!stack.length) throw linkEscape(label, target);
10598
10904
  stack.pop();
@@ -10604,7 +10910,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10604
10910
  stack = [];
10605
10911
  continue;
10606
10912
  }
10607
- const base = domain === "tree" ? root : dependencyRoot;
10913
+ const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
10608
10914
  const current = join16(base, ...stack);
10609
10915
  let st;
10610
10916
  try {
@@ -11623,7 +11929,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11623
11929
  }
11624
11930
  }
11625
11931
  );
11626
- const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
11932
+ const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
11627
11933
  const completedNormally = !supervisorTimedOut && !supervisor.error && supervisor.status === 0 && supervisor.signal == null;
11628
11934
  if (!completedNormally) {
11629
11935
  return {
@@ -11674,7 +11980,7 @@ function pidAlive2(pid) {
11674
11980
  process.kill(pid, 0);
11675
11981
  return true;
11676
11982
  } catch (e) {
11677
- return e.code === "EPERM";
11983
+ return errnoCode(e) === "EPERM";
11678
11984
  }
11679
11985
  }
11680
11986
  function supervisedObserverLog(cwd) {
@@ -11717,7 +12023,7 @@ function stopObserverProcess(observer) {
11717
12023
  waitMs(100);
11718
12024
  const beforeStop = watcherTelemetry(observer.log);
11719
12025
  const pid = observer.pid;
11720
- if (pidAlive2(pid)) {
12026
+ if (pid !== null && pidAlive2(pid)) {
11721
12027
  try {
11722
12028
  process.kill(pid, "SIGTERM");
11723
12029
  } catch {
@@ -12191,7 +12497,7 @@ function policyFor(opts, cwd) {
12191
12497
  return atBase ?? defaultPolicy();
12192
12498
  }
12193
12499
  function asMapping(value) {
12194
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
12500
+ return isRecord(value) ? value : null;
12195
12501
  }
12196
12502
  function wiringState(status, authority) {
12197
12503
  if (status === "ok") return "OK";
@@ -12512,8 +12818,8 @@ function githubApi(cwd, endpoint) {
12512
12818
  );
12513
12819
  return JSON.parse(stdout);
12514
12820
  } catch (e) {
12515
- const x = e;
12516
- const detail = x.stderr ? String(x.stderr).replace(/\s+/g, " ").trim() : x.message;
12821
+ const failure = execFailure(e);
12822
+ const detail = failure.stderr ? failure.stderr.replace(/\s+/g, " ").trim() : failure.message;
12517
12823
  throw new Error(
12518
12824
  "GitHub API " + endpoint + " failed: " + (detail || "unknown error")
12519
12825
  );
@@ -12824,7 +13130,7 @@ function summarizeTraceRuns(opts) {
12824
13130
  for (const item of run) {
12825
13131
  const rel = normalRepoPath(opts.root, item.path);
12826
13132
  const isTracked = rel !== null && opts.tracked.has(rel);
12827
- const keyPath = isTracked ? rel : rel === null ? resolve9(opts.root, item.path) : null;
13133
+ const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve9(opts.root, item.path);
12828
13134
  if (keyPath === null) continue;
12829
13135
  const target = isTracked ? repo : external;
12830
13136
  const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
@@ -12909,7 +13215,7 @@ function materializeBase(base, cwd, dest) {
12909
13215
  }
12910
13216
  function traceFiles(prefix) {
12911
13217
  const dir = resolve9(prefix, "..");
12912
- const base = prefix.split(sep3).at(-1);
13218
+ const base = prefix.split(sep3).at(-1) ?? prefix;
12913
13219
  return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join19(dir, name)).sort();
12914
13220
  }
12915
13221
  function rewriteTraceRoot(accesses, actualRoot) {