tamperward 2.18.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -216,6 +216,10 @@ function defaultPolicy(version = 1) {
216
216
  // broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
217
217
  // block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
218
218
  "ts-any-launder": { severity: "warn" },
219
+ // net growth of ordinary `as T` / `<T>x` / `x!` assertions in non-test source (#383).
220
+ // Fires on a large share of legitimate TypeScript mainline diffs (measured in
221
+ // harness/fp-study/CAST-GROWTH-CORPUS.md), so it is a review prompt, not a gate.
222
+ "ts-cast-growth": { severity: "warn" },
219
223
  "lint-suppression": { severity: "block" },
220
224
  "coverage-lowering": { severity: "block" },
221
225
  "ci-tampering": { severity: "block" },
@@ -244,6 +248,45 @@ function defaultPolicy(version = 1) {
244
248
  return p;
245
249
  }
246
250
 
251
+ // src/narrow.ts
252
+ function isRecord(v) {
253
+ return typeof v === "object" && v !== null && !Array.isArray(v);
254
+ }
255
+ function errnoCode(e) {
256
+ const code2 = isRecord(e) ? e.code : void 0;
257
+ return typeof code2 === "string" ? code2 : void 0;
258
+ }
259
+ function errorMessage(e) {
260
+ return e instanceof Error ? e.message : String(e);
261
+ }
262
+ function textOf(v) {
263
+ if (typeof v === "string") return v;
264
+ if (Buffer.isBuffer(v)) return v.toString("utf8");
265
+ return "";
266
+ }
267
+ function execFailure(e) {
268
+ const r = isRecord(e) ? e : {};
269
+ return {
270
+ stdout: textOf(r.stdout),
271
+ stderr: textOf(r.stderr),
272
+ status: typeof r.status === "number" ? r.status : null,
273
+ code: errnoCode(e),
274
+ message: errorMessage(e)
275
+ };
276
+ }
277
+ function finiteNumber(v) {
278
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
279
+ }
280
+ function stringOrUndefined(v) {
281
+ return typeof v === "string" ? v : void 0;
282
+ }
283
+ function nullableString(v) {
284
+ return v === null || typeof v === "string" ? v : void 0;
285
+ }
286
+ function nullableNumber(v) {
287
+ return v === null || typeof v === "number" && Number.isFinite(v) ? v : void 0;
288
+ }
289
+
247
290
  // src/disk.ts
248
291
  var READ_CAP = 64 * 1024 * 1024;
249
292
  var NO_META = { mode: 0, size: 0, mtimeMs: 0 };
@@ -251,7 +294,7 @@ function errText(e) {
251
294
  return e instanceof Error ? e.message : String(e);
252
295
  }
253
296
  function errCode(e) {
254
- return String(e?.code ?? "");
297
+ return errnoCode(e) ?? "";
255
298
  }
256
299
  function irregularName(st) {
257
300
  if (st.isFIFO()) return "fifo";
@@ -341,7 +384,7 @@ function inspectResolved(abs2) {
341
384
  }
342
385
  return inspectPath(real);
343
386
  }
344
- function textOf(e) {
387
+ function textOf2(e) {
345
388
  return e.content == null ? null : e.content.toString("utf8");
346
389
  }
347
390
  function describe(e, shown) {
@@ -675,9 +718,9 @@ function git(args, cwd) {
675
718
  env: trustedGitEnv()
676
719
  });
677
720
  } catch (e) {
678
- const err3 = e;
679
- const detail = String(err3.stderr ?? "").trim();
680
- throw new Error(detail ? `git ${args[0]}: ${detail}` : err3.message ?? `git ${args[0]} failed`);
721
+ const failure = execFailure(e);
722
+ const detail = failure.stderr.trim();
723
+ throw new Error(detail ? `git ${args[0]}: ${detail}` : failure.message || `git ${args[0]} failed`);
681
724
  }
682
725
  }
683
726
  function blobAt(rev, path, cwd) {
@@ -688,7 +731,7 @@ function blobAt(rev, path, cwd) {
688
731
  }
689
732
  }
690
733
  function fromDisk(path, cwd) {
691
- return textOf(inspectRel(cwd ?? process.cwd(), path));
734
+ return textOf2(inspectRel(cwd ?? process.cwd(), path));
692
735
  }
693
736
  function enrich(c, beforeReader, afterReader) {
694
737
  if (c.kind !== "file") return c;
@@ -807,7 +850,7 @@ function ignoredAdds(opts = {}, keep, exclude) {
807
850
  }
808
851
  function diffWorktreeWithUntracked(opts = {}, keepIgnored) {
809
852
  const tracked = diffWorktree(opts);
810
- const seen = new Set(tracked.filter((c) => c.kind === "file").map((c) => c.path));
853
+ const seen = new Set(tracked.flatMap((c) => c.kind === "file" ? [c.path] : []));
811
854
  const untracked = untrackedAdds(opts, void 0, seen);
812
855
  for (const c of untracked) if (c.kind === "file") seen.add(c.path);
813
856
  const ignored = keepIgnored ? ignoredAdds(opts, keepIgnored, seen) : [];
@@ -832,7 +875,7 @@ function asStr(v) {
832
875
  return typeof v === "string" ? v : "";
833
876
  }
834
877
  function readDisk(path) {
835
- return textOf(inspectResolved(path));
878
+ return textOf2(inspectResolved(path));
836
879
  }
837
880
  function abs(path, cwd) {
838
881
  return resolve(cwd, path);
@@ -865,11 +908,11 @@ function synthFileChange(displayPath, before, after) {
865
908
  maxBuffer: 256 * 1024 * 1024
866
909
  });
867
910
  } catch (e) {
868
- const err3 = e;
869
- if (err3.status !== 1) {
870
- throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err3.code ?? err3.message ?? String(e)}`);
911
+ const failure = execFailure(e);
912
+ if (failure.status !== 1) {
913
+ throw new Error(`cannot diff the incoming edit to ${displayPath}: ${failure.code ?? failure.message}`);
871
914
  }
872
- raw = err3.stdout ? String(err3.stdout) : "";
915
+ raw = failure.stdout;
873
916
  }
874
917
  } finally {
875
918
  rmSync(dir, { recursive: true, force: true });
@@ -905,8 +948,8 @@ function changesFromClaudeHook(input, cwd) {
905
948
  let after = before;
906
949
  const edits = Array.isArray(ti.edits) ? ti.edits : [];
907
950
  for (const raw of edits) {
908
- const ed = raw;
909
- after = applyEdit(after, ed.old_string ?? "", ed.new_string ?? "");
951
+ const ed = isRecord(raw) ? raw : {};
952
+ after = applyEdit(after, asStr(ed.old_string), asStr(ed.new_string));
910
953
  }
911
954
  return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
912
955
  }
@@ -1110,7 +1153,7 @@ var noVerify = {
1110
1153
  };
1111
1154
 
1112
1155
  // src/detectors/ts-any-cast.ts
1113
- import ts from "typescript";
1156
+ import ts2 from "typescript";
1114
1157
 
1115
1158
  // src/diff/select.ts
1116
1159
  function addedLines(c) {
@@ -1190,38 +1233,168 @@ function insideStringLiteral(line, idx, lang) {
1190
1233
  return quote !== null;
1191
1234
  }
1192
1235
 
1236
+ // src/detectors/ts-cast-growth.ts
1237
+ import ts from "typescript";
1238
+ var RULE2 = "ts-cast-growth";
1239
+ var DECLARATION = /\.d\.(?:ts|mts|cts)$/;
1240
+ var OUT_OF_BUDGET_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|generated|__generated__)\/|\.(?:generated|gen)\.[cm]?[jt]sx?$/;
1241
+ var GENERATED_HEADER = /@generated\b|\bAUTO-?GENERATED\b|\bDO NOT EDIT\b/i;
1242
+ var HEADER_LINES = 20;
1243
+ function scriptKind(path) {
1244
+ const lower = path.toLowerCase();
1245
+ if (lower.endsWith(".tsx")) return ts.ScriptKind.TSX;
1246
+ if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) return ts.ScriptKind.TS;
1247
+ if (lower.endsWith(".jsx")) return ts.ScriptKind.JSX;
1248
+ return ts.ScriptKind.JS;
1249
+ }
1250
+ function outOfBudget(path, before) {
1251
+ if (DECLARATION.test(path) || OUT_OF_BUDGET_PATH.test(path)) return true;
1252
+ return before != null && GENERATED_HEADER.test(before.split("\n", HEADER_LINES).join("\n"));
1253
+ }
1254
+ function unparenthesized(e) {
1255
+ let x = e;
1256
+ while (ts.isParenthesizedExpression(x)) x = x.expression;
1257
+ return x;
1258
+ }
1259
+ function assertedType(node) {
1260
+ let t = node.type;
1261
+ while (ts.isParenthesizedTypeNode(t)) t = t.type;
1262
+ return t;
1263
+ }
1264
+ function isDoubleCast(node) {
1265
+ const inner = unparenthesized(node.expression);
1266
+ return (ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner)) && assertedType(inner).kind === ts.SyntaxKind.UnknownKeyword;
1267
+ }
1268
+ function surfaceOf(path, src) {
1269
+ let sf;
1270
+ try {
1271
+ sf = ts.createSourceFile(path, src, ts.ScriptTarget.Latest, true, scriptKind(path));
1272
+ } catch {
1273
+ return null;
1274
+ }
1275
+ const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
1276
+ if (diagnostics.length > 0) return null;
1277
+ const surface = { type: 0, nonNull: 0, assertions: [] };
1278
+ const lines = src.split("\n");
1279
+ const record = (kind, node) => {
1280
+ const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
1281
+ surface.assertions.push({ kind, line, text: node.getText(sf).replace(/\s+/g, " "), lineText: (lines[line - 1] ?? "").trim() });
1282
+ if (kind === "type") surface.type++;
1283
+ else surface.nonNull++;
1284
+ };
1285
+ const isRow4OrHonest = (node) => {
1286
+ const type = assertedType(node);
1287
+ if (type.kind === ts.SyntaxKind.AnyKeyword || type.kind === ts.SyntaxKind.UnknownKeyword) return true;
1288
+ return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
1289
+ };
1290
+ const visit = (node) => {
1291
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
1292
+ if (!isDoubleCast(node) && !isRow4OrHonest(node)) record("type", node);
1293
+ } else if (ts.isNonNullExpression(node)) {
1294
+ record("non-null", node);
1295
+ }
1296
+ ts.forEachChild(node, visit);
1297
+ };
1298
+ visit(sf);
1299
+ return surface;
1300
+ }
1301
+ function unmatchedAfter(before, after) {
1302
+ const n = before.length;
1303
+ const m = after.length;
1304
+ const same = (i2, j2) => before[i2].lineText === after[j2].lineText && before[i2].text === after[j2].text;
1305
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1306
+ for (let i2 = n - 1; i2 >= 0; i2--) {
1307
+ for (let j2 = m - 1; j2 >= 0; j2--) {
1308
+ lcs[i2][j2] = same(i2, j2) ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
1309
+ }
1310
+ }
1311
+ const out2 = [];
1312
+ let i = 0;
1313
+ let j = 0;
1314
+ while (j < m) {
1315
+ if (i < n && same(i, j)) {
1316
+ i++;
1317
+ j++;
1318
+ } else if (i < n && lcs[i + 1][j] >= lcs[i][j + 1]) {
1319
+ i++;
1320
+ } else {
1321
+ out2.push(after[j]);
1322
+ j++;
1323
+ }
1324
+ }
1325
+ return out2;
1326
+ }
1327
+ var tsCastGrowth = {
1328
+ id: RULE2,
1329
+ surface: ["file"],
1330
+ certainty: "mechanical",
1331
+ run(changes, policy) {
1332
+ const out2 = [];
1333
+ for (const c of changes) {
1334
+ if (c.kind !== "file" || c.op === "delete" || c.after == null) continue;
1335
+ if (!isCodeFile(c.path) || outOfBudget(c.path, c.before)) continue;
1336
+ if (protectedCategory(c.path, policy) === "tests") continue;
1337
+ const after = surfaceOf(c.path, c.after);
1338
+ const before = c.before == null ? { type: 0, nonNull: 0, assertions: [] } : surfaceOf(c.path, c.before);
1339
+ if (!after || !before) continue;
1340
+ const dType = after.type - before.type;
1341
+ const dNonNull = after.nonNull - before.nonNull;
1342
+ if (dType <= 0 && dNonNull <= 0) continue;
1343
+ const grownKinds = /* @__PURE__ */ new Set([...dType > 0 ? ["type"] : [], ...dNonNull > 0 ? ["non-null"] : []]);
1344
+ const addedLineNumbers2 = new Set(addedLines(c).flatMap((l) => l.newLine == null ? [] : [l.newLine]));
1345
+ const onAddedLine = after.assertions.find((a) => grownKinds.has(a.kind) && addedLineNumbers2.has(a.line));
1346
+ const first = onAddedLine ?? unmatchedAfter(before.assertions, after.assertions).find((a) => grownKinds.has(a.kind)) ?? after.assertions.find((a) => grownKinds.has(a.kind));
1347
+ const parts = [];
1348
+ if (dType > 0) parts.push(`+${dType} type assertion${dType === 1 ? "" : "s"} (\`as T\` / \`<T>x\`)`);
1349
+ if (dNonNull > 0) parts.push(`+${dNonNull} non-null assertion${dNonNull === 1 ? "" : "s"} (\`x!\`)`);
1350
+ out2.push(
1351
+ makeFinding(RULE2, policy, {
1352
+ file: c.path,
1353
+ ...first ? { line: first.line } : {},
1354
+ message: `The unsafe assertion surface grew: ${parts.join(", ")} net of any removed in this change (now ${after.type} type / ${after.nonNull} non-null in this file).`,
1355
+ evidence: first?.text ?? parts.join(", "),
1356
+ remediation: "Prove the value instead of asserting it: parse or validate at the boundary, use a type guard or discriminated union, or handle the nullable case. If the assertion is genuinely unavoidable, keep it at the narrowest boundary and say why in a comment.",
1357
+ defaultSeverity: "warn"
1358
+ })
1359
+ );
1360
+ }
1361
+ return out2;
1362
+ }
1363
+ };
1364
+
1193
1365
  // src/detectors/ts-any-cast.ts
1194
1366
  var BLOCK_RULE = "ts-any-cast";
1195
1367
  var WARN_RULE = "ts-any-launder";
1196
- var DOUBLE_CAST = /\bas\s+unknown\s+as\b/g;
1368
+ var DOUBLE_CAST = /\bas\s+\(*\s*unknown\s*\)*\s+as\b/g;
1197
1369
  var SUPPRESS = /@ts-(?:ignore|expect-error|nocheck)\b/g;
1198
1370
  var JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*any\s*\}\s*\*\/\s*\(/g;
1199
1371
  var JS_FILE = /\.(?:js|jsx|mjs|cjs)$/;
1200
1372
  var countMatches = (s, re) => (s.match(re) || []).length;
1201
1373
  function countAny(src) {
1202
- const r = { cast: 0, broad: 0 };
1374
+ const r = { cast: 0, broad: 0, double: 0 };
1203
1375
  try {
1204
- const sf = ts.createSourceFile("f.ts", src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1376
+ const sf = ts2.createSourceFile("f.ts", src, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
1205
1377
  const visit = (node) => {
1206
- if (node.kind === ts.SyntaxKind.AnyKeyword) {
1378
+ if ((ts2.isAsExpression(node) || ts2.isTypeAssertionExpression(node)) && isDoubleCast(node)) r.double++;
1379
+ if (node.kind === ts2.SyntaxKind.AnyKeyword) {
1207
1380
  let target = node;
1208
1381
  let p = target.parent;
1209
- while (p && ts.isParenthesizedTypeNode(p)) {
1382
+ while (p && ts2.isParenthesizedTypeNode(p)) {
1210
1383
  target = p;
1211
1384
  p = p.parent;
1212
1385
  }
1213
- const isCast = p && ts.isAsExpression(p) && p.type === target || p && ts.isTypeAssertionExpression(p) && p.type === target;
1386
+ const isCast = p && ts2.isAsExpression(p) && p.type === target || p && ts2.isTypeAssertionExpression(p) && p.type === target;
1214
1387
  if (isCast) r.cast++;
1215
1388
  else r.broad++;
1216
1389
  }
1217
- ts.forEachChild(node, visit);
1390
+ ts2.forEachChild(node, visit);
1218
1391
  };
1219
1392
  visit(sf);
1220
1393
  } catch {
1221
1394
  }
1222
1395
  return r;
1223
1396
  }
1224
- var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|\bas\s+unknown\s+as\b|@ts-(?:ignore|expect-error|nocheck)\b/;
1397
+ var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|\bas\s+\(*\s*unknown\s*\)*\s+as\b|@ts-(?:ignore|expect-error|nocheck)\b/;
1225
1398
  var BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
1226
1399
  var BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
1227
1400
  var WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
@@ -1240,7 +1413,7 @@ var tsAnyCast = {
1240
1413
  const b = countAny(before);
1241
1414
  const dCast = a.cast - b.cast;
1242
1415
  const dBroad = a.broad - b.broad;
1243
- const dDouble = countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST);
1416
+ const dDouble = Math.max(a.double - b.double, countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST));
1244
1417
  const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
1245
1418
  const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
1246
1419
  const blockReasons = [];
@@ -1304,7 +1477,7 @@ var tsAnyCast = {
1304
1477
  };
1305
1478
 
1306
1479
  // src/detectors/lint-suppression.ts
1307
- var RULE2 = "lint-suppression";
1480
+ var RULE3 = "lint-suppression";
1308
1481
  var PATTERNS = {
1309
1482
  js: [
1310
1483
  { re: /eslint-disable(?:-next-line|-line)?\b/, why: "eslint-disable suppresses lint rules" },
@@ -1352,7 +1525,7 @@ var PATTERNS = {
1352
1525
  ]
1353
1526
  };
1354
1527
  var lintSuppression = {
1355
- id: RULE2,
1528
+ id: RULE3,
1356
1529
  surface: ["file"],
1357
1530
  certainty: "mechanical",
1358
1531
  run(changes, policy) {
@@ -1368,7 +1541,7 @@ var lintSuppression = {
1368
1541
  const m = p.re.exec(l.content);
1369
1542
  if (!m || insideStringLiteral(l.content, m.index, lang)) continue;
1370
1543
  out2.push(
1371
- makeFinding(RULE2, policy, {
1544
+ makeFinding(RULE3, policy, {
1372
1545
  file: c.path,
1373
1546
  line: l.newLine ?? void 0,
1374
1547
  message: `Lint suppression added: ${p.why}.`,
@@ -1385,8 +1558,8 @@ var lintSuppression = {
1385
1558
  };
1386
1559
 
1387
1560
  // src/detectors/test-skip.ts
1388
- import ts2 from "typescript";
1389
- var RULE3 = "test-skip";
1561
+ import ts3 from "typescript";
1562
+ var RULE4 = "test-skip";
1390
1563
  var acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
1391
1564
  var JS_RUNNER = "\\b(?:it|test|describe|suite)";
1392
1565
  var JS_MOD = "concurrent|sequential|shuffle";
@@ -1489,11 +1662,11 @@ var JS_TEST_MODULES = /* @__PURE__ */ new Set([
1489
1662
  "bun:test",
1490
1663
  "@playwright/test"
1491
1664
  ]);
1492
- function scriptKind(path) {
1493
- if (/\.tsx$/i.test(path)) return ts2.ScriptKind.TSX;
1494
- if (/\.jsx$/i.test(path)) return ts2.ScriptKind.JSX;
1495
- if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts2.ScriptKind.JS;
1496
- return ts2.ScriptKind.TS;
1665
+ function scriptKind2(path) {
1666
+ if (/\.tsx$/i.test(path)) return ts3.ScriptKind.TSX;
1667
+ if (/\.jsx$/i.test(path)) return ts3.ScriptKind.JSX;
1668
+ if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts3.ScriptKind.JS;
1669
+ return ts3.ScriptKind.TS;
1497
1670
  }
1498
1671
  function addedLineNumbers(c) {
1499
1672
  const out2 = /* @__PURE__ */ new Set();
@@ -1505,32 +1678,32 @@ function addedLineNumbers(c) {
1505
1678
  return out2;
1506
1679
  }
1507
1680
  function moduleName(expr) {
1508
- return ts2.isStringLiteralLike(expr) ? expr.text : null;
1681
+ return ts3.isStringLiteralLike(expr) ? expr.text : null;
1509
1682
  }
1510
1683
  function requiredModule(expr) {
1511
- if (!ts2.isCallExpression(expr) || !ts2.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1684
+ if (!ts3.isCallExpression(expr) || !ts3.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
1512
1685
  return moduleName(expr.arguments[0]);
1513
1686
  }
1514
1687
  function astContext(path, source) {
1515
1688
  const rootName = path.startsWith("/") ? path : "/tamperward/" + path;
1516
- const sf = ts2.createSourceFile(
1689
+ const sf = ts3.createSourceFile(
1517
1690
  rootName,
1518
1691
  source,
1519
- ts2.ScriptTarget.Latest,
1692
+ ts3.ScriptTarget.Latest,
1520
1693
  true,
1521
- scriptKind(path)
1694
+ scriptKind2(path)
1522
1695
  );
1523
- const diagnostics = sf.parseDiagnostics ?? [];
1696
+ const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
1524
1697
  if (diagnostics.length > 0) return null;
1525
1698
  const options = {
1526
1699
  allowJs: true,
1527
1700
  checkJs: false,
1528
1701
  noLib: true,
1529
1702
  noResolve: true,
1530
- target: ts2.ScriptTarget.Latest,
1531
- module: ts2.ModuleKind.ESNext
1703
+ target: ts3.ScriptTarget.Latest,
1704
+ module: ts3.ModuleKind.ESNext
1532
1705
  };
1533
- const host = ts2.createCompilerHost(options, true);
1706
+ const host = ts3.createCompilerHost(options, true);
1534
1707
  host.getSourceFile = (fileName) => fileName === rootName ? sf : void 0;
1535
1708
  host.fileExists = (fileName) => fileName === rootName;
1536
1709
  host.readFile = (fileName) => fileName === rootName ? source : void 0;
@@ -1541,7 +1714,7 @@ function astContext(path, source) {
1541
1714
  host.getCanonicalFileName = (fileName) => fileName;
1542
1715
  host.useCaseSensitiveFileNames = () => true;
1543
1716
  host.getNewLine = () => "\n";
1544
- const program = ts2.createProgram([rootName], options, host);
1717
+ const program = ts3.createProgram([rootName], options, host);
1545
1718
  return { sf, checker: program.getTypeChecker() };
1546
1719
  }
1547
1720
  function symbolAt(ctx, node) {
@@ -1555,13 +1728,13 @@ function importAliases(ctx) {
1555
1728
  if (sym) out2.set(sym, { causes });
1556
1729
  };
1557
1730
  for (const stmt of ctx.sf.statements) {
1558
- if (ts2.isImportDeclaration(stmt) && ts2.isStringLiteralLike(stmt.moduleSpecifier)) {
1731
+ if (ts3.isImportDeclaration(stmt) && ts3.isStringLiteralLike(stmt.moduleSpecifier)) {
1559
1732
  if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
1560
1733
  const clause = stmt.importClause;
1561
1734
  if (!clause) continue;
1562
1735
  if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
1563
1736
  const bindings = clause.namedBindings;
1564
- if (bindings && ts2.isNamedImports(bindings)) {
1737
+ if (bindings && ts3.isNamedImports(bindings)) {
1565
1738
  for (const el of bindings.elements) {
1566
1739
  const imported = el.propertyName?.text ?? el.name.text;
1567
1740
  if (JS_RUNNERS.has(imported)) add(el.name);
@@ -1569,17 +1742,17 @@ function importAliases(ctx) {
1569
1742
  }
1570
1743
  continue;
1571
1744
  }
1572
- if (!ts2.isVariableStatement(stmt)) continue;
1745
+ if (!ts3.isVariableStatement(stmt)) continue;
1573
1746
  for (const decl of stmt.declarationList.declarations) {
1574
- if (ts2.isObjectBindingPattern(decl.name) && decl.initializer) {
1747
+ if (ts3.isObjectBindingPattern(decl.name) && decl.initializer) {
1575
1748
  const mod = requiredModule(decl.initializer);
1576
1749
  if (!mod || !JS_TEST_MODULES.has(mod)) continue;
1577
1750
  for (const el of decl.name.elements) {
1578
- if (!ts2.isIdentifier(el.name)) continue;
1579
- const imported = el.propertyName && ts2.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1751
+ if (!ts3.isIdentifier(el.name)) continue;
1752
+ const imported = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1580
1753
  if (JS_RUNNERS.has(imported)) add(el.name);
1581
1754
  }
1582
- } else if (ts2.isIdentifier(decl.name) && decl.initializer && ts2.isIdentifier(decl.initializer)) {
1755
+ } else if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isIdentifier(decl.initializer)) {
1583
1756
  simpleAliases.push({ local: decl.name, source: decl.initializer });
1584
1757
  }
1585
1758
  }
@@ -1605,11 +1778,11 @@ function importAliases(ctx) {
1605
1778
  function topLevelStaticStrings(ctx) {
1606
1779
  const pending = /* @__PURE__ */ new Map();
1607
1780
  for (const stmt of ctx.sf.statements) {
1608
- if (!ts2.isVariableStatement(stmt)) continue;
1609
- const isConst = (stmt.declarationList.flags & ts2.NodeFlags.Const) !== 0;
1781
+ if (!ts3.isVariableStatement(stmt)) continue;
1782
+ const isConst = (stmt.declarationList.flags & ts3.NodeFlags.Const) !== 0;
1610
1783
  if (!isConst) continue;
1611
1784
  for (const decl of stmt.declarationList.declarations) {
1612
- if (!ts2.isIdentifier(decl.name) || !decl.initializer) continue;
1785
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
1613
1786
  const sym = symbolAt(ctx, decl.name);
1614
1787
  if (sym) pending.set(sym, { decl: decl.name, expr: decl.initializer });
1615
1788
  }
@@ -1617,15 +1790,15 @@ function topLevelStaticStrings(ctx) {
1617
1790
  const resolved = /* @__PURE__ */ new Map();
1618
1791
  const resolving = /* @__PURE__ */ new Set();
1619
1792
  const valueOf = (expr) => {
1620
- if (ts2.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
1621
- if (ts2.isParenthesizedExpression(expr)) return valueOf(expr.expression);
1622
- if (ts2.isBinaryExpression(expr) && expr.operatorToken.kind === ts2.SyntaxKind.PlusToken) {
1793
+ if (ts3.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
1794
+ if (ts3.isParenthesizedExpression(expr)) return valueOf(expr.expression);
1795
+ if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
1623
1796
  const left = valueOf(expr.left);
1624
1797
  const right = valueOf(expr.right);
1625
1798
  if (!left || !right) return null;
1626
1799
  return { value: left.value + right.value, causes: [...left.causes, ...right.causes] };
1627
1800
  }
1628
- if (ts2.isIdentifier(expr)) {
1801
+ if (ts3.isIdentifier(expr)) {
1629
1802
  const sym = symbolAt(ctx, expr);
1630
1803
  if (!sym) return null;
1631
1804
  const cached = resolved.get(sym);
@@ -1652,15 +1825,15 @@ function topLevelStaticStrings(ctx) {
1652
1825
  return resolved;
1653
1826
  }
1654
1827
  function staticPropertyName(expr, ctx, strings) {
1655
- if (ts2.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
1656
- if (ts2.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
1657
- if (ts2.isBinaryExpression(expr) && expr.operatorToken.kind === ts2.SyntaxKind.PlusToken) {
1828
+ if (ts3.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
1829
+ if (ts3.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
1830
+ if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
1658
1831
  const left = staticPropertyName(expr.left, ctx, strings);
1659
1832
  const right = staticPropertyName(expr.right, ctx, strings);
1660
1833
  if (!left || !right) return null;
1661
1834
  return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
1662
1835
  }
1663
- if (ts2.isIdentifier(expr)) {
1836
+ if (ts3.isIdentifier(expr)) {
1664
1837
  const sym = symbolAt(ctx, expr);
1665
1838
  if (!sym) return null;
1666
1839
  const value = strings.get(sym);
@@ -1673,8 +1846,8 @@ function runnerChain(expr, ctx, runners, strings) {
1673
1846
  const causes = [];
1674
1847
  let cur = expr;
1675
1848
  let terminalNode = expr;
1676
- while (ts2.isPropertyAccessExpression(cur) || ts2.isElementAccessExpression(cur)) {
1677
- if (ts2.isPropertyAccessExpression(cur)) {
1849
+ while (ts3.isPropertyAccessExpression(cur) || ts3.isElementAccessExpression(cur)) {
1850
+ if (ts3.isPropertyAccessExpression(cur)) {
1678
1851
  props.unshift(cur.name.text);
1679
1852
  if (props.length === 1) terminalNode = cur.name;
1680
1853
  cur = cur.expression;
@@ -1688,7 +1861,7 @@ function runnerChain(expr, ctx, runners, strings) {
1688
1861
  if (props.length === 1) terminalNode = cur.argumentExpression;
1689
1862
  cur = cur.expression;
1690
1863
  }
1691
- if (!ts2.isIdentifier(cur)) return null;
1864
+ if (!ts3.isIdentifier(cur)) return null;
1692
1865
  const sym = symbolAt(ctx, cur);
1693
1866
  if (sym) {
1694
1867
  const binding = runners.get(sym);
@@ -1700,17 +1873,17 @@ function runnerChain(expr, ctx, runners, strings) {
1700
1873
  return { root: cur.text, props, terminalNode, causes };
1701
1874
  }
1702
1875
  function optionDisables(value) {
1703
- return !(value.kind === ts2.SyntaxKind.FalseKeyword || ts2.isNumericLiteral(value) && Number(value.text) === 0);
1876
+ return !(value.kind === ts3.SyntaxKind.FalseKeyword || ts3.isNumericLiteral(value) && Number(value.text) === 0);
1704
1877
  }
1705
1878
  function shorthandOptionDisables(prop, ctx) {
1706
1879
  const sym = ctx.checker.getShorthandAssignmentValueSymbol(prop) ?? symbolAt(ctx, prop.name);
1707
- const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts2.isVariableDeclaration(d));
1708
- if (!decl || !ts2.isVariableDeclaration(decl) || !decl.initializer) return true;
1880
+ const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts3.isVariableDeclaration(d));
1881
+ if (!decl || !ts3.isVariableDeclaration(decl) || !decl.initializer) return true;
1709
1882
  return optionDisables(decl.initializer);
1710
1883
  }
1711
1884
  function structuralNodeKey(node, sf) {
1712
1885
  const children = node.getChildren(sf);
1713
- const text = node.text;
1886
+ const text = "text" in node ? node.text : void 0;
1714
1887
  if (children.length === 0) {
1715
1888
  return typeof text === "string" ? `${node.kind}:${JSON.stringify(text)}` : String(node.kind);
1716
1889
  }
@@ -1722,7 +1895,7 @@ function semanticSkipHits(ctx) {
1722
1895
  const hits = [];
1723
1896
  const callOrdinals = /* @__PURE__ */ new Map();
1724
1897
  const visit = (node) => {
1725
- if (ts2.isCallExpression(node)) {
1898
+ if (ts3.isCallExpression(node)) {
1726
1899
  const callText = node.getText(ctx.sf).trim();
1727
1900
  const callIdentity = structuralNodeKey(node, ctx.sf);
1728
1901
  const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
@@ -1747,19 +1920,19 @@ function semanticSkipHits(ctx) {
1747
1920
  push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
1748
1921
  } else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
1749
1922
  push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
1750
- } else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts2.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
1923
+ } else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts3.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
1751
1924
  push(terminalNode, [], "an empty .each table (no case ever runs)");
1752
1925
  }
1753
1926
  if (props.length === 0) {
1754
1927
  for (const arg of node.arguments) {
1755
- if (!ts2.isObjectLiteralExpression(arg)) continue;
1928
+ if (!ts3.isObjectLiteralExpression(arg)) continue;
1756
1929
  for (const prop of arg.properties) {
1757
- if (ts2.isPropertyAssignment(prop)) {
1930
+ if (ts3.isPropertyAssignment(prop)) {
1758
1931
  let name = null;
1759
1932
  let propertyCauses = [];
1760
- if (ts2.isIdentifier(prop.name) || ts2.isStringLiteralLike(prop.name)) {
1933
+ if (ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name)) {
1761
1934
  name = prop.name.text;
1762
- } else if (ts2.isComputedPropertyName(prop.name)) {
1935
+ } else if (ts3.isComputedPropertyName(prop.name)) {
1763
1936
  const resolved = staticPropertyName(prop.name.expression, ctx, strings);
1764
1937
  name = resolved?.name ?? null;
1765
1938
  propertyCauses = resolved?.causes ?? [];
@@ -1768,7 +1941,7 @@ function semanticSkipHits(ctx) {
1768
1941
  if (!optionDisables(prop.initializer)) continue;
1769
1942
  const why = name === "only" ? "an { only: ... } option focusing the test" : name === "todo" ? "a { todo: ... } option (the test no longer fails the run)" : "a { skip: ... } option disabling the test";
1770
1943
  push(prop.name, propertyCauses, why);
1771
- } else if (ts2.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1944
+ } else if (ts3.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
1772
1945
  if (!shorthandOptionDisables(prop, ctx)) continue;
1773
1946
  push(prop.name, [], `a { ${prop.name.text} } option that conditionally narrows the test run`);
1774
1947
  }
@@ -1777,7 +1950,7 @@ function semanticSkipHits(ctx) {
1777
1950
  }
1778
1951
  }
1779
1952
  }
1780
- ts2.forEachChild(node, visit);
1953
+ ts3.forEachChild(node, visit);
1781
1954
  };
1782
1955
  visit(ctx.sf);
1783
1956
  return hits;
@@ -1816,7 +1989,7 @@ function astSkipHits(c) {
1816
1989
  return { hits, authoritative: true };
1817
1990
  }
1818
1991
  var testSkip = {
1819
- id: RULE3,
1992
+ id: RULE4,
1820
1993
  surface: ["file"],
1821
1994
  certainty: "mechanical",
1822
1995
  run(changes, policy) {
@@ -1834,7 +2007,7 @@ var testSkip = {
1834
2007
  for (const hit of analysis.hits) {
1835
2008
  astHitLines.add(hit.line);
1836
2009
  out2.push(
1837
- makeFinding(RULE3, policy, {
2010
+ makeFinding(RULE4, policy, {
1838
2011
  file: c.path,
1839
2012
  line: hit.line,
1840
2013
  message: `Test skipped or narrowed: ${hit.why}.`,
@@ -1852,7 +2025,7 @@ var testSkip = {
1852
2025
  if (comment && !p.comment) continue;
1853
2026
  if (matchesOutsideString(p, l.content, lang)) {
1854
2027
  out2.push(
1855
- makeFinding(RULE3, policy, {
2028
+ makeFinding(RULE4, policy, {
1856
2029
  file: c.path,
1857
2030
  line: l.newLine ?? void 0,
1858
2031
  message: `Test skipped or narrowed: ${p.why}.`,
@@ -1871,7 +2044,7 @@ var testSkip = {
1871
2044
 
1872
2045
  // src/detectors/coverage-lowering.ts
1873
2046
  import picomatch2 from "picomatch";
1874
- import ts3 from "typescript";
2047
+ import ts4 from "typescript";
1875
2048
 
1876
2049
  // src/detectors/repo.ts
1877
2050
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -1994,34 +2167,34 @@ function containsProtected(token, policy, category, ctx, except) {
1994
2167
  }
1995
2168
 
1996
2169
  // src/detectors/coverage-lowering.ts
1997
- var RULE4 = "coverage-lowering";
2170
+ var RULE5 = "coverage-lowering";
1998
2171
  var METRICS = ["branches", "functions", "lines", "statements"];
1999
- var isMetric = (k) => METRICS.includes(k);
2172
+ var isMetric = (k) => METRICS.some((m) => m === k);
2000
2173
  var norm = (p) => p.replace(/^\.\//, "");
2001
2174
  function keyName(name) {
2002
- if (ts3.isIdentifier(name) || ts3.isStringLiteral(name) || ts3.isNumericLiteral(name)) return name.text;
2175
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
2003
2176
  return null;
2004
2177
  }
2005
2178
  function numericOf(e) {
2006
- if (ts3.isNumericLiteral(e)) return Number(e.text);
2007
- if (ts3.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
2008
- if (ts3.isPrefixUnaryExpression(e) && e.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(e.operand)) {
2179
+ if (ts4.isNumericLiteral(e)) return Number(e.text);
2180
+ if (ts4.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
2181
+ if (ts4.isPrefixUnaryExpression(e) && e.operator === ts4.SyntaxKind.MinusToken && ts4.isNumericLiteral(e.operand)) {
2009
2182
  return -Number(e.operand.text);
2010
2183
  }
2011
2184
  return void 0;
2012
2185
  }
2013
2186
  function metricsOf(expr, resolve10) {
2014
2187
  const obj = resolve10(expr);
2015
- if (!ts3.isObjectLiteralExpression(obj)) return void 0;
2188
+ if (!ts4.isObjectLiteralExpression(obj)) return void 0;
2016
2189
  const out2 = { values: {}, opaque: false };
2017
2190
  let any = false;
2018
2191
  for (const p of obj.properties) {
2019
- if (ts3.isSpreadAssignment(p)) {
2192
+ if (ts4.isSpreadAssignment(p)) {
2020
2193
  out2.opaque = true;
2021
2194
  any = true;
2022
2195
  continue;
2023
2196
  }
2024
- if (!ts3.isPropertyAssignment(p)) continue;
2197
+ if (!ts4.isPropertyAssignment(p)) continue;
2025
2198
  const k = keyName(p.name);
2026
2199
  if (k === null || !isMetric(k)) continue;
2027
2200
  any = true;
@@ -2033,34 +2206,34 @@ function metricsOf(expr, resolve10) {
2033
2206
  }
2034
2207
  function underKey(node, key2) {
2035
2208
  for (let p = node.parent; p; p = p.parent) {
2036
- if (ts3.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2209
+ if (ts4.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
2037
2210
  }
2038
2211
  return false;
2039
2212
  }
2040
2213
  function ownerKey(node) {
2041
2214
  const obj = node.parent;
2042
- if (!obj || !ts3.isObjectLiteralExpression(obj)) return null;
2215
+ if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
2043
2216
  let q = obj.parent;
2044
- while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
2045
- return q && ts3.isPropertyAssignment(q) ? keyName(q.name) : null;
2217
+ while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
2218
+ return q && ts4.isPropertyAssignment(q) ? keyName(q.name) : null;
2046
2219
  }
2047
2220
  function asExpression(src) {
2048
2221
  const t = src.trim();
2049
2222
  return t.startsWith("{") ? `(${t})` : src;
2050
2223
  }
2051
2224
  function sourceOf(src) {
2052
- return ts3.createSourceFile("cfg.ts", asExpression(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
2225
+ return ts4.createSourceFile("cfg.ts", asExpression(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
2053
2226
  }
2054
2227
  function resolverFor(sf) {
2055
2228
  const consts = /* @__PURE__ */ new Map();
2056
2229
  for (const st of sf.statements) {
2057
- if (!ts3.isVariableStatement(st)) continue;
2230
+ if (!ts4.isVariableStatement(st)) continue;
2058
2231
  for (const d of st.declarationList.declarations) {
2059
- if (ts3.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2232
+ if (ts4.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
2060
2233
  }
2061
2234
  }
2062
2235
  return (e) => {
2063
- if (ts3.isIdentifier(e)) {
2236
+ if (ts4.isIdentifier(e)) {
2064
2237
  const bound = consts.get(e.text);
2065
2238
  return bound ?? e;
2066
2239
  }
@@ -2073,25 +2246,25 @@ function parseThresholds(src) {
2073
2246
  const sf = sourceOf(src);
2074
2247
  const resolve10 = resolverFor(sf);
2075
2248
  const visit = (node) => {
2076
- if (ts3.isPropertyAssignment(node)) {
2249
+ if (ts4.isPropertyAssignment(node)) {
2077
2250
  const key2 = keyName(node.name);
2078
2251
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
2079
2252
  res.present = true;
2080
2253
  const init = resolve10(node.initializer);
2081
- if (!ts3.isObjectLiteralExpression(init)) {
2254
+ if (!ts4.isObjectLiteralExpression(init)) {
2082
2255
  res.opaque = true;
2083
2256
  } else {
2084
2257
  const flat = metricsOf(init, resolve10);
2085
2258
  if (flat) res.global = merge(res.global, flat);
2086
2259
  for (const p of init.properties) {
2087
- if (ts3.isSpreadAssignment(p)) {
2260
+ if (ts4.isSpreadAssignment(p)) {
2088
2261
  res.opaque = true;
2089
2262
  continue;
2090
2263
  }
2091
- if (!ts3.isPropertyAssignment(p)) continue;
2264
+ if (!ts4.isPropertyAssignment(p)) continue;
2092
2265
  const k = keyName(p.name);
2093
2266
  if (k === null || isMetric(k)) continue;
2094
- if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts3.SyntaxKind.TrueKeyword) {
2267
+ if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts4.SyntaxKind.TrueKeyword) {
2095
2268
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
2096
2269
  continue;
2097
2270
  }
@@ -2103,7 +2276,7 @@ function parseThresholds(src) {
2103
2276
  }
2104
2277
  }
2105
2278
  }
2106
- ts3.forEachChild(node, visit);
2279
+ ts4.forEachChild(node, visit);
2107
2280
  };
2108
2281
  visit(sf);
2109
2282
  } catch {
@@ -2114,12 +2287,12 @@ function merge(a, b) {
2114
2287
  return { values: { ...a?.values ?? {}, ...b.values }, opaque: (a?.opaque ?? false) || b.opaque };
2115
2288
  }
2116
2289
  function literals(e) {
2117
- if (ts3.isStringLiteral(e)) return { items: [e.text], opaque: false };
2118
- if (!ts3.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2290
+ if (ts4.isStringLiteral(e)) return { items: [e.text], opaque: false };
2291
+ if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
2119
2292
  const items = [];
2120
2293
  let opaque = false;
2121
2294
  for (const el of e.elements) {
2122
- if (ts3.isStringLiteral(el) || ts3.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2295
+ if (ts4.isStringLiteral(el) || ts4.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
2123
2296
  else opaque = true;
2124
2297
  }
2125
2298
  return { items, opaque };
@@ -2130,7 +2303,7 @@ function parseLists(src) {
2130
2303
  const sf = sourceOf(src);
2131
2304
  const resolve10 = resolverFor(sf);
2132
2305
  const visit = (node) => {
2133
- if (ts3.isPropertyAssignment(node)) {
2306
+ if (ts4.isPropertyAssignment(node)) {
2134
2307
  const k = keyName(node.name);
2135
2308
  const vitestCoverage = ownerKey(node) === "coverage";
2136
2309
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
@@ -2144,7 +2317,7 @@ function parseLists(src) {
2144
2317
  if (opaque) res.opaqueIgnore = true;
2145
2318
  }
2146
2319
  }
2147
- ts3.forEachChild(node, visit);
2320
+ ts4.forEachChild(node, visit);
2148
2321
  };
2149
2322
  visit(sf);
2150
2323
  } catch {
@@ -2155,15 +2328,15 @@ function parseSwitches(src) {
2155
2328
  const res = {};
2156
2329
  try {
2157
2330
  const sf = sourceOf(src);
2158
- const bool = (e) => e.kind === ts3.SyntaxKind.TrueKeyword ? true : e.kind === ts3.SyntaxKind.FalseKeyword ? false : void 0;
2331
+ const bool = (e) => e.kind === ts4.SyntaxKind.TrueKeyword ? true : e.kind === ts4.SyntaxKind.FalseKeyword ? false : void 0;
2159
2332
  const visit = (node) => {
2160
- if (ts3.isPropertyAssignment(node)) {
2333
+ if (ts4.isPropertyAssignment(node)) {
2161
2334
  const k = keyName(node.name);
2162
2335
  const owner = ownerKey(node);
2163
2336
  if (k === "enabled" && owner === "coverage") res.enabled = bool(node.initializer);
2164
2337
  else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool(node.initializer);
2165
2338
  }
2166
- ts3.forEachChild(node, visit);
2339
+ ts4.forEachChild(node, visit);
2167
2340
  };
2168
2341
  visit(sf);
2169
2342
  } catch {
@@ -2176,11 +2349,11 @@ function sectionText(src, key2) {
2176
2349
  let out2 = "";
2177
2350
  const visit = (node) => {
2178
2351
  if (out2) return;
2179
- if (ts3.isPropertyAssignment(node) && keyName(node.name) === key2 && ts3.isObjectLiteralExpression(node.initializer)) {
2352
+ if (ts4.isPropertyAssignment(node) && keyName(node.name) === key2 && ts4.isObjectLiteralExpression(node.initializer)) {
2180
2353
  out2 = node.initializer.getText(sf);
2181
2354
  return;
2182
2355
  }
2183
- ts3.forEachChild(node, visit);
2356
+ ts4.forEachChild(node, visit);
2184
2357
  };
2185
2358
  visit(sf);
2186
2359
  return out2;
@@ -2517,7 +2690,7 @@ function gateMovedElsewhere(c, changes, policy) {
2517
2690
  });
2518
2691
  }
2519
2692
  var coverageLowering = {
2520
- id: RULE4,
2693
+ id: RULE5,
2521
2694
  surface: ["file"],
2522
2695
  certainty: "mechanical",
2523
2696
  run(changes, policy, _view, ctx) {
@@ -2539,7 +2712,7 @@ var coverageLowering = {
2539
2712
  reasons.push(...simpleNarrowings(c.path, c.before ?? "", c.after));
2540
2713
  for (const reason of reasons) {
2541
2714
  out2.push(
2542
- makeFinding(RULE4, policy, {
2715
+ makeFinding(RULE5, policy, {
2543
2716
  file: c.path,
2544
2717
  message: `Coverage gate weakened: ${reason}.`,
2545
2718
  evidence: reason,
@@ -2552,7 +2725,7 @@ var coverageLowering = {
2552
2725
  const gated = isJsConfig(c.path) ? parseThresholds(before).present : simpleWeakenings(c.path, before, "").length > 0 || SIMPLE_FILES.some((s) => s.file.test(c.path) && s.keys.some((k) => numbersOf(before, k.key).length));
2553
2726
  if (gated && !gateMovedElsewhere(c, changes, policy)) {
2554
2727
  out2.push(
2555
- makeFinding(RULE4, policy, {
2728
+ makeFinding(RULE5, policy, {
2556
2729
  file: c.path,
2557
2730
  message: "Coverage gate weakened: the coverage config was deleted.",
2558
2731
  evidence: c.path,
@@ -2566,7 +2739,7 @@ var coverageLowering = {
2566
2739
  const removedCoverage = removed.find((l) => /--coverage\b/.test(l.content));
2567
2740
  if (removedCoverage && !added.some((l) => /--coverage\b/.test(l.content))) {
2568
2741
  out2.push(
2569
- makeFinding(RULE4, policy, {
2742
+ makeFinding(RULE5, policy, {
2570
2743
  file: c.path,
2571
2744
  message: "The --coverage flag was removed from a test script.",
2572
2745
  evidence: removedCoverage.content.trim(),
@@ -2580,7 +2753,7 @@ var coverageLowering = {
2580
2753
  if (removed.some((r) => s.re.test(r.content))) continue;
2581
2754
  if (/passWithNoTests/.test(s.re.source) && dirHasSpec(c.path, policy, ctx) === false) continue;
2582
2755
  out2.push(
2583
- makeFinding(RULE4, policy, {
2756
+ makeFinding(RULE5, policy, {
2584
2757
  file: c.path,
2585
2758
  line: l.newLine ?? void 0,
2586
2759
  message: s.message,
@@ -2603,7 +2776,7 @@ var coverageLowering = {
2603
2776
  const ov = oldNums.get(m[1]);
2604
2777
  if (ov !== void 0 && Number(m[2]) < ov) {
2605
2778
  out2.push(
2606
- makeFinding(RULE4, policy, {
2779
+ makeFinding(RULE5, policy, {
2607
2780
  file: c.path,
2608
2781
  line: l.newLine ?? void 0,
2609
2782
  message: `Coverage threshold for ${m[1]} lowered ${ov} \u2192 ${m[2]}.`,
@@ -2626,7 +2799,7 @@ var coverageLowering = {
2626
2799
  };
2627
2800
 
2628
2801
  // src/detectors/ci-tampering.ts
2629
- var RULE5 = "ci-tampering";
2802
+ var RULE6 = "ci-tampering";
2630
2803
  var USES = /^\s*-\s*uses:/;
2631
2804
  var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
2632
2805
  var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
@@ -2890,7 +3063,7 @@ function isTruthy(raw) {
2890
3063
  return f !== void 0 && truthy(f);
2891
3064
  }
2892
3065
  var SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
2893
- var indentOf = (l) => l.match(/^\s*/)[0].length;
3066
+ var indentOf = (l) => l.length - l.trimStart().length;
2894
3067
  var STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
2895
3068
  function stepOf(lines, i) {
2896
3069
  const ind = indentOf(lines[i]);
@@ -3072,7 +3245,7 @@ function triggerNarrowings(before, after, opts) {
3072
3245
  }
3073
3246
  var REUSED_WORKFLOW = /^\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
3074
3247
  var ciTampering = {
3075
- id: RULE5,
3248
+ id: RULE6,
3076
3249
  surface: ["file"],
3077
3250
  certainty: "mechanical",
3078
3251
  run(changes, policy, _view, ctx) {
@@ -3084,7 +3257,7 @@ var ciTampering = {
3084
3257
  if (c.kind !== "file") continue;
3085
3258
  if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
3086
3259
  out2.push(
3087
- makeFinding(RULE5, policy, {
3260
+ makeFinding(RULE6, policy, {
3088
3261
  file: c.path,
3089
3262
  message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
3090
3263
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -3108,7 +3281,7 @@ var ciTampering = {
3108
3281
  }
3109
3282
  if (coe && isTruthy(coe[1])) {
3110
3283
  out2.push(
3111
- makeFinding(RULE5, policy, {
3284
+ makeFinding(RULE6, policy, {
3112
3285
  file: c.path,
3113
3286
  line: l.newLine ?? void 0,
3114
3287
  message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
@@ -3118,7 +3291,7 @@ var ciTampering = {
3118
3291
  );
3119
3292
  } else if (cond && isAlwaysFalse(cond[1])) {
3120
3293
  out2.push(
3121
- makeFinding(RULE5, policy, {
3294
+ makeFinding(RULE6, policy, {
3122
3295
  file: c.path,
3123
3296
  line: l.newLine ?? void 0,
3124
3297
  message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
@@ -3138,7 +3311,7 @@ var ciTampering = {
3138
3311
  const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
3139
3312
  if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
3140
3313
  out2.push(
3141
- makeFinding(RULE5, policy, {
3314
+ makeFinding(RULE6, policy, {
3142
3315
  file: c.path,
3143
3316
  line: idx + 1,
3144
3317
  message: `A check's run block was neutralised: ${n.what}.`,
@@ -3154,7 +3327,7 @@ var ciTampering = {
3154
3327
  const step = stepOf(afterLines, l.newLine - 1);
3155
3328
  if (step && !stepHasCheck(afterLines, step)) continue;
3156
3329
  out2.push(
3157
- makeFinding(RULE5, policy, {
3330
+ makeFinding(RULE6, policy, {
3158
3331
  file: c.path,
3159
3332
  line: l.newLine,
3160
3333
  message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
@@ -3163,11 +3336,11 @@ var ciTampering = {
3163
3336
  })
3164
3337
  );
3165
3338
  }
3166
- if (c.before != null) {
3339
+ if (c.before != null && c.after != null) {
3167
3340
  triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
3168
3341
  for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
3169
3342
  out2.push(
3170
- makeFinding(RULE5, policy, {
3343
+ makeFinding(RULE6, policy, {
3171
3344
  file: c.path,
3172
3345
  message: `The workflow's triggers were narrowed: ${reason}.`,
3173
3346
  evidence: reason,
@@ -3196,7 +3369,7 @@ var ciTampering = {
3196
3369
  if (s.state === "kept") continue;
3197
3370
  if (s.state === "neutralised") {
3198
3371
  out2.push(
3199
- makeFinding(RULE5, policy, {
3372
+ makeFinding(RULE6, policy, {
3200
3373
  file: c.path,
3201
3374
  message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
3202
3375
  evidence: `${l.content.trim()} \u2192 ${s.by}`,
@@ -3206,7 +3379,7 @@ var ciTampering = {
3206
3379
  continue;
3207
3380
  }
3208
3381
  out2.push(
3209
- makeFinding(RULE5, policy, {
3382
+ makeFinding(RULE6, policy, {
3210
3383
  file: c.path,
3211
3384
  message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
3212
3385
  evidence: l.content.trim(),
@@ -3235,7 +3408,7 @@ function shippedVersion() {
3235
3408
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
3236
3409
  try {
3237
3410
  const pkg = JSON.parse(readFileSync3(join5(here, rel), "utf8"));
3238
- if (pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
3411
+ if (isRecord(pkg) && pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
3239
3412
  return pkg.version;
3240
3413
  }
3241
3414
  } catch {
@@ -3319,11 +3492,25 @@ function resolvesToClaudeSettings(path, env = process.env) {
3319
3492
 
3320
3493
  // src/detectors/policy-diff.ts
3321
3494
  import { parse } from "yaml";
3495
+ function verifyBlock(v) {
3496
+ if (!isRecord(v)) return void 0;
3497
+ return {
3498
+ ...typeof v.command === "string" ? { command: v.command } : {},
3499
+ ...typeof v.budget === "number" ? { budget: v.budget } : {},
3500
+ ...Array.isArray(v.inputs) ? { inputs: v.inputs.filter((g) => typeof g === "string") } : {},
3501
+ ...typeof v.backend === "string" ? { backend: v.backend } : {},
3502
+ ...typeof v.image === "string" ? { image: v.image } : {}
3503
+ };
3504
+ }
3505
+ function ruleOverrides(v) {
3506
+ if (!isRecord(v)) return {};
3507
+ return Object.fromEntries(Object.entries(v).map(([name, cfg]) => [name, isRecord(cfg) ? cfg : {}]));
3508
+ }
3322
3509
  var globs2 = (list2) => Array.isArray(list2) ? list2.filter((g) => typeof g === "string").map(normalizeGlob) : [];
3323
3510
  function safeParse(src) {
3324
3511
  try {
3325
3512
  const v = parse(src);
3326
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
3513
+ if (isRecord(v)) return v;
3327
3514
  return null;
3328
3515
  } catch {
3329
3516
  return null;
@@ -3334,18 +3521,24 @@ function effective(raw) {
3334
3521
  const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
3335
3522
  const base = defaultPolicy(version);
3336
3523
  const rules = { ...base.rules };
3337
- for (const [name, cfg] of Object.entries(raw.rules ?? {})) {
3338
- const over = cfg && typeof cfg === "object" && !Array.isArray(cfg) ? cfg : {};
3339
- rules[name] = { ...rules[name] ?? {}, ...over, ...over.exclude ? { exclude: globs2(over.exclude) } : {} };
3524
+ for (const [name, over] of Object.entries(ruleOverrides(raw.rules))) {
3525
+ rules[name] = {
3526
+ ...rules[name] ?? {},
3527
+ ...over.severity !== void 0 ? { severity: over.severity } : {},
3528
+ ...over.enabled !== void 0 ? { enabled: over.enabled } : {},
3529
+ ...over.exclude !== void 0 ? { exclude: globs2(over.exclude) } : {}
3530
+ };
3340
3531
  }
3341
- const userProtected = raw.protected && typeof raw.protected === "object" ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
3532
+ const userProtected = isRecord(raw.protected) ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
3533
+ const signoff = isRecord(raw.signoff) ? raw.signoff : {};
3534
+ const requiredFor = [signoff.required_for, signoff.requiredFor].find(Array.isArray);
3342
3535
  return {
3343
3536
  version,
3344
3537
  rules,
3345
3538
  ignore: raw.ignore ? globs2(raw.ignore) : base.ignore ?? [],
3346
3539
  protected: mergeProtected(base.protected, userProtected),
3347
- requiredFor: raw.signoff?.required_for ?? raw.signoff?.requiredFor ?? base.signoff.requiredFor,
3348
- ledger: typeof raw.signoff?.ledger === "string" ? raw.signoff.ledger : base.signoff.ledger
3540
+ requiredFor: requiredFor ? requiredFor.filter((s) => typeof s === "string") : base.signoff.requiredFor,
3541
+ ledger: typeof signoff.ledger === "string" ? signoff.ledger : base.signoff.ledger
3349
3542
  };
3350
3543
  }
3351
3544
  function policyWeakening(before, after) {
@@ -3400,8 +3593,8 @@ function policyWeakening(before, after) {
3400
3593
  if (ae.ledger !== be.ledger) {
3401
3594
  reasons.push(`sign-off ledger moved (${be.ledger} \u2192 ${ae.ledger}) \u2014 the local layer would trust a different file`);
3402
3595
  }
3403
- const bv = b.verify;
3404
- const av = a.verify;
3596
+ const bv = verifyBlock(b.verify);
3597
+ const av = verifyBlock(a.verify);
3405
3598
  if (bv?.command) {
3406
3599
  if (!av?.command) {
3407
3600
  reasons.push("verify block removed \u2014 pristine-suite re-execution disabled");
@@ -4159,7 +4352,7 @@ function scriptWeakening(before, after, opts = {}) {
4159
4352
  const evidence = (trace?.line ?? before.find((l) => checkIdentity(stripComment(l).trim().replace(/^!\s+/, "").replace(PREFIX, "")) === id) ?? id).trim();
4160
4353
  out2.push({ reason: `the check invocation \`${id}\` was ${how}`, evidence });
4161
4354
  }
4162
- const floor = beforeInv.filter((i) => i.state === "live" && i.pin && PLAIN_SEMVER.test(i.pin)).map((i) => i.pin).sort((a, b) => compareVersions(a, b) ?? 0)[0];
4355
+ const floor = beforeInv.flatMap((i) => i.state === "live" && i.pin && PLAIN_SEMVER.test(i.pin) ? [i.pin] : []).sort((a, b) => compareVersions(a, b) ?? 0)[0];
4163
4356
  if (floor) {
4164
4357
  for (const inv of afterInv) {
4165
4358
  if (inv.state !== "live" || !inv.pin) continue;
@@ -4555,7 +4748,10 @@ function destinations(cmd, args, isDir) {
4555
4748
  positional.push(a);
4556
4749
  }
4557
4750
  const base = (p) => p.replace(/\/+$/, "").split("/").pop() ?? p;
4558
- if (target !== null) return positional.map((p) => `${target.replace(/\/+$/, "")}/${base(p)}`);
4751
+ if (target !== null) {
4752
+ const dir = target.replace(/\/+$/, "");
4753
+ return positional.map((p) => `${dir}/${base(p)}`);
4754
+ }
4559
4755
  if (positional.length < 2) return [];
4560
4756
  const dest = positional[positional.length - 1];
4561
4757
  const srcs = positional.slice(0, -1);
@@ -4569,16 +4765,18 @@ function expandShellPath(t, env = process.env) {
4569
4765
  }
4570
4766
  function hookTests(policy, ctx) {
4571
4767
  const cwd = ctx?.cwd;
4572
- const onDisk = cwd !== void 0 && existsSync2(cwd);
4573
- const root = onDisk ? canonicalPath(cwd) : cwd;
4768
+ const diskCwd = cwd !== void 0 && existsSync2(cwd) ? cwd : void 0;
4769
+ const onDisk = diskCwd !== void 0;
4770
+ const root = diskCwd !== void 0 ? canonicalPath(diskCwd) : cwd;
4574
4771
  const memo = /* @__PURE__ */ new Map();
4575
4772
  const rel = (t) => {
4576
4773
  const hit = memo.get(t);
4577
4774
  if (hit !== void 0) return hit;
4578
4775
  let p = expandShellPath(t.replace(/^["']|["']$/g, ""));
4579
4776
  const pathy = isAbsolute4(p) || p.includes("/") && !/^[-:]/.test(p);
4580
- if (pathy && (isAbsolute4(p) || onDisk)) {
4581
- p = canonicalPath(isAbsolute4(p) ? p : resolve2(cwd, p));
4777
+ const abs2 = isAbsolute4(p) ? p : diskCwd !== void 0 ? resolve2(diskCwd, p) : null;
4778
+ if (pathy && abs2 !== null) {
4779
+ p = canonicalPath(abs2);
4582
4780
  if (root !== void 0) {
4583
4781
  if (p === root) p = ".";
4584
4782
  else if (p.startsWith(root + "/")) p = p.slice(root.length + 1);
@@ -4743,7 +4941,7 @@ function xargsWritesHook(toks, fed, policy, ctx) {
4743
4941
  }
4744
4942
 
4745
4943
  // src/detectors/hook-tampering.ts
4746
- var RULE6 = "hook-tampering";
4944
+ var RULE7 = "hook-tampering";
4747
4945
  var ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
4748
4946
  var EVENTS = ["PreToolUse", "Stop"];
4749
4947
  var isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -4959,7 +5157,7 @@ function coverage(cs) {
4959
5157
  const live = cs.filter((c) => c.runs);
4960
5158
  if (live.length === 0) return "none";
4961
5159
  if (live.some((c) => c.tools === null)) return null;
4962
- return new Set(live.flatMap((c) => [...c.tools]));
5160
+ return new Set(live.flatMap((c) => c.tools ? [...c.tools] : []));
4963
5161
  }
4964
5162
  function duplicateJsonKeys(src) {
4965
5163
  const dups = [];
@@ -5072,7 +5270,7 @@ function settingsWeakening(before, after, local) {
5072
5270
  const schema = schemaProblem(e.group, e.h);
5073
5271
  if (schema) {
5074
5272
  reasons.push(`a ${e.event} hook entry carries a value the runtime's schema rejects (${schema}) \u2014 the runtime continues without the broken file, the gate included`);
5075
- } else if (e.event === "PreToolUse" || e.event === "Stop") {
5273
+ } else if ((e.event === "PreToolUse" || e.event === "Stop") && isObj(e.h)) {
5076
5274
  reasons.push(`a ${e.event} hook entry beside the gate ${siblingProblem(e.h)}`);
5077
5275
  }
5078
5276
  }
@@ -5090,7 +5288,7 @@ function claudeSettingsFindings(c, policy) {
5090
5288
  if (c.after == null) return null;
5091
5289
  const after = parseSettings(c.after);
5092
5290
  const before = c.before != null ? parseSettings(c.before) : null;
5093
- const finding = (reason) => makeFinding(RULE6, policy, {
5291
+ const finding = (reason) => makeFinding(RULE7, policy, {
5094
5292
  file: c.path,
5095
5293
  message: `The Claude Code hook wiring was weakened: ${reason}.`,
5096
5294
  evidence: reason,
@@ -5111,10 +5309,10 @@ function claudeSettingsFindings(c, policy) {
5111
5309
  function policyFindings(c, policy) {
5112
5310
  if (c.after == null) return null;
5113
5311
  const added = c.before == null;
5114
- const reasons = added ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
5312
+ const reasons = c.before == null ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
5115
5313
  if (reasons === null) return null;
5116
5314
  return reasons.map(
5117
- (reason) => makeFinding(RULE6, policy, {
5315
+ (reason) => makeFinding(RULE7, policy, {
5118
5316
  file: c.path,
5119
5317
  message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
5120
5318
  evidence: reason,
@@ -5177,7 +5375,7 @@ function configFindings(c, changes, policy, ctx) {
5177
5375
  reasons = after === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : isLefthook(c.path) ? lefthookWeakening(before, after) : preCommitWeakening(before, after);
5178
5376
  }
5179
5377
  return reasons.map(
5180
- (reason) => makeFinding(RULE6, policy, {
5378
+ (reason) => makeFinding(RULE7, policy, {
5181
5379
  file: c.path,
5182
5380
  message: `The hook configuration was weakened: ${reason}.`,
5183
5381
  evidence: reason,
@@ -5260,7 +5458,7 @@ function huskyRuntimeWrite(path, content, ctx) {
5260
5458
  const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
5261
5459
  const entry = inspectRel(join6(ctx.cwd, root), ".husky/pre-commit");
5262
5460
  if (entry.kind !== "file") return false;
5263
- const gate = textOf(entry);
5461
+ const gate = textOf2(entry);
5264
5462
  if (gate == null) return false;
5265
5463
  return invocations(gate.split("\n"), { errexit: true }).some(
5266
5464
  (i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
@@ -5269,7 +5467,7 @@ function huskyRuntimeWrite(path, content, ctx) {
5269
5467
  var isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
5270
5468
  var isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
5271
5469
  var hookTampering = {
5272
- id: RULE6,
5470
+ id: RULE7,
5273
5471
  surface: ["file", "command"],
5274
5472
  certainty: "mechanical",
5275
5473
  run(changes, policy, _view, ctx) {
@@ -5282,7 +5480,7 @@ var hookTampering = {
5282
5480
  if (isPackageJson(c.path) && c.before != null && c.after != null) {
5283
5481
  for (const reason of packageJsonWeakening(c.before, c.after)) {
5284
5482
  out2.push(
5285
- makeFinding(RULE6, policy, {
5483
+ makeFinding(RULE7, policy, {
5286
5484
  file: c.path,
5287
5485
  message: `The hook installation was weakened: ${reason}.`,
5288
5486
  evidence: reason,
@@ -5295,7 +5493,7 @@ var hookTampering = {
5295
5493
  }
5296
5494
  if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
5297
5495
  out2.push(
5298
- makeFinding(RULE6, policy, {
5496
+ makeFinding(RULE7, policy, {
5299
5497
  file: c.path,
5300
5498
  message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
5301
5499
  evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
@@ -5305,7 +5503,7 @@ var hookTampering = {
5305
5503
  }
5306
5504
  if (c.op === "delete") {
5307
5505
  out2.push(
5308
- makeFinding(RULE6, policy, {
5506
+ makeFinding(RULE7, policy, {
5309
5507
  file: c.path,
5310
5508
  message: "A protected hook/policy file was deleted.",
5311
5509
  evidence: c.path,
@@ -5314,7 +5512,7 @@ var hookTampering = {
5314
5512
  );
5315
5513
  } else if (c.op === "rename" && !renameKept(c)) {
5316
5514
  out2.push(
5317
- makeFinding(RULE6, policy, {
5515
+ makeFinding(RULE7, policy, {
5318
5516
  file: c.path,
5319
5517
  message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
5320
5518
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -5328,7 +5526,7 @@ var hookTampering = {
5328
5526
  if (c.before != null && c.after != null) {
5329
5527
  for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
5330
5528
  out2.push(
5331
- makeFinding(RULE6, policy, {
5529
+ makeFinding(RULE7, policy, {
5332
5530
  file: c.path,
5333
5531
  message: `CODEOWNERS was weakened: ${reason}.`,
5334
5532
  evidence: reason,
@@ -5344,12 +5542,12 @@ var hookTampering = {
5344
5542
  const husky = isHuskyScript(c.path);
5345
5543
  const opts = husky ? { errexit: true } : {};
5346
5544
  const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
5347
- if (beforePin !== null) {
5545
+ if (beforePin !== null && c.after != null) {
5348
5546
  const afterPin = initScriptPin(c.after);
5349
5547
  const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
5350
5548
  if (!kept) {
5351
5549
  out2.push(
5352
- makeFinding(RULE6, policy, {
5550
+ makeFinding(RULE7, policy, {
5353
5551
  file: c.path,
5354
5552
  message: "A protected hook script no longer matches the shape `tamperward init` writes.",
5355
5553
  evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
@@ -5372,7 +5570,7 @@ var hookTampering = {
5372
5570
  if (shebang || !live) {
5373
5571
  const why = shebang ?? (gate.length ? `its \`${gate[0].identity}\` is ${gate[0].state}${gate[0].why ? ` (${gate[0].why})` : ""}` : "it runs no `tamperward check`");
5374
5572
  out2.push(
5375
- makeFinding(RULE6, policy, {
5573
+ makeFinding(RULE7, policy, {
5376
5574
  file: c.path,
5377
5575
  message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
5378
5576
  evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or make the gate live in it`,
@@ -5381,7 +5579,7 @@ var hookTampering = {
5381
5579
  );
5382
5580
  } else if (earlyExit) {
5383
5581
  out2.push(
5384
- makeFinding(RULE6, policy, {
5582
+ makeFinding(RULE7, policy, {
5385
5583
  file: c.path,
5386
5584
  line: earlyExit.newLine ?? void 0,
5387
5585
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -5403,7 +5601,7 @@ var hookTampering = {
5403
5601
  deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
5404
5602
  ].filter((d) => d !== null);
5405
5603
  out2.push(
5406
- makeFinding(RULE6, policy, {
5604
+ makeFinding(RULE7, policy, {
5407
5605
  file: c.path,
5408
5606
  message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
5409
5607
  evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or restore it`,
@@ -5415,7 +5613,7 @@ var hookTampering = {
5415
5613
  }
5416
5614
  if (shebangChanged && c.after != null) {
5417
5615
  out2.push(
5418
- makeFinding(RULE6, policy, {
5616
+ makeFinding(RULE7, policy, {
5419
5617
  file: c.path,
5420
5618
  line: 1,
5421
5619
  message: `A protected hook's interpreter was changed: ${shebang}.`,
@@ -5426,7 +5624,7 @@ var hookTampering = {
5426
5624
  }
5427
5625
  if (earlyExit) {
5428
5626
  out2.push(
5429
- makeFinding(RULE6, policy, {
5627
+ makeFinding(RULE7, policy, {
5430
5628
  file: c.path,
5431
5629
  line: earlyExit.newLine ?? void 0,
5432
5630
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -5438,7 +5636,7 @@ var hookTampering = {
5438
5636
  const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
5439
5637
  if (deadGuard) {
5440
5638
  out2.push(
5441
- makeFinding(RULE6, policy, {
5639
+ makeFinding(RULE7, policy, {
5442
5640
  file: c.path,
5443
5641
  line: deadGuard.newLine ?? void 0,
5444
5642
  message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
@@ -5450,7 +5648,7 @@ var hookTampering = {
5450
5648
  const lost = scriptWeakening(beforeLines, afterLines, opts);
5451
5649
  if (lost.length) {
5452
5650
  out2.push(
5453
- makeFinding(RULE6, policy, {
5651
+ makeFinding(RULE7, policy, {
5454
5652
  file: c.path,
5455
5653
  message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
5456
5654
  evidence: lost[0].evidence,
@@ -5469,7 +5667,7 @@ var hookTampering = {
5469
5667
  const m = l.content.match(INLINE);
5470
5668
  if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
5471
5669
  out2.push(
5472
- makeFinding(RULE6, policy, {
5670
+ makeFinding(RULE7, policy, {
5473
5671
  file: c.path,
5474
5672
  line: l.newLine ?? void 0,
5475
5673
  message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
@@ -5479,7 +5677,7 @@ var hookTampering = {
5479
5677
  );
5480
5678
  } else if (/enabled:\s*false\b/.test(l.content)) {
5481
5679
  out2.push(
5482
- makeFinding(RULE6, policy, {
5680
+ makeFinding(RULE7, policy, {
5483
5681
  file: c.path,
5484
5682
  line: l.newLine ?? void 0,
5485
5683
  message: "A rule was disabled (enabled: false) in the policy.",
@@ -5497,7 +5695,7 @@ var hookTampering = {
5497
5695
  const hiding = updateIndexHiding(toks, policy);
5498
5696
  if (hiding) {
5499
5697
  out2.push(
5500
- makeFinding(RULE6, policy, {
5698
+ makeFinding(RULE7, policy, {
5501
5699
  file: repoRelative(hiding.file, ctx),
5502
5700
  message: `Hook tampering via shell: ${hiding.reason}.`,
5503
5701
  evidence: seg,
@@ -5513,7 +5711,7 @@ var hookTampering = {
5513
5711
  const file = target ? repoRelative(target, ctx) : void 0;
5514
5712
  const outside = file !== void 0 && (isAbsolute5(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
5515
5713
  out2.push(
5516
- makeFinding(RULE6, policy, {
5714
+ makeFinding(RULE7, policy, {
5517
5715
  ...file !== void 0 ? { file } : {},
5518
5716
  message: `Hook tampering via shell: ${why}${outside}.`,
5519
5717
  evidence: seg,
@@ -5530,11 +5728,11 @@ var hookTampering = {
5530
5728
  };
5531
5729
 
5532
5730
  // src/detectors/test-deletion.ts
5533
- import ts5 from "typescript";
5731
+ import ts6 from "typescript";
5534
5732
 
5535
5733
  // src/detectors/suite-config.ts
5536
5734
  import picomatch3 from "picomatch";
5537
- import ts4 from "typescript";
5735
+ import ts5 from "typescript";
5538
5736
  var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
5539
5737
  var JEST_DEFAULT_IGNORE = ["/node_modules/"];
5540
5738
  var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
@@ -5565,18 +5763,18 @@ var PYTEST_CANONICAL_SAMPLES = [
5565
5763
  "tests/conftest.py"
5566
5764
  ];
5567
5765
  function keyName2(name) {
5568
- if (ts4.isIdentifier(name) || ts4.isStringLiteral(name)) return name.text;
5766
+ if (ts5.isIdentifier(name) || ts5.isStringLiteral(name)) return name.text;
5569
5767
  return null;
5570
5768
  }
5571
- var isStr = (e) => ts4.isStringLiteral(e) || ts4.isNoSubstitutionTemplateLiteral(e);
5769
+ var isStr = (e) => ts5.isStringLiteral(e) || ts5.isNoSubstitutionTemplateLiteral(e);
5572
5770
  function literals2(e) {
5573
5771
  if (isStr(e)) return { items: [e.text], opaque: false };
5574
- if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5772
+ if (!ts5.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5575
5773
  const items = [];
5576
5774
  let opaque = false;
5577
5775
  for (const el of e.elements) {
5578
5776
  if (isStr(el)) items.push(el.text);
5579
- else if (ts4.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5777
+ else if (ts5.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5580
5778
  items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5581
5779
  } else opaque = true;
5582
5780
  }
@@ -5584,10 +5782,10 @@ function literals2(e) {
5584
5782
  }
5585
5783
  function ownerKey2(node) {
5586
5784
  const obj = node.parent;
5587
- if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
5785
+ if (!obj || !ts5.isObjectLiteralExpression(obj)) return null;
5588
5786
  let q = obj.parent;
5589
- while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
5590
- return q && ts4.isPropertyAssignment(q) ? keyName2(q.name) : null;
5787
+ while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
5788
+ return q && ts5.isPropertyAssignment(q) ? keyName2(q.name) : null;
5591
5789
  }
5592
5790
  function asExpression2(src) {
5593
5791
  const t = src.trim();
@@ -5622,13 +5820,12 @@ function effectivePytestFile(files) {
5622
5820
  function effectivePytestConfig(path, ctx) {
5623
5821
  if (path.includes("/")) return false;
5624
5822
  const base = path;
5625
- const ORDER = PYTEST_INI_ORDER;
5626
- const rank = ORDER.indexOf(base);
5823
+ const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
5627
5824
  if (rank < 0) return false;
5628
5825
  const files = ctx?.trackedFiles;
5629
5826
  if (!files) return true;
5630
5827
  const rootNames = new Set(files.filter((f) => !f.includes("/")));
5631
- return !ORDER.slice(0, rank).some((n) => rootNames.has(n));
5828
+ return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
5632
5829
  }
5633
5830
  function pytestBlock(src) {
5634
5831
  const lines = src.split(/\r?\n/);
@@ -5728,15 +5925,15 @@ function collect(root, runner) {
5728
5925
  projectList = e;
5729
5926
  };
5730
5927
  const visit = (node) => {
5731
- if (ts4.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5928
+ if (ts5.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
5732
5929
  sel.present = true;
5733
5930
  sel.opaque = true;
5734
5931
  }
5735
- if (ts4.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts4.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5932
+ if (ts5.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts5.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
5736
5933
  sel.present = true;
5737
5934
  sel.opaque = true;
5738
5935
  }
5739
- if (ts4.isPropertyAssignment(node)) {
5936
+ if (ts5.isPropertyAssignment(node)) {
5740
5937
  const k = keyName2(node.name);
5741
5938
  if (runner === "jest") {
5742
5939
  if (k === "testMatch") sel.include = take(k, node.initializer);
@@ -5766,7 +5963,7 @@ function collect(root, runner) {
5766
5963
  }
5767
5964
  }
5768
5965
  }
5769
- ts4.forEachChild(node, visit);
5966
+ ts5.forEachChild(node, visit);
5770
5967
  };
5771
5968
  visit(root);
5772
5969
  if (projectList) sel.projects = readProjects(projectList, sel, runner);
@@ -5775,30 +5972,30 @@ function collect(root, runner) {
5775
5972
  }
5776
5973
  function unwrapConfigCall(e) {
5777
5974
  let x = e;
5778
- while (ts4.isParenthesizedExpression(x) || ts4.isAsExpression(x) || x.kind === ts4.SyntaxKind.SatisfiesExpression) {
5975
+ while (ts5.isParenthesizedExpression(x) || ts5.isAsExpression(x) || ts5.isSatisfiesExpression(x)) {
5779
5976
  x = x.expression;
5780
5977
  }
5781
- if (ts4.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5978
+ if (ts5.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
5782
5979
  return x;
5783
5980
  }
5784
5981
  var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
5785
5982
  function spreadsIntoConfig(node, runner) {
5786
5983
  const obj = node.parent;
5787
- if (!ts4.isObjectLiteralExpression(obj)) return false;
5788
- const keys = obj.properties.map((p) => ts4.isPropertyAssignment(p) ? keyName2(p.name) : null);
5984
+ if (!ts5.isObjectLiteralExpression(obj)) return false;
5985
+ const keys = obj.properties.map((p) => ts5.isPropertyAssignment(p) ? keyName2(p.name) : null);
5789
5986
  if (runner === "vitest") {
5790
5987
  if (keys.includes("test")) return true;
5791
5988
  let q = obj.parent;
5792
- while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
5793
- return !!q && ts4.isPropertyAssignment(q) && keyName2(q.name) === "test";
5989
+ while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
5990
+ return !!q && ts5.isPropertyAssignment(q) && keyName2(q.name) === "test";
5794
5991
  }
5795
5992
  return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
5796
5993
  }
5797
5994
  function extendsOf(el) {
5798
5995
  for (const p of el.properties) {
5799
- if (!ts4.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
5800
- if (p.initializer.kind === ts4.SyntaxKind.TrueKeyword) return true;
5801
- if (p.initializer.kind === ts4.SyntaxKind.FalseKeyword) return null;
5996
+ if (!ts5.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
5997
+ if (p.initializer.kind === ts5.SyntaxKind.TrueKeyword) return true;
5998
+ if (p.initializer.kind === ts5.SyntaxKind.FalseKeyword) return null;
5802
5999
  return "path";
5803
6000
  }
5804
6001
  return null;
@@ -5819,14 +6016,14 @@ function readProject(el, root, runner) {
5819
6016
  return p;
5820
6017
  }
5821
6018
  function readProjects(e, root, runner) {
5822
- if (!ts4.isArrayLiteralExpression(e)) {
6019
+ if (!ts5.isArrayLiteralExpression(e)) {
5823
6020
  root.opaque = true;
5824
6021
  return [];
5825
6022
  }
5826
6023
  const list2 = [];
5827
6024
  for (const el of e.elements) {
5828
6025
  const lit = unwrapConfigCall(el);
5829
- if (ts4.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
6026
+ if (ts5.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
5830
6027
  else root.opaque = true;
5831
6028
  }
5832
6029
  return list2;
@@ -5834,14 +6031,14 @@ function readProjects(e, root, runner) {
5834
6031
  function workspaceArray(sf) {
5835
6032
  for (const st of sf.statements) {
5836
6033
  let e;
5837
- if (ts4.isExportAssignment(st)) e = st.expression;
5838
- else if (ts4.isExpressionStatement(st) && ts4.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
6034
+ if (ts5.isExportAssignment(st)) e = st.expression;
6035
+ else if (ts5.isExpressionStatement(st) && ts5.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
5839
6036
  if (!e) continue;
5840
- while (ts4.isParenthesizedExpression(e) || ts4.isAsExpression(e) || e.kind === ts4.SyntaxKind.SatisfiesExpression) {
6037
+ while (ts5.isParenthesizedExpression(e) || ts5.isAsExpression(e) || ts5.isSatisfiesExpression(e)) {
5841
6038
  e = e.expression;
5842
6039
  }
5843
- if (ts4.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
5844
- return ts4.isArrayLiteralExpression(e) ? e : null;
6040
+ if (ts5.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
6041
+ return ts5.isArrayLiteralExpression(e) ? e : null;
5845
6042
  }
5846
6043
  return null;
5847
6044
  }
@@ -5849,7 +6046,7 @@ function parseSelection(src, runner, path = "") {
5849
6046
  let sel = empty();
5850
6047
  if (runner === "pytest") return pytestSelection(src);
5851
6048
  try {
5852
- const sf = ts4.createSourceFile("cfg.ts", asExpression2(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
6049
+ const sf = ts5.createSourceFile("cfg.ts", asExpression2(src), ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
5853
6050
  if (runner === "vitest" && isWorkspaceFile(path)) {
5854
6051
  const arr = workspaceArray(sf);
5855
6052
  if (arr) {
@@ -5943,7 +6140,7 @@ function suiteNarrowings(before, after, path, samples) {
5943
6140
  }
5944
6141
 
5945
6142
  // src/detectors/test-deletion.ts
5946
- var RULE7 = "test-deletion";
6143
+ var RULE8 = "test-deletion";
5947
6144
  var JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
5948
6145
  var TEST_DEFS = {
5949
6146
  py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
@@ -5956,21 +6153,21 @@ var TEST_DEFS = {
5956
6153
  cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
5957
6154
  };
5958
6155
  function calleeName(expr) {
5959
- if (ts5.isIdentifier(expr)) return expr.text;
5960
- if (ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression)) {
6156
+ if (ts6.isIdentifier(expr)) return expr.text;
6157
+ if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression)) {
5961
6158
  return expr.expression.text;
5962
6159
  }
5963
6160
  return null;
5964
6161
  }
5965
6162
  var TABLE_METHOD = /^(?:each|for)$/;
5966
- var isEachOf = (expr) => ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
5967
- var isDescribeEach = (expr) => ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
6163
+ var isEachOf = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
6164
+ var isDescribeEach = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
5968
6165
  function loopRows(iterable) {
5969
- if (ts5.isArrayLiteralExpression(iterable)) {
6166
+ if (ts6.isArrayLiteralExpression(iterable)) {
5970
6167
  let n = 0;
5971
6168
  let open = false;
5972
6169
  for (const el of iterable.elements) {
5973
- if (ts5.isSpreadElement(el)) open = true;
6170
+ if (ts6.isSpreadElement(el)) open = true;
5974
6171
  else n++;
5975
6172
  }
5976
6173
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -5982,20 +6179,20 @@ function templateRows(node) {
5982
6179
  return Math.max(rows, 1);
5983
6180
  }
5984
6181
  function hasSubstantiveBody(call) {
5985
- const fn = call.arguments.find((a) => ts5.isArrowFunction(a) || ts5.isFunctionExpression(a));
6182
+ const fn = call.arguments.find((a) => ts6.isArrowFunction(a) || ts6.isFunctionExpression(a));
5986
6183
  if (!fn) return true;
5987
6184
  const body = fn.body;
5988
- const text = ts5.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
6185
+ const text = ts6.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
5989
6186
  return text.split("\n").some((l) => isSignificantLine(l.trim(), "js"));
5990
6187
  }
5991
6188
  function eachRows(call) {
5992
6189
  const arg = call.arguments[0];
5993
6190
  if (!arg) return { n: 1, open: true };
5994
- if (ts5.isArrayLiteralExpression(arg)) {
6191
+ if (ts6.isArrayLiteralExpression(arg)) {
5995
6192
  let n = 0;
5996
6193
  let open = false;
5997
6194
  for (const el of arg.elements) {
5998
- if (ts5.isSpreadElement(el)) open = true;
6195
+ if (ts6.isSpreadElement(el)) open = true;
5999
6196
  else n++;
6000
6197
  }
6001
6198
  return { n: Math.max(n, open ? 0 : 1), open };
@@ -6010,35 +6207,35 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
6010
6207
  for (const line of src.split("\n")) if (re.test(line)) n2++;
6011
6208
  return { min: n2, open: false };
6012
6209
  }
6013
- const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
6210
+ const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6014
6211
  let n = 0;
6015
6212
  let open = false;
6016
6213
  const visit = (node, mult) => {
6017
- if (ts5.isForOfStatement(node)) {
6214
+ if (ts6.isForOfStatement(node)) {
6018
6215
  const r = loopRows(node.expression);
6019
6216
  if (r.open) open = true;
6020
6217
  visit(node.statement, mult * r.n);
6021
6218
  return;
6022
6219
  }
6023
- if (ts5.isForStatement(node) || ts5.isForInStatement(node) || ts5.isWhileStatement(node) || ts5.isDoStatement(node)) {
6220
+ if (ts6.isForStatement(node) || ts6.isForInStatement(node) || ts6.isWhileStatement(node) || ts6.isDoStatement(node)) {
6024
6221
  open = true;
6025
6222
  }
6026
- if (ts5.isCallExpression(node)) {
6223
+ if (ts6.isCallExpression(node)) {
6027
6224
  const callee = node.expression;
6028
- if (ts5.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6225
+ if (ts6.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
6029
6226
  const r = loopRows(callee.expression);
6030
6227
  if (r.open) open = true;
6031
6228
  for (const a of node.arguments) visit(a, mult * r.n);
6032
6229
  return;
6033
6230
  }
6034
6231
  const head = node.expression;
6035
- if (ts5.isCallExpression(head) && isDescribeEach(head.expression)) {
6232
+ if (ts6.isCallExpression(head) && isDescribeEach(head.expression)) {
6036
6233
  const r = eachRows(head);
6037
6234
  if (r.open) open = true;
6038
6235
  for (const a of node.arguments) visit(a, mult * r.n);
6039
6236
  return;
6040
6237
  }
6041
- if (ts5.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6238
+ if (ts6.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
6042
6239
  for (const a of node.arguments) visit(a, mult * templateRows(head));
6043
6240
  return;
6044
6241
  }
@@ -6050,10 +6247,10 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
6050
6247
  const name = calleeName(node.expression);
6051
6248
  if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
6052
6249
  }
6053
- } else if (ts5.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6250
+ } else if (ts6.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
6054
6251
  n += templateRows(node) * mult;
6055
6252
  }
6056
- ts5.forEachChild(node, (c) => visit(c, mult));
6253
+ ts6.forEachChild(node, (c) => visit(c, mult));
6057
6254
  };
6058
6255
  visit(sf, 1);
6059
6256
  return { min: n, open };
@@ -6090,7 +6287,8 @@ function scriptNarrowings(before, after, path) {
6090
6287
  if ((path.split("/").pop() ?? path) !== "package.json") return [];
6091
6288
  const scriptsOf = (src) => {
6092
6289
  try {
6093
- const s = JSON.parse(src ?? "{}").scripts ?? {};
6290
+ const pkg = JSON.parse(src ?? "{}");
6291
+ const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
6094
6292
  return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
6095
6293
  } catch {
6096
6294
  return {};
@@ -6135,7 +6333,7 @@ function leavesGlob(dir, dest, policy, ctx) {
6135
6333
  }
6136
6334
  var SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
6137
6335
  var testDeletion = {
6138
- id: RULE7,
6336
+ id: RULE8,
6139
6337
  surface: ["file", "command"],
6140
6338
  certainty: "mechanical",
6141
6339
  run(changes, policy, _view, ctx) {
@@ -6166,7 +6364,7 @@ var testDeletion = {
6166
6364
  if (c.op === "delete" && isTest) {
6167
6365
  if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
6168
6366
  out2.push(
6169
- makeFinding(RULE7, policy, {
6367
+ makeFinding(RULE8, policy, {
6170
6368
  file: c.path,
6171
6369
  message: "A test file was deleted.",
6172
6370
  evidence: c.path,
@@ -6175,7 +6373,7 @@ var testDeletion = {
6175
6373
  );
6176
6374
  } else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
6177
6375
  out2.push(
6178
- makeFinding(RULE7, policy, {
6376
+ makeFinding(RULE8, policy, {
6179
6377
  file: c.path,
6180
6378
  message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
6181
6379
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -6190,7 +6388,7 @@ var testDeletion = {
6190
6388
  const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
6191
6389
  if (isRelocation(removed, before.min - after.min)) continue;
6192
6390
  out2.push(
6193
- makeFinding(RULE7, policy, {
6391
+ makeFinding(RULE8, policy, {
6194
6392
  file: c.path,
6195
6393
  message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
6196
6394
  evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
@@ -6203,7 +6401,7 @@ var testDeletion = {
6203
6401
  const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
6204
6402
  for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
6205
6403
  out2.push(
6206
- makeFinding(RULE7, policy, {
6404
+ makeFinding(RULE8, policy, {
6207
6405
  file: c.path,
6208
6406
  message: `The runner no longer selects ${n.path}: ${n.reason}.`,
6209
6407
  evidence: `${n.path} dropped from the suite by ${c.path}`,
@@ -6213,7 +6411,7 @@ var testDeletion = {
6213
6411
  }
6214
6412
  for (const n of scriptNarrowings(c.before, c.after, c.path)) {
6215
6413
  out2.push(
6216
- makeFinding(RULE7, policy, {
6414
+ makeFinding(RULE8, policy, {
6217
6415
  file: c.path,
6218
6416
  message: `The test script now narrows the suite: ${n.flag} added to scripts.${n.script}.`,
6219
6417
  evidence: `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${JSON.stringify(n.after)}`,
@@ -6301,7 +6499,7 @@ var testDeletion = {
6301
6499
  }
6302
6500
  if (why) {
6303
6501
  out2.push(
6304
- makeFinding(RULE7, policy, {
6502
+ makeFinding(RULE8, policy, {
6305
6503
  message: `Test removed via shell: ${why}.`,
6306
6504
  evidence,
6307
6505
  remediation: "Fix the code under test. Mutating a spec from the shell is still deleting the test \u2014 and is blocked the same way."
@@ -6316,11 +6514,10 @@ var testDeletion = {
6316
6514
  }
6317
6515
  };
6318
6516
  function pytestPrecedenceFindings(changes, policy, ctx) {
6319
- const order = PYTEST_INI_ORDER;
6320
6517
  const touched = /* @__PURE__ */ new Map();
6321
6518
  for (const c of changes) {
6322
6519
  if (c.kind !== "file" || c.oldPath) continue;
6323
- if (order.includes(c.path)) touched.set(c.path, c);
6520
+ if (PYTEST_INI_ORDER.some((name) => name === c.path)) touched.set(c.path, c);
6324
6521
  }
6325
6522
  if (touched.size === 0) return [];
6326
6523
  const listing = trackedFiles(ctx);
@@ -6328,7 +6525,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6328
6525
  const rootNames = new Set(listing.filter((f) => !f.includes("/")));
6329
6526
  const base = /* @__PURE__ */ new Map();
6330
6527
  const head = /* @__PURE__ */ new Map();
6331
- for (const name of order) {
6528
+ for (const name of PYTEST_INI_ORDER) {
6332
6529
  const c = touched.get(name);
6333
6530
  if (c) {
6334
6531
  base.set(name, c.before);
@@ -6345,7 +6542,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6345
6542
  if (!was || !now || was.path === now.path) return [];
6346
6543
  const samples = runnerSamples(now.path, policy, ctx, "pytest");
6347
6544
  return suiteNarrowings(was.content, now.content, now.path, samples).map(
6348
- (n) => makeFinding(RULE7, policy, {
6545
+ (n) => makeFinding(RULE8, policy, {
6349
6546
  file: now.path,
6350
6547
  message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
6351
6548
  evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
@@ -6355,8 +6552,8 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
6355
6552
  }
6356
6553
 
6357
6554
  // src/detectors/test-content-removal.ts
6358
- import ts6 from "typescript";
6359
- var RULE8 = "test-content-removal";
6555
+ import ts7 from "typescript";
6556
+ var RULE9 = "test-content-removal";
6360
6557
  var MIN_REMOVED_LINES = 3;
6361
6558
  var MIN_REMOVED_ROWS = 2;
6362
6559
  var ws = (s) => s.replace(/\s+/g, "");
@@ -6365,13 +6562,13 @@ function eachTables(src, path) {
6365
6562
  let open = false;
6366
6563
  if (langOf(path) !== "js" && langOf(path) !== null) return { rows, open };
6367
6564
  try {
6368
- const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6565
+ const sf = ts7.createSourceFile("spec.ts", src, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
6369
6566
  const visit = (node) => {
6370
- if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts6.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
6567
+ if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts7.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
6371
6568
  const arg = node.arguments[0];
6372
- if (arg && ts6.isArrayLiteralExpression(arg)) {
6569
+ if (arg && ts7.isArrayLiteralExpression(arg)) {
6373
6570
  for (const el of arg.elements) {
6374
- if (ts6.isSpreadElement(el)) {
6571
+ if (ts7.isSpreadElement(el)) {
6375
6572
  open = true;
6376
6573
  continue;
6377
6574
  }
@@ -6381,7 +6578,7 @@ function eachTables(src, path) {
6381
6578
  }
6382
6579
  } else open = true;
6383
6580
  }
6384
- ts6.forEachChild(node, visit);
6581
+ ts7.forEachChild(node, visit);
6385
6582
  };
6386
6583
  visit(sf);
6387
6584
  } catch {
@@ -6399,7 +6596,7 @@ function significantLinesOrdered(src, path) {
6399
6596
  return out2;
6400
6597
  }
6401
6598
  var testContentRemoval = {
6402
- id: RULE8,
6599
+ id: RULE9,
6403
6600
  surface: ["file"],
6404
6601
  certainty: "mechanical",
6405
6602
  run(changes, policy) {
@@ -6440,7 +6637,7 @@ var testContentRemoval = {
6440
6637
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
6441
6638
  if (goneRows.length >= MIN_REMOVED_ROWS) {
6442
6639
  out2.push(
6443
- makeFinding(RULE8, policy, {
6640
+ makeFinding(RULE9, policy, {
6444
6641
  file: c.path,
6445
6642
  message: `Rows removed from a test table: ${goneRows.length} case(s) gone from an each-table that spreads from elsewhere, and they reappear nowhere in the change.`,
6446
6643
  evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -6451,7 +6648,7 @@ var testContentRemoval = {
6451
6648
  }
6452
6649
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
6453
6650
  out2.push(
6454
- makeFinding(RULE8, policy, {
6651
+ makeFinding(RULE9, policy, {
6455
6652
  file: c.path,
6456
6653
  message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
6457
6654
  evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -6465,7 +6662,7 @@ var testContentRemoval = {
6465
6662
  };
6466
6663
 
6467
6664
  // src/detectors/snapshot-rewrite.ts
6468
- var RULE9 = "snapshot-rewrite";
6665
+ var RULE10 = "snapshot-rewrite";
6469
6666
  var RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
6470
6667
  var PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
6471
6668
  var UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
@@ -6482,12 +6679,12 @@ function namesProtectedSnapshot(seg, policy) {
6482
6679
  return null;
6483
6680
  }
6484
6681
  var snapshotRewrite = {
6485
- id: RULE9,
6682
+ id: RULE10,
6486
6683
  surface: ["command", "file"],
6487
6684
  certainty: "mechanical",
6488
6685
  run(changes, policy) {
6489
6686
  const out2 = [];
6490
- const warn = (input) => out2.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
6687
+ const warn = (input) => out2.push(makeFinding(RULE10, policy, { ...input, defaultSeverity: "warn" }));
6491
6688
  for (const c of changes) {
6492
6689
  if (c.kind === "command") {
6493
6690
  for (const seg of segments(c.raw)) {
@@ -6516,7 +6713,7 @@ var snapshotRewrite = {
6516
6713
  if (c.op === "add" || !isSnap && !wasSnap) continue;
6517
6714
  const what = c.op === "delete" ? "deleted" : c.op === "rename" && !isSnap ? `renamed out of the protected snapshot set (to ${c.path})` : "rewritten";
6518
6715
  warn({
6519
- file: c.op === "rename" && !isSnap ? c.oldPath : c.path,
6716
+ file: c.op === "rename" && !isSnap ? c.oldPath ?? c.path : c.path,
6520
6717
  message: `A recorded expected output was ${what}.`,
6521
6718
  evidence: c.op === "rename" ? `${c.oldPath} -> ${c.path}` : c.path,
6522
6719
  remediation: "If the intended output genuinely changed, a human should confirm the new expectation against the requirement; if the test was failing, fix the code instead of re-recording the expectation from it."
@@ -6527,7 +6724,7 @@ var snapshotRewrite = {
6527
6724
  };
6528
6725
 
6529
6726
  // src/detectors/snapshot-only.ts
6530
- var RULE10 = "snapshot-only-rewrite";
6727
+ var RULE11 = "snapshot-only-rewrite";
6531
6728
  var COMMIT_VIEWS = ["staged", "range"];
6532
6729
  function isSnapshotChange(c, policy) {
6533
6730
  if (c.kind !== "file") return false;
@@ -6536,7 +6733,7 @@ function isSnapshotChange(c, policy) {
6536
6733
  return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
6537
6734
  }
6538
6735
  var snapshotOnlyRewrite = {
6539
- id: RULE10,
6736
+ id: RULE11,
6540
6737
  surface: ["file"],
6541
6738
  certainty: "mechanical",
6542
6739
  run(changes, policy, view) {
@@ -6546,9 +6743,10 @@ var snapshotOnlyRewrite = {
6546
6743
  const snaps = files.filter((c) => isSnapshotChange(c, policy));
6547
6744
  if (snaps.length === 0 || snaps.length !== files.length) return [];
6548
6745
  const first = snaps[0];
6549
- const names = snaps.map((c) => c.kind === "file" ? c.path : "").filter(Boolean).slice(0, 3).join(", ");
6746
+ if (!first) return [];
6747
+ const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
6550
6748
  return [
6551
- makeFinding(RULE10, policy, {
6749
+ makeFinding(RULE11, policy, {
6552
6750
  file: first.path,
6553
6751
  message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
6554
6752
  evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
@@ -6560,8 +6758,8 @@ var snapshotOnlyRewrite = {
6560
6758
  };
6561
6759
 
6562
6760
  // src/detectors/assertion-weakening.ts
6563
- import ts7 from "typescript";
6564
- var RULE11 = "assertion-weakening";
6761
+ import ts8 from "typescript";
6762
+ var RULE12 = "assertion-weakening";
6565
6763
  var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
6566
6764
  var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
6567
6765
  var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
@@ -6569,55 +6767,55 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
6569
6767
  var compact = (s) => s.replace(/\s+/g, "");
6570
6768
  function literalTitle(node) {
6571
6769
  if (!node) return null;
6572
- if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
6770
+ if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) return node.text;
6573
6771
  return null;
6574
6772
  }
6575
6773
  function scriptKindForPath(path) {
6576
6774
  const lower = path.toLowerCase();
6577
- if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
6775
+ if (lower.endsWith(".tsx")) return ts8.ScriptKind.TSX;
6578
6776
  if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
6579
- return ts7.ScriptKind.TS;
6777
+ return ts8.ScriptKind.TS;
6580
6778
  }
6581
- if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
6582
- return ts7.ScriptKind.JS;
6779
+ if (lower.endsWith(".jsx")) return ts8.ScriptKind.JSX;
6780
+ return ts8.ScriptKind.JS;
6583
6781
  }
6584
6782
  function staticFacts(node) {
6585
6783
  if (!node) return null;
6586
- if (node.kind === ts7.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6587
- if (node.kind === ts7.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6588
- if (node.kind === ts7.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6589
- if (ts7.isIdentifier(node) && node.text === "undefined") {
6784
+ if (node.kind === ts8.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6785
+ if (node.kind === ts8.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6786
+ if (node.kind === ts8.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6787
+ if (ts8.isIdentifier(node) && node.text === "undefined") {
6590
6788
  return { defined: false, truthy: false };
6591
6789
  }
6592
- if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
6593
- if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
6790
+ if (ts8.isVoidExpression(node)) return { defined: false, truthy: false };
6791
+ if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) {
6594
6792
  return { defined: true, truthy: node.text.length > 0 };
6595
6793
  }
6596
- if (ts7.isNumericLiteral(node)) {
6794
+ if (ts8.isNumericLiteral(node)) {
6597
6795
  return { defined: true, truthy: Number(node.text) !== 0 };
6598
6796
  }
6599
- if (ts7.isBigIntLiteral(node)) {
6797
+ if (ts8.isBigIntLiteral(node)) {
6600
6798
  return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
6601
6799
  }
6602
- if (ts7.isPrefixUnaryExpression(node) && (node.operator === ts7.SyntaxKind.MinusToken || node.operator === ts7.SyntaxKind.PlusToken) && ts7.isNumericLiteral(node.operand)) {
6603
- const value = Number(node.operand.text) * (node.operator === ts7.SyntaxKind.MinusToken ? -1 : 1);
6800
+ if (ts8.isPrefixUnaryExpression(node) && (node.operator === ts8.SyntaxKind.MinusToken || node.operator === ts8.SyntaxKind.PlusToken) && ts8.isNumericLiteral(node.operand)) {
6801
+ const value = Number(node.operand.text) * (node.operator === ts8.SyntaxKind.MinusToken ? -1 : 1);
6604
6802
  return { defined: true, truthy: value !== 0 };
6605
6803
  }
6606
- if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
6804
+ if (ts8.isObjectLiteralExpression(node) || ts8.isArrayLiteralExpression(node) || ts8.isRegularExpressionLiteral(node) || ts8.isArrowFunction(node) || ts8.isFunctionExpression(node) || ts8.isClassExpression(node) || ts8.isNewExpression(node)) {
6607
6805
  return { defined: true, truthy: true };
6608
6806
  }
6609
6807
  return null;
6610
6808
  }
6611
6809
  function expectAssertion(node, sf) {
6612
- if (!ts7.isPropertyAccessExpression(node.expression)) return null;
6810
+ if (!ts8.isPropertyAccessExpression(node.expression)) return null;
6613
6811
  const matcher = node.expression.name.text;
6614
6812
  let receiver = node.expression.expression;
6615
6813
  let polarity = "";
6616
- if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6814
+ if (ts8.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6617
6815
  polarity = "not";
6618
6816
  receiver = receiver.expression;
6619
6817
  }
6620
- if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6818
+ if (!ts8.isCallExpression(receiver) || !ts8.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6621
6819
  return null;
6622
6820
  }
6623
6821
  const subject = compact(receiver.arguments[0].getText(sf));
@@ -6633,41 +6831,41 @@ function expectAssertion(node, sf) {
6633
6831
  };
6634
6832
  }
6635
6833
  function simpleCallName(node) {
6636
- return ts7.isIdentifier(node.expression) ? node.expression.text : null;
6834
+ return ts8.isIdentifier(node.expression) ? node.expression.text : null;
6637
6835
  }
6638
6836
  function callbackFor(node) {
6639
6837
  const cb = node.arguments[1];
6640
- return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
6838
+ return cb && (ts8.isArrowFunction(cb) || ts8.isFunctionExpression(cb)) ? cb : null;
6641
6839
  }
6642
6840
  function blocks(src, path) {
6643
6841
  const out2 = /* @__PURE__ */ new Map();
6644
6842
  if (langOf(path) !== "js") return out2;
6645
6843
  try {
6646
- const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
6844
+ const sf = ts8.createSourceFile(path, src, ts8.ScriptTarget.Latest, true, scriptKindForPath(path));
6647
6845
  const visit = (node, suites) => {
6648
- if (ts7.isCallExpression(node)) {
6846
+ if (ts8.isCallExpression(node)) {
6649
6847
  const call = simpleCallName(node);
6650
6848
  const title = literalTitle(node.arguments[0]);
6651
6849
  const cb = callbackFor(node);
6652
6850
  if (call && SUITE_CALLS.has(call) && title !== null && cb) {
6653
6851
  const nextSuites = [...suites, title];
6654
- ts7.forEachChild(cb, (child) => visit(child, nextSuites));
6852
+ ts8.forEachChild(cb, (child) => visit(child, nextSuites));
6655
6853
  return;
6656
6854
  }
6657
6855
  if (call && TEST_CALLS.has(call) && title !== null && cb) {
6658
6856
  const assertions = [];
6659
6857
  const collect2 = (child) => {
6660
- if (child !== cb && ts7.isCallExpression(child)) {
6858
+ if (child !== cb && ts8.isCallExpression(child)) {
6661
6859
  const nestedCall = simpleCallName(child);
6662
6860
  if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
6663
6861
  return;
6664
6862
  }
6665
6863
  }
6666
- if (ts7.isCallExpression(child)) {
6864
+ if (ts8.isCallExpression(child)) {
6667
6865
  const a = expectAssertion(child, sf);
6668
6866
  if (a) assertions.push(a);
6669
6867
  }
6670
- ts7.forEachChild(child, collect2);
6868
+ ts8.forEachChild(child, collect2);
6671
6869
  };
6672
6870
  collect2(cb);
6673
6871
  const identity2 = [...suites, title].join(" > ");
@@ -6677,7 +6875,7 @@ function blocks(src, path) {
6677
6875
  return;
6678
6876
  }
6679
6877
  }
6680
- ts7.forEachChild(node, (child) => visit(child, suites));
6878
+ ts8.forEachChild(node, (child) => visit(child, suites));
6681
6879
  };
6682
6880
  visit(sf, []);
6683
6881
  } catch {
@@ -6726,7 +6924,7 @@ function findingForBlock(path, before, after, policy) {
6726
6924
  const weakened = directionalWeakening(before.assertions, after.assertions);
6727
6925
  if (weakened) {
6728
6926
  const message = weakened.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened.before.matcher} was replaced by weaker ${weakened.after.matcher} on the same subject.`;
6729
- return makeFinding(RULE11, policy, {
6927
+ return makeFinding(RULE12, policy, {
6730
6928
  file: path,
6731
6929
  message,
6732
6930
  evidence: `${weakened.before.text} -> ${weakened.after.text}`,
@@ -6735,7 +6933,7 @@ function findingForBlock(path, before, after, policy) {
6735
6933
  }
6736
6934
  const removed = pureRemoval(before.assertions, after.assertions);
6737
6935
  if (removed) {
6738
- return makeFinding(RULE11, policy, {
6936
+ return makeFinding(RULE12, policy, {
6739
6937
  file: path,
6740
6938
  message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
6741
6939
  evidence: removed.text,
@@ -6745,7 +6943,7 @@ function findingForBlock(path, before, after, policy) {
6745
6943
  return null;
6746
6944
  }
6747
6945
  var assertionWeakening = {
6748
- id: RULE11,
6946
+ id: RULE12,
6749
6947
  surface: ["file"],
6750
6948
  certainty: "heuristic",
6751
6949
  run(changes, policy) {
@@ -6771,6 +6969,7 @@ var assertionWeakening = {
6771
6969
  var allDetectors = [
6772
6970
  noVerify,
6773
6971
  tsAnyCast,
6972
+ tsCastGrowth,
6774
6973
  lintSuppression,
6775
6974
  testSkip,
6776
6975
  coverageLowering,
@@ -6812,14 +7011,15 @@ function evaluate(changes, policy, detectors = allDetectors, view, ctx) {
6812
7011
  process.stderr.write(`tamperward: detector "${d.id}" errored: ${String(e)}
6813
7012
  `);
6814
7013
  if (view === "staged" || view === "worktree" || view === "range") {
6815
- out2.push({
7014
+ const failed = {
6816
7015
  rule: "detector-error",
6817
7016
  severity: "block",
6818
7017
  message: `Detector "${d.id}" failed to run; the verdict is incomplete.`,
6819
7018
  evidence: String(e).slice(0, 200),
6820
7019
  remediation: "A rule that cannot run is not a rule that passed. Fix the input or the detector; do not read this as clean.",
6821
7020
  signoff: { required: true, command: `tamperward allow detector-error --reason "..."` }
6822
- });
7021
+ };
7022
+ out2.push(failed);
6823
7023
  }
6824
7024
  }
6825
7025
  }
@@ -6860,7 +7060,7 @@ function normalizeVersion(v, where = POLICY_FILE) {
6860
7060
  return v;
6861
7061
  }
6862
7062
  var SEVERITIES = ["block", "warn"];
6863
- var isSeverity = (v) => SEVERITIES.includes(v);
7063
+ var isSeverity = (v) => SEVERITIES.some((s) => s === v);
6864
7064
  var isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
6865
7065
  var isMapping = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6866
7066
  var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "protected", "rules", "ignore", "signoff", "verify"]);
@@ -6874,75 +7074,105 @@ function validate(r, where) {
6874
7074
  throw new PolicyError(`${where}: ${msg}`);
6875
7075
  }
6876
7076
  const show = (v) => JSON.stringify(v) ?? String(v);
7077
+ const out2 = {};
7078
+ if (r.version !== void 0) out2.version = r.version;
6877
7079
  const unknown = Object.keys(r).filter((k) => !TOP_LEVEL_KEYS.has(k));
6878
7080
  if (unknown.length) {
6879
7081
  bad(`unknown top-level key${unknown.length === 1 ? "" : "s"} ${unknown.map((k) => JSON.stringify(k)).join(", ")} (expected one of ${[...TOP_LEVEL_KEYS].join(", ")})`);
6880
7082
  }
6881
7083
  if (r.rules !== void 0) {
6882
7084
  if (!isMapping(r.rules)) bad(`rules must be a mapping of rule name to { severity, enabled, exclude }, got ${show(r.rules)}`);
7085
+ const rules = {};
6883
7086
  for (const [name, cfg] of Object.entries(r.rules)) {
6884
7087
  if (!isMapping(cfg)) bad(`rules.${name} must be a mapping like { severity: block }, got ${show(cfg)}`);
6885
- if (cfg.severity !== void 0 && !isSeverity(cfg.severity)) {
6886
- bad(`rules.${name}.severity must be "block" or "warn", got ${show(cfg.severity)}`);
7088
+ const { severity, enabled, exclude } = cfg;
7089
+ if (severity !== void 0 && !isSeverity(severity)) {
7090
+ bad(`rules.${name}.severity must be "block" or "warn", got ${show(severity)}`);
6887
7091
  }
6888
- if (cfg.enabled !== void 0 && typeof cfg.enabled !== "boolean") {
6889
- bad(`rules.${name}.enabled must be true or false, got ${show(cfg.enabled)}`);
7092
+ if (enabled !== void 0 && typeof enabled !== "boolean") {
7093
+ bad(`rules.${name}.enabled must be true or false, got ${show(enabled)}`);
6890
7094
  }
6891
- if (cfg.exclude !== void 0 && !isStringList(cfg.exclude)) {
6892
- bad(`rules.${name}.exclude must be a list of globs, got ${show(cfg.exclude)}`);
7095
+ if (exclude !== void 0 && !isStringList(exclude)) {
7096
+ bad(`rules.${name}.exclude must be a list of globs, got ${show(exclude)}`);
6893
7097
  }
7098
+ rules[name] = {
7099
+ ...severity !== void 0 ? { severity } : {},
7100
+ ...enabled !== void 0 ? { enabled } : {},
7101
+ ...exclude !== void 0 ? { exclude } : {}
7102
+ };
6894
7103
  }
7104
+ out2.rules = rules;
7105
+ }
7106
+ if (r.ignore !== void 0) {
7107
+ if (!isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
7108
+ out2.ignore = r.ignore;
6895
7109
  }
6896
- if (r.ignore !== void 0 && !isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
6897
7110
  if (r.protected !== void 0) {
6898
7111
  if (!isMapping(r.protected)) bad(`protected must be a mapping of category to a list of globs, got ${show(r.protected)}`);
7112
+ const categories = {};
6899
7113
  for (const [cat, globs4] of Object.entries(r.protected)) {
6900
7114
  if (!isStringList(globs4)) bad(`protected.${cat} must be a list of globs, got ${show(globs4)}`);
7115
+ categories[cat] = globs4;
6901
7116
  }
7117
+ out2.protected = categories;
6902
7118
  }
6903
7119
  if (r.signoff !== void 0) {
6904
7120
  if (!isMapping(r.signoff)) bad(`signoff must be a mapping, got ${show(r.signoff)}`);
7121
+ const signoff = {};
6905
7122
  for (const key2 of ["required_for", "requiredFor"]) {
6906
7123
  const v = r.signoff[key2];
6907
- if (v !== void 0 && !(Array.isArray(v) && v.every(isSeverity))) {
7124
+ if (v === void 0) continue;
7125
+ if (!(Array.isArray(v) && v.every(isSeverity))) {
6908
7126
  bad(`signoff.${key2} must be a list of "block" / "warn", got ${show(v)}`);
6909
7127
  }
7128
+ signoff[key2] = v;
6910
7129
  }
6911
7130
  const ledger = r.signoff.ledger;
6912
7131
  if (ledger !== void 0 && typeof ledger !== "string") bad(`signoff.ledger must be a path, got ${show(ledger)}`);
6913
- if (typeof ledger === "string" && !ledgerInsideRepo(ledger)) {
6914
- bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
7132
+ if (typeof ledger === "string") {
7133
+ if (!ledgerInsideRepo(ledger)) bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
7134
+ signoff.ledger = ledger;
6915
7135
  }
7136
+ out2.signoff = signoff;
6916
7137
  }
6917
7138
  if (r.verify !== void 0) {
6918
7139
  if (!isMapping(r.verify)) bad(`verify must be a mapping like { command: "npm test" }, got ${show(r.verify)}`);
6919
7140
  const v = r.verify;
6920
- if (v.command !== void 0 && typeof v.command !== "string") bad(`verify.command must be a string, got ${show(v.command)}`);
6921
- if (v.budget !== void 0 && !(typeof v.budget === "number" && Number.isFinite(v.budget) && v.budget > 0)) {
6922
- bad(`verify.budget must be a positive number of seconds, got ${show(v.budget)}`);
7141
+ const { command, budget, inputs, backend, image } = v;
7142
+ if (command !== void 0 && typeof command !== "string") bad(`verify.command must be a string, got ${show(command)}`);
7143
+ if (budget !== void 0 && !(typeof budget === "number" && Number.isFinite(budget) && budget > 0)) {
7144
+ bad(`verify.budget must be a positive number of seconds, got ${show(budget)}`);
6923
7145
  }
6924
- if (v.inputs !== void 0 && !isStringList(v.inputs)) bad(`verify.inputs must be a list of globs, got ${show(v.inputs)}`);
6925
- if (v.backend !== void 0 && v.backend !== "local" && v.backend !== "container") {
6926
- bad(`verify.backend must be "local" or "container", got ${show(v.backend)}`);
7146
+ if (inputs !== void 0 && !isStringList(inputs)) bad(`verify.inputs must be a list of globs, got ${show(inputs)}`);
7147
+ if (backend !== void 0 && backend !== "local" && backend !== "container") {
7148
+ bad(`verify.backend must be "local" or "container", got ${show(backend)}`);
6927
7149
  }
6928
- if (v.image !== void 0 && typeof v.image !== "string") {
6929
- bad(`verify.image must be an immutable container image reference, got ${show(v.image)}`);
7150
+ if (image !== void 0 && typeof image !== "string") {
7151
+ bad(`verify.image must be an immutable container image reference, got ${show(image)}`);
6930
7152
  }
6931
- const backend = v.backend ?? "local";
6932
- if (backend === "container") {
6933
- if (typeof v.image !== "string") bad('verify.image is required when verify.backend is "container"');
6934
- if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(v.image)) {
6935
- bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(v.image)}`);
7153
+ if ((backend ?? "local") === "container") {
7154
+ if (typeof image !== "string") bad('verify.image is required when verify.backend is "container"');
7155
+ if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(image)) {
7156
+ bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(image)}`);
6936
7157
  }
6937
- } else if (v.image !== void 0) {
7158
+ } else if (image !== void 0) {
6938
7159
  bad('verify.image is only valid when verify.backend is "container"');
6939
7160
  }
7161
+ out2.verify = {
7162
+ ...command !== void 0 ? { command } : {},
7163
+ ...budget !== void 0 ? { budget } : {},
7164
+ ...inputs !== void 0 ? { inputs } : {},
7165
+ ...backend !== void 0 ? { backend } : {},
7166
+ ...image !== void 0 ? { image } : {}
7167
+ };
6940
7168
  }
7169
+ return out2;
6941
7170
  }
6942
7171
  function parsePolicy(raw, where = POLICY_FILE) {
6943
- const r = raw ?? {};
6944
- const version = normalizeVersion(r.version, where);
6945
- validate(r, where);
7172
+ const doc = raw ?? {};
7173
+ if (!isMapping(doc)) throw new PolicyError(`${where} is not a policy mapping`);
7174
+ const version = normalizeVersion(doc.version, where);
7175
+ const r = validate(doc, where);
6946
7176
  const base = defaultPolicy(version);
6947
7177
  const globs4 = (list2) => list2?.map(normalizeGlob);
6948
7178
  const userProtected = r.protected ? Object.fromEntries(Object.entries(r.protected).map(([cat, list2]) => [cat, globs4(list2) ?? []])) : void 0;
@@ -6981,10 +7211,7 @@ function parseOrThrow(src, where) {
6981
7211
  try {
6982
7212
  raw = parse2(src);
6983
7213
  } catch (e) {
6984
- throw new PolicyError(`${where} is not valid YAML: ${e.message}`);
6985
- }
6986
- if (raw !== null && raw !== void 0 && (typeof raw !== "object" || Array.isArray(raw))) {
6987
- throw new PolicyError(`${where} is not a policy mapping`);
7214
+ throw new PolicyError(`${where} is not valid YAML: ${errorMessage(e)}`);
6988
7215
  }
6989
7216
  return parsePolicy(raw, where);
6990
7217
  }
@@ -7004,6 +7231,14 @@ import { createHash as createHash2 } from "node:crypto";
7004
7231
  import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "node:fs";
7005
7232
  import { dirname as dirname2, join as join8 } from "node:path";
7006
7233
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
7234
+ function ledgerEntryFrom(value) {
7235
+ if (!isRecord(value)) return null;
7236
+ const { rule, file, fingerprint: fingerprint2, reason, recordedAt, expiresAt } = value;
7237
+ if (typeof rule !== "string" || typeof fingerprint2 !== "string" || typeof reason !== "string") return null;
7238
+ if (typeof recordedAt !== "number" || typeof expiresAt !== "number") return null;
7239
+ if (file !== void 0 && typeof file !== "string") return null;
7240
+ return { rule, ...file !== void 0 ? { file } : {}, fingerprint: fingerprint2, reason, recordedAt, expiresAt };
7241
+ }
7007
7242
  function fingerprint(rule, file, evidence) {
7008
7243
  return createHash2("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
7009
7244
  }
@@ -7022,8 +7257,8 @@ function readLedger(cwd, policy) {
7022
7257
  for (const line of readFileSync6(p, "utf8").split("\n")) {
7023
7258
  if (!line.trim()) continue;
7024
7259
  try {
7025
- const e = JSON.parse(line);
7026
- if (e && typeof e.fingerprint === "string") out2.push(e);
7260
+ const e = ledgerEntryFrom(JSON.parse(line));
7261
+ if (e) out2.push(e);
7027
7262
  } catch {
7028
7263
  }
7029
7264
  }
@@ -7059,9 +7294,10 @@ function oobToken(want, oob, head) {
7059
7294
  }
7060
7295
  const [rule, sha2] = [t.slice(0, at), t.slice(at + 1)];
7061
7296
  if (rule !== want) continue;
7062
- const normalizedHead = head?.trim().toLowerCase();
7297
+ if (!head) return t;
7298
+ const normalizedHead = head.trim().toLowerCase();
7063
7299
  const normalizedSha = sha2.trim().toLowerCase();
7064
- if (!head || /^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
7300
+ if (/^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
7065
7301
  }
7066
7302
  return null;
7067
7303
  }
@@ -7084,6 +7320,12 @@ function oobFromEnv(env = process.env) {
7084
7320
  return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim().replace(/^tamperward:allow:/, "")).filter(Boolean);
7085
7321
  }
7086
7322
 
7323
+ // src/machine-output.ts
7324
+ var MACHINE_SCHEMA_VERSION = 1;
7325
+ function machineOutput(payload) {
7326
+ return { ...payload, schema_version: MACHINE_SCHEMA_VERSION };
7327
+ }
7328
+
7087
7329
  // src/cli/render/text.ts
7088
7330
  var ESC = "\x1B";
7089
7331
  var RESET = `${ESC}[0m`;
@@ -7287,7 +7529,7 @@ function isGitHubActions(env = process.env) {
7287
7529
  // src/cli/report.ts
7288
7530
  var FORMATS = ["auto", "text", "json", "github"];
7289
7531
  function isFormat(s) {
7290
- return FORMATS.includes(s);
7532
+ return FORMATS.some((f) => f === s);
7291
7533
  }
7292
7534
  function resolveFormat(input, env = process.env) {
7293
7535
  if (input.json) return "json";
@@ -7303,7 +7545,7 @@ function report(input) {
7303
7545
  JSON.stringify(
7304
7546
  // `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
7305
7547
  // is additive, so an existing consumer reading only the old keys is unaffected.
7306
- { findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
7548
+ machineOutput({ findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } }),
7307
7549
  null,
7308
7550
  2
7309
7551
  ) + "\n"
@@ -7379,7 +7621,9 @@ function check(opts) {
7379
7621
  process.stderr.write(`tamperward: ${cleared.length} blocking finding(s) cleared by ${how}: ${cleared.map((f) => f.rule + (f.file ? `(${f.file})` : "")).join(", ")}
7380
7622
  `);
7381
7623
  }
7382
- report({ findings, scanned: changes.length, ignoredFiles, json: opts.json, format: opts.format });
7624
+ if (!opts.silent) {
7625
+ report({ findings, scanned: changes.length, ignoredFiles, json: opts.json, format: opts.format });
7626
+ }
7383
7627
  return hasBlocking(findings) ? 1 : 0;
7384
7628
  }
7385
7629
  function runCheck(opts) {
@@ -7463,6 +7707,17 @@ import { createHash as createHash3 } from "node:crypto";
7463
7707
  import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
7464
7708
  import { isAbsolute as isAbsolute7, join as join10 } from "node:path";
7465
7709
  import { execFileSync as execFileSync5 } from "node:child_process";
7710
+ function ptreeFrom(value) {
7711
+ if (!isRecord(value)) return null;
7712
+ const out2 = {};
7713
+ for (const [path, entry] of Object.entries(value)) {
7714
+ if (!isRecord(entry)) return null;
7715
+ const { hash, mode, size, mtimeMs } = entry;
7716
+ if (typeof hash !== "string" || typeof mode !== "number" || typeof size !== "number" || typeof mtimeMs !== "number") return null;
7717
+ out2[path] = { hash, mode, size, mtimeMs };
7718
+ }
7719
+ return out2;
7720
+ }
7466
7721
  var UNSAFE2 = /[^A-Za-z0-9._-]/g;
7467
7722
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
7468
7723
  function gitDir2(cwd) {
@@ -7554,7 +7809,7 @@ function driftBetween(expected, current) {
7554
7809
  function loadTree(p) {
7555
7810
  if (!p || !existsSync7(p)) return null;
7556
7811
  try {
7557
- return JSON.parse(readFileSync8(p, "utf8"));
7812
+ return ptreeFrom(JSON.parse(readFileSync8(p, "utf8")));
7558
7813
  } catch {
7559
7814
  return null;
7560
7815
  }
@@ -7614,8 +7869,25 @@ function watcherHealthPath(log) {
7614
7869
  function readWatcherHealth(log) {
7615
7870
  try {
7616
7871
  const value = JSON.parse(readFileSync9(watcherHealthPath(log), "utf8"));
7617
- if (value.version !== 1 || typeof value.pid !== "number" || typeof value.started_at !== "string" || !["healthy", "degraded", "stopped"].includes(String(value.state))) return null;
7618
- return value;
7872
+ if (!isRecord(value)) return null;
7873
+ const { state, backend, pid, started_at } = value;
7874
+ if (value.version !== 1 || typeof pid !== "number" || typeof started_at !== "string") return null;
7875
+ if (state !== "healthy" && state !== "degraded" && state !== "stopped") return null;
7876
+ return {
7877
+ version: 1,
7878
+ state,
7879
+ backend: backend === "initializing" || backend === "recursive" ? backend : "fallback",
7880
+ pid,
7881
+ started_at,
7882
+ stopped_at: nullableString(value.stopped_at) ?? null,
7883
+ watched_dirs: finiteNumber(value.watched_dirs) ?? 0,
7884
+ last_append_at: nullableString(value.last_append_at) ?? null,
7885
+ event_count: finiteNumber(value.event_count) ?? 0,
7886
+ dropped_events: finiteNumber(value.dropped_events) ?? 0,
7887
+ error_count: finiteNumber(value.error_count) ?? 0,
7888
+ last_error: nullableString(value.last_error) ?? null,
7889
+ log: stringOrUndefined(value.log) ?? log
7890
+ };
7619
7891
  } catch {
7620
7892
  return null;
7621
7893
  }
@@ -7626,7 +7898,7 @@ function pidAlive(pid) {
7626
7898
  process.kill(pid, 0);
7627
7899
  return true;
7628
7900
  } catch (e) {
7629
- return e.code === "EPERM";
7901
+ return errnoCode(e) === "EPERM";
7630
7902
  }
7631
7903
  }
7632
7904
  function watcherTelemetry(log) {
@@ -7830,7 +8102,23 @@ function runWatch(args) {
7830
8102
 
7831
8103
  // src/detectors/fs-events.ts
7832
8104
  import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7833
- var RULE12 = "transient-protected-mutation";
8105
+ function fsEventFrom(line) {
8106
+ let value;
8107
+ try {
8108
+ value = JSON.parse(line);
8109
+ } catch {
8110
+ return null;
8111
+ }
8112
+ if (!isRecord(value)) return null;
8113
+ const { ts: ts9, path, kind } = value;
8114
+ if (typeof ts9 !== "string" || typeof path !== "string" || kind !== "change" && kind !== "rename") return null;
8115
+ const mode = nullableNumber(value.mode);
8116
+ const size = nullableNumber(value.size);
8117
+ const hash = nullableString(value.hash);
8118
+ if (mode === void 0 || size === void 0 || hash === void 0) return null;
8119
+ return { ts: ts9, path, kind, mode, size, hash };
8120
+ }
8121
+ var RULE13 = "transient-protected-mutation";
7834
8122
  var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
7835
8123
  var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
7836
8124
  var DEFAULT_EVENT_READ_IO = {
@@ -7901,11 +8189,9 @@ function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_E
7901
8189
  const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
7902
8190
  for (const line of complete.split("\n")) {
7903
8191
  if (!line.trim()) continue;
7904
- try {
7905
- events.push(JSON.parse(line));
7906
- } catch {
7907
- malformedLines++;
7908
- }
8192
+ const event = fsEventFrom(line);
8193
+ if (event) events.push(event);
8194
+ else malformedLines++;
7909
8195
  }
7910
8196
  const newOffset = start + lastNewline + 1;
7911
8197
  return {
@@ -8025,7 +8311,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
8025
8311
  };
8026
8312
  }
8027
8313
  function transientFindings(events, persistentPaths, policy, finalHash) {
8028
- if (!isEnabled(RULE12, policy)) return [];
8314
+ if (!isEnabled(RULE13, policy)) return [];
8029
8315
  const byPath = /* @__PURE__ */ new Map();
8030
8316
  for (const e of events) {
8031
8317
  if (!isProtected(e.path, policy)) continue;
@@ -8045,7 +8331,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
8045
8331
  const modeChurn = modes.size > 1;
8046
8332
  if (!contentChurn && !modeChurn) continue;
8047
8333
  const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
8048
- const f = makeFinding(RULE12, policy, {
8334
+ const f = makeFinding(RULE13, policy, {
8049
8335
  file: path,
8050
8336
  message: `Transient protected mutation: ${what}, leaving no net diff.`,
8051
8337
  evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
@@ -8079,10 +8365,16 @@ function parseInput(raw) {
8079
8365
  } catch (e) {
8080
8366
  throw new HookInputError(`the hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
8081
8367
  }
8082
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8368
+ if (!isRecord(parsed)) {
8083
8369
  throw new HookInputError(`the hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`);
8084
8370
  }
8085
- return parsed;
8371
+ return {
8372
+ ...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
8373
+ ...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
8374
+ ...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
8375
+ ...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {},
8376
+ ...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}
8377
+ };
8086
8378
  }
8087
8379
  function recordDenylog(blocks2) {
8088
8380
  const log = process.env.TAMPERWARD_DENYLOG;
@@ -8725,6 +9017,9 @@ function missingTools(matcher) {
8725
9017
  if (have === null) return [];
8726
9018
  return PRE_MATCHER.split("|").filter((t) => !have.has(t));
8727
9019
  }
9020
+ function isHooksShape(hooks) {
9021
+ return hooksShapeError(hooks) === null;
9022
+ }
8728
9023
  function hooksShapeError(hooks) {
8729
9024
  if (hooks === void 0 || hooks === null) return null;
8730
9025
  if (!isPlainObject(hooks)) return "hooks is not an object";
@@ -8747,7 +9042,7 @@ function verifierCandidates(cwd) {
8747
9042
  };
8748
9043
  try {
8749
9044
  const pkg = JSON.parse(readFileSync11(join12(cwd, "package.json"), "utf8"));
8750
- const test = pkg.scripts?.test;
9045
+ const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
8751
9046
  if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
8752
9047
  } catch {
8753
9048
  }
@@ -8846,14 +9141,14 @@ function planClaudeHooks(cwd) {
8846
9141
  detail: "exists but is not valid JSON \u2014 fix it, then re-run init (refusing to overwrite)"
8847
9142
  };
8848
9143
  }
8849
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
9144
+ if (!isRecord(parsed)) {
8850
9145
  return { item: "agent", path: rel, status: "error", detail: "exists but is not a JSON object \u2014 refusing to overwrite" };
8851
9146
  }
8852
- settings = parsed;
8853
- }
8854
- const shapeError = hooksShapeError(settings.hooks);
8855
- if (shapeError) {
8856
- return { item: "agent", path: rel, status: "error", detail: `${shapeError} \u2014 fix it, then re-run init (refusing to overwrite)` };
9147
+ const shapeError = hooksShapeError(parsed.hooks);
9148
+ if (shapeError || !isHooksShape(parsed.hooks)) {
9149
+ return { item: "agent", path: rel, status: "error", detail: `${shapeError ?? "hooks is not the shape Claude Code reads"} \u2014 fix it, then re-run init (refusing to overwrite)` };
9150
+ }
9151
+ settings = { ...parsed, hooks: parsed.hooks ?? void 0 };
8857
9152
  }
8858
9153
  const hooks = settings.hooks ??= {};
8859
9154
  const entriesWith = (arr, needle) => (arr ?? []).filter((m) => (m.hooks ?? []).some((h) => needle.test(String(h.command ?? ""))));
@@ -9077,7 +9372,7 @@ function planCodeowners(cwd) {
9077
9372
  const path = join12(cwd, existingRel ?? rel);
9078
9373
  const existing = existingRel ? readFileSync11(path, "utf8") : null;
9079
9374
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
9080
- if (missing.length === 0) {
9375
+ if (existingRel !== void 0 && missing.length === 0) {
9081
9376
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
9082
9377
  }
9083
9378
  const inferred = inferOwner(cwd);
@@ -9703,14 +9998,15 @@ function decodeUtf8Tail(tail) {
9703
9998
  while (start < tail.length && (tail[start] & 192) === 128) start++;
9704
9999
  return tail.subarray(start).toString("utf8");
9705
10000
  }
9706
- function streamFromRaw(raw) {
10001
+ function streamFromRaw(value) {
10002
+ const raw = isRecord(value) ? value : {};
9707
10003
  let tail = Buffer.alloc(0);
9708
10004
  try {
9709
- tail = Buffer.from(raw?.tail_b64 ?? "", "base64");
10005
+ tail = Buffer.from(typeof raw.tail_b64 === "string" ? raw.tail_b64 : "", "base64");
9710
10006
  } catch {
9711
10007
  tail = Buffer.alloc(0);
9712
10008
  }
9713
- const captured = Number.isFinite(raw?.captured_bytes) ? Math.max(0, Number(raw?.captured_bytes)) : tail.length;
10009
+ const captured = typeof raw.captured_bytes === "number" && Number.isFinite(raw.captured_bytes) ? Math.max(0, raw.captured_bytes) : tail.length;
9714
10010
  return {
9715
10011
  captured_bytes: captured,
9716
10012
  retained_bytes: tail.length,
@@ -9721,7 +10017,7 @@ function streamFromRaw(raw) {
9721
10017
  function scrubDiagnosticText(input) {
9722
10018
  let out2 = "";
9723
10019
  for (const ch of input) {
9724
- const cp = ch.codePointAt(0);
10020
+ const cp = ch.codePointAt(0) ?? 0;
9725
10021
  if (ch === "\n" || ch === " ") {
9726
10022
  out2 += ch;
9727
10023
  } else if (cp === 13) {
@@ -9924,7 +10220,7 @@ function parseCapturedSupervisorResult(stdoutText) {
9924
10220
  } catch {
9925
10221
  return null;
9926
10222
  }
9927
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
10223
+ if (!isRecord(raw)) return null;
9928
10224
  const diagnostics = {
9929
10225
  stdout: streamFromRaw(raw.stdout),
9930
10226
  stderr: streamFromRaw(raw.stderr)
@@ -9975,7 +10271,7 @@ function runCapturedProcessSync(executable, args, opts) {
9975
10271
  const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
9976
10272
  const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
9977
10273
  if (!parsed) {
9978
- const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
10274
+ const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
9979
10275
  return {
9980
10276
  exit: null,
9981
10277
  signal: supervisor.signal ? String(supervisor.signal) : null,
@@ -10070,16 +10366,16 @@ function dockerAvailable(enginePath, host) {
10070
10366
  }
10071
10367
  }
10072
10368
  function declaredImageVolumePaths(metadata) {
10073
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
10369
+ if (!isRecord(metadata)) {
10074
10370
  throw new Error("image metadata must be a Docker image-inspect object");
10075
10371
  }
10076
10372
  const config = metadata.Config;
10077
- if (!config || typeof config !== "object" || Array.isArray(config)) {
10373
+ if (!isRecord(config)) {
10078
10374
  throw new Error("image metadata Config must be an object");
10079
10375
  }
10080
10376
  const volumes = config.Volumes;
10081
10377
  if (volumes === void 0 || volumes === null) return [];
10082
- if (typeof volumes !== "object" || Array.isArray(volumes)) {
10378
+ if (!isRecord(volumes)) {
10083
10379
  throw new Error("image metadata Config.Volumes must be a mapping or null");
10084
10380
  }
10085
10381
  return Object.keys(volumes).sort();
@@ -10583,8 +10879,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10583
10879
  const pending = linkParts(target);
10584
10880
  const seen = /* @__PURE__ */ new Set();
10585
10881
  let hops = 0;
10586
- while (pending.length) {
10587
- const part = pending.shift();
10882
+ for (let part = pending.shift(); part !== void 0; part = pending.shift()) {
10588
10883
  if (part === "..") {
10589
10884
  if (!stack.length) throw linkEscape(label, target);
10590
10885
  stack.pop();
@@ -10596,7 +10891,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
10596
10891
  stack = [];
10597
10892
  continue;
10598
10893
  }
10599
- const base = domain === "tree" ? root : dependencyRoot;
10894
+ const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
10600
10895
  const current = join16(base, ...stack);
10601
10896
  let st;
10602
10897
  try {
@@ -10894,10 +11189,24 @@ function renderStageDiagnostics(out2, stage, result) {
10894
11189
  }
10895
11190
  function runVerify(opts) {
10896
11191
  const cwd = opts.cwd ?? process.cwd();
10897
- const out2 = (s) => void process.stdout.write(s + "\n");
10898
- if (opts.invalid) {
10899
- out2(`verify: ${opts.invalid} \u2014 failing closed`);
11192
+ const out2 = opts.silent ? (_s) => {
11193
+ } : (s) => void process.stdout.write(s + "\n");
11194
+ const cannotVerify = (reason, detail, extra = {}) => {
11195
+ if (opts.json) {
11196
+ out2(JSON.stringify({
11197
+ schema_version: MACHINE_SCHEMA_VERSION,
11198
+ verdict: "CANNOT_VERIFY",
11199
+ reason,
11200
+ ...detail ? { detail } : {},
11201
+ ...extra
11202
+ }));
11203
+ } else if (detail) {
11204
+ out2(`verify: ${detail} \u2014 failing closed`);
11205
+ }
10900
11206
  return 2;
11207
+ };
11208
+ if (opts.invalid) {
11209
+ return cannotVerify("INVALID_ARGUMENTS", opts.invalid);
10901
11210
  }
10902
11211
  let policy;
10903
11212
  try {
@@ -10905,24 +11214,24 @@ function runVerify(opts) {
10905
11214
  else if (opts.base) policy = loadPolicyAt(resolveBase(opts.base, cwd), cwd) ?? defaultPolicy();
10906
11215
  else policy = loadPolicy(cwd);
10907
11216
  } catch (e) {
10908
- out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
10909
- return 2;
11217
+ return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
10910
11218
  }
10911
11219
  if (opts.requireAncestor) {
10912
11220
  const requested = git2(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
10913
11221
  if (!baseIsAncestorOfHead(requested, cwd)) {
10914
- out2(
10915
- `verify: --require-ancestor: ${requested.slice(0, 10)} is not an ancestor of HEAD \u2014 the anchor would silently resolve to an older commit. Failing closed.`
11222
+ return cannotVerify(
11223
+ "BASE_NOT_ANCESTOR",
11224
+ `--require-ancestor: ${requested.slice(0, 10)} is not an ancestor of HEAD; the anchor would silently resolve to an older commit`,
11225
+ { base: requested }
10916
11226
  );
10917
- return 2;
10918
11227
  }
10919
11228
  }
10920
11229
  const cmd = opts.cmd ?? policy.verify?.command;
10921
11230
  if (!cmd) {
10922
- out2(
10923
- opts.base ? "verify: no suite command in the policy at the trusted base \u2014 the base governs the verifier, so a `verify:` block added only on the candidate is not used. Add it at the base, or pass --cmd explicitly." : "verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd"
11231
+ return cannotVerify(
11232
+ "NO_SUITE_COMMAND",
11233
+ opts.base ? "no suite command in the policy at the trusted base; add it at the base or pass --cmd explicitly" : "no suite command; set policy verify.command or pass --cmd"
10924
11234
  );
10925
- return 2;
10926
11235
  }
10927
11236
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
10928
11237
  const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
@@ -10930,6 +11239,7 @@ function runVerify(opts) {
10930
11239
  if (!verifierBackend.available) {
10931
11240
  if (opts.json) {
10932
11241
  out2(JSON.stringify({
11242
+ schema_version: MACHINE_SCHEMA_VERSION,
10933
11243
  verdict: "CANNOT_VERIFY",
10934
11244
  reason: "VERIFIER_BACKEND_UNAVAILABLE",
10935
11245
  verifier_backend: backendReport(),
@@ -10945,6 +11255,7 @@ function runVerify(opts) {
10945
11255
  if (!isolated && !localVerifierShell(process.platform, cmd)) {
10946
11256
  if (opts.json) {
10947
11257
  out2(JSON.stringify({
11258
+ schema_version: MACHINE_SCHEMA_VERSION,
10948
11259
  verdict: "CANNOT_VERIFY",
10949
11260
  reason: "LOCAL_VERIFIER_UNSUPPORTED_PLATFORM",
10950
11261
  platform: process.platform,
@@ -10967,6 +11278,7 @@ function runVerify(opts) {
10967
11278
  if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
10968
11279
  if (opts.json) {
10969
11280
  out2(JSON.stringify({
11281
+ schema_version: MACHINE_SCHEMA_VERSION,
10970
11282
  verdict: "CANNOT_VERIFY",
10971
11283
  reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
10972
11284
  verifier_backend: backendReport(),
@@ -10986,8 +11298,7 @@ function runVerify(opts) {
10986
11298
  try {
10987
11299
  base = resolveBase(opts.base ?? "HEAD", cwd);
10988
11300
  } catch {
10989
- out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
10990
- return 2;
11301
+ return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
10991
11302
  }
10992
11303
  const visRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-vis-"));
10993
11304
  const visDir = join16(visRoot, "t");
@@ -11004,17 +11315,22 @@ function runVerify(opts) {
11004
11315
  ) ?? checkDeps() : { ok: true };
11005
11316
  if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
11006
11317
  cleanup([visRoot]);
11007
- out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
11008
- out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
11009
- return 2;
11318
+ return cannotVerify(
11319
+ "DEPENDENCY_DRIFT",
11320
+ dependencyAtEntry.reason ?? "dependency identity changed before the visible suite ran",
11321
+ { dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
11322
+ );
11010
11323
  }
11011
11324
  try {
11012
11325
  mkdirSync6(visDir);
11013
11326
  materialize(cwd, visDir, frozenNodeModules);
11014
11327
  } catch (e) {
11015
11328
  cleanup([visRoot]);
11016
- out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
11017
- return 2;
11329
+ return cannotVerify(
11330
+ "MATERIALIZATION_FAILED",
11331
+ `could not materialize (${e instanceof Error ? e.message : String(e)})`,
11332
+ { verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
11333
+ );
11018
11334
  }
11019
11335
  const visible = runStage(visDir);
11020
11336
  if (visible.failure === "backend" || visible.failure === "resource") {
@@ -11022,6 +11338,7 @@ function runVerify(opts) {
11022
11338
  const exhausted = visible.failure === "resource";
11023
11339
  if (opts.json) {
11024
11340
  out2(JSON.stringify({
11341
+ schema_version: MACHINE_SCHEMA_VERSION,
11025
11342
  verdict: "CANNOT_VERIFY",
11026
11343
  reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
11027
11344
  stage: "visible",
@@ -11042,16 +11359,20 @@ function runVerify(opts) {
11042
11359
  }
11043
11360
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
11044
11361
  cleanup([visRoot]);
11045
- out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
11046
- out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
11047
- return 2;
11362
+ return cannotVerify(
11363
+ "WORKTREE_CHANGED",
11364
+ "the working tree changed while the visible suite was running",
11365
+ { stage: "visible", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
11366
+ );
11048
11367
  }
11049
11368
  const dependencyAfterVisible = checkDeps();
11050
11369
  if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
11051
11370
  cleanup([visRoot]);
11052
- out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
11053
- out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
11054
- return 2;
11371
+ return cannotVerify(
11372
+ "DEPENDENCY_DRIFT",
11373
+ dependencyAfterVisible.reason ?? "dependency identity changed while the visible suite was running",
11374
+ { stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
11375
+ );
11055
11376
  }
11056
11377
  const priRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-pri-"));
11057
11378
  const priDir = join16(priRoot, "t");
@@ -11070,8 +11391,11 @@ function runVerify(opts) {
11070
11391
  ));
11071
11392
  } catch (e) {
11072
11393
  cleanup([visRoot, priRoot]);
11073
- out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
11074
- return 2;
11394
+ return cannotVerify(
11395
+ "MATERIALIZATION_FAILED",
11396
+ `could not materialize pristine copy (${e instanceof Error ? e.message : String(e)})`,
11397
+ { stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
11398
+ );
11075
11399
  }
11076
11400
  const overlayBefore = overlayDigest(priDir, restored);
11077
11401
  const pristine = runStage(priDir);
@@ -11080,6 +11404,7 @@ function runVerify(opts) {
11080
11404
  const exhausted = pristine.failure === "resource";
11081
11405
  if (opts.json) {
11082
11406
  out2(JSON.stringify({
11407
+ schema_version: MACHINE_SCHEMA_VERSION,
11083
11408
  verdict: "CANNOT_VERIFY",
11084
11409
  reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
11085
11410
  stage: "pristine",
@@ -11108,13 +11433,14 @@ function runVerify(opts) {
11108
11433
  const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
11109
11434
  cleanup([visRoot, priRoot]);
11110
11435
  if (overlayMoved || treeMoved || depsMoved) {
11111
- out2(
11112
- "verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the frozen dependency environment changed while the pristine suite was running") + " \u2014"
11113
- );
11114
- out2("the verdict would describe something other than what ran. Failing closed, not open.");
11115
- out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
11116
- out2("whatever mode your runner calls CI, so verification observes rather than updates.)");
11117
- return 2;
11436
+ const reason = overlayMoved ? "PRISTINE_INTEGRITY_CHANGED" : treeMoved ? "WORKTREE_CHANGED" : "DEPENDENCY_DRIFT";
11437
+ const detail = overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : dependencyAfterPristine.reason ?? "the frozen dependency environment changed while the pristine suite was running";
11438
+ return cannotVerify(reason, detail, {
11439
+ stage: "pristine",
11440
+ verifier_backend: backendReport(),
11441
+ dependency_environment: dependencyReport(),
11442
+ oracle_assurance: oracleAssuranceReport()
11443
+ });
11118
11444
  }
11119
11445
  let verdict2;
11120
11446
  let code2;
@@ -11136,6 +11462,7 @@ function runVerify(opts) {
11136
11462
  if (opts.json) {
11137
11463
  out2(
11138
11464
  JSON.stringify({
11465
+ schema_version: MACHINE_SCHEMA_VERSION,
11139
11466
  verdict: verdict2,
11140
11467
  base,
11141
11468
  command: cmd,
@@ -11299,8 +11626,9 @@ var AGENT_SUPERVISOR = String.raw`
11299
11626
  const { spawn, spawnSync } = require('node:child_process');
11300
11627
  const fs = require('node:fs');
11301
11628
 
11302
- const [resultFile, budgetRaw, command, ...args] = process.argv.slice(1);
11629
+ const [resultFile, budgetRaw, machineRaw, command, ...args] = process.argv.slice(1);
11303
11630
  const budgetMs = budgetRaw === '' ? null : Number(budgetRaw);
11631
+ const machineMode = machineRaw === '1';
11304
11632
  let child;
11305
11633
  let timedOut = false;
11306
11634
  let finished = false;
@@ -11326,7 +11654,9 @@ function killTree(pid) {
11326
11654
 
11327
11655
  try {
11328
11656
  child = spawn(command, args, {
11329
- stdio: 'inherit',
11657
+ // Machine mode owns stdout for the final envelope document. Preserve agent
11658
+ // diagnostics by routing both child streams to the parent's stderr.
11659
+ stdio: machineMode ? ['inherit', 2, 2] : 'inherit',
11330
11660
  detached: process.platform !== 'win32',
11331
11661
  });
11332
11662
  } catch (e) {
@@ -11363,8 +11693,9 @@ child.once('exit', (code, signal) => finish(code, signal));
11363
11693
  var LINUX_SUBREAPER_SUPERVISOR = String.raw`
11364
11694
  import ctypes, json, os, signal, subprocess, sys, time
11365
11695
 
11366
- result_file, agent_env_file, agent_cwd, budget_raw, test_mode, command, *args = sys.argv[1:]
11696
+ result_file, agent_env_file, agent_cwd, budget_raw, test_mode, machine_raw, command, *args = sys.argv[1:]
11367
11697
  budget = None if budget_raw == "" else float(budget_raw)
11698
+ machine_mode = machine_raw == "1"
11368
11699
  libc = ctypes.CDLL(None, use_errno=True)
11369
11700
  PR_SET_DUMPABLE = 4
11370
11701
  PR_SET_CHILD_SUBREAPER = 36
@@ -11482,6 +11813,10 @@ try:
11482
11813
  cwd=agent_cwd,
11483
11814
  env=agent_env,
11484
11815
  start_new_session=True,
11816
+ # In machine mode stdout is reserved for TamperWard's one final JSON
11817
+ # document. Agent stdout is retained as diagnostics on stderr.
11818
+ stdout=sys.stderr if machine_mode else None,
11819
+ stderr=None,
11485
11820
  )
11486
11821
  except Exception as exc:
11487
11822
  # No candidate process exists, so the lifecycle domain is trivially empty.
@@ -11511,7 +11846,7 @@ write_result({
11511
11846
  })
11512
11847
  raise SystemExit(0)
11513
11848
  `;
11514
- function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode) {
11849
+ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
11515
11850
  const stateDir = mkdtempSync4(join17(tmpdir4(), "tw-agent-supervisor-"));
11516
11851
  const resultFile = join17(stateDir, "result.json");
11517
11852
  const agentEnvFile = join17(stateDir, "agent-env.json");
@@ -11544,6 +11879,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11544
11879
  cwd,
11545
11880
  budgetSecs === void 0 ? "" : String(budgetSecs),
11546
11881
  lifecycleTestMode ?? "",
11882
+ machineMode ? "1" : "0",
11547
11883
  ...argv2
11548
11884
  ];
11549
11885
  supervisorCwd = "/";
@@ -11555,6 +11891,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11555
11891
  AGENT_SUPERVISOR,
11556
11892
  resultFile,
11557
11893
  budgetSecs === void 0 ? "" : String(budgetSecs * 1e3),
11894
+ machineMode ? "1" : "0",
11558
11895
  ...argv2
11559
11896
  ];
11560
11897
  }
@@ -11573,7 +11910,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
11573
11910
  }
11574
11911
  }
11575
11912
  );
11576
- const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
11913
+ const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
11577
11914
  const completedNormally = !supervisorTimedOut && !supervisor.error && supervisor.status === 0 && supervisor.signal == null;
11578
11915
  if (!completedNormally) {
11579
11916
  return {
@@ -11624,7 +11961,7 @@ function pidAlive2(pid) {
11624
11961
  process.kill(pid, 0);
11625
11962
  return true;
11626
11963
  } catch (e) {
11627
- return e.code === "EPERM";
11964
+ return errnoCode(e) === "EPERM";
11628
11965
  }
11629
11966
  }
11630
11967
  function supervisedObserverLog(cwd) {
@@ -11667,15 +12004,14 @@ function stopObserverProcess(observer) {
11667
12004
  waitMs(100);
11668
12005
  const beforeStop = watcherTelemetry(observer.log);
11669
12006
  const pid = observer.pid;
11670
- if (pidAlive2(pid)) {
12007
+ if (pid !== null && pidAlive2(pid)) {
11671
12008
  try {
11672
12009
  process.kill(pid, "SIGTERM");
11673
12010
  } catch {
11674
12011
  }
11675
12012
  const deadline = Date.now() + 2e3;
11676
12013
  while (Date.now() < deadline) {
11677
- const health = watcherTelemetry(observer.log);
11678
- if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
12014
+ if (!pidAlive2(pid)) break;
11679
12015
  waitMs(25);
11680
12016
  }
11681
12017
  if (pidAlive2(pid)) {
@@ -11688,22 +12024,22 @@ function stopObserverProcess(observer) {
11688
12024
  observer.finished = true;
11689
12025
  return beforeStop;
11690
12026
  }
11691
- function observerSummary(observer, telemetry) {
12027
+ function observerSummary(observer, telemetry, write = out) {
11692
12028
  const h = telemetry.health;
11693
12029
  const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
11694
12030
  const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
11695
- out(
12031
+ write(
11696
12032
  `tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
11697
12033
  );
11698
12034
  }
11699
- function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11700
- observerSummary(observer, telemetry);
12035
+ function collectObserverFindings(observer, telemetry, cwd, base, head, policy, write = out) {
12036
+ observerSummary(observer, telemetry, write);
11701
12037
  const strict = process.env.TAMPERWARD_TRANSIENT === "block";
11702
12038
  const drained = drainEvents(observer.log, 0);
11703
12039
  const { events } = drained;
11704
12040
  if (!drained.complete) {
11705
12041
  const issue = drained.issue ?? "read-stalled";
11706
- out(
12042
+ write(
11707
12043
  `tamperward run \u2014 transient observer: telemetry was not fully classified (${issue}; ${drained.bytesRead}/${MAX_EVENT_SWEEP_BYTES} bytes read). ` + (strict ? "Strict transient policy fails closed." : "Observer telemetry remains advisory, so this degrades evidence but does not independently fail the envelope.")
11708
12044
  );
11709
12045
  if (strict) return { blocking: true };
@@ -11719,7 +12055,7 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11719
12055
  changes.filter((change) => change.kind === "file").map((change) => change.path)
11720
12056
  );
11721
12057
  } catch (e) {
11722
- out(
12058
+ write(
11723
12059
  `tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).` + (strict ? " Strict transient policy fails closed." : "")
11724
12060
  );
11725
12061
  return { blocking: strict };
@@ -11730,16 +12066,16 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
11730
12066
  };
11731
12067
  const findings = transientFindings(events, persistent, policy, finalHash);
11732
12068
  for (const finding of findings) {
11733
- out(
12069
+ write(
11734
12070
  `[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
11735
12071
  );
11736
12072
  }
11737
12073
  return { blocking: findings.some((finding) => finding.severity === "block") };
11738
12074
  }
11739
- function finishObserverAdvisory(observer) {
12075
+ function finishObserverAdvisory(observer, write = out) {
11740
12076
  if (!observer || observer.finished) return;
11741
12077
  const telemetry = stopObserverProcess(observer);
11742
- observerSummary(observer, telemetry);
12078
+ observerSummary(observer, telemetry, write);
11743
12079
  }
11744
12080
  function git3(args, cwd) {
11745
12081
  return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
@@ -11798,6 +12134,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
11798
12134
  }
11799
12135
  function runEnvelope(opts) {
11800
12136
  const cwd = resolve7(opts.cwd ?? process.cwd());
12137
+ const say = opts.json ? (_s) => {
12138
+ } : out;
11801
12139
  if (!opts.argv.length) {
11802
12140
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
11803
12141
  return 2;
@@ -11848,8 +12186,8 @@ function runEnvelope(opts) {
11848
12186
  return 2;
11849
12187
  }
11850
12188
  const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
11851
- out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
11852
- out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
12189
+ say(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
12190
+ say(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
11853
12191
  if (!verifierBackend.available) {
11854
12192
  err("tamperward run: the requested verifier backend cannot be established \u2014");
11855
12193
  err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
@@ -11865,7 +12203,7 @@ function runEnvelope(opts) {
11865
12203
  }
11866
12204
  const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
11867
12205
  if (dependencyEnvironment) {
11868
- out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
12206
+ say(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
11869
12207
  if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
11870
12208
  err("tamperward run: the verifier dependency environment cannot be attested \u2014");
11871
12209
  err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
@@ -11873,11 +12211,11 @@ function runEnvelope(opts) {
11873
12211
  return 2;
11874
12212
  }
11875
12213
  } else {
11876
- out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
12214
+ say(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
11877
12215
  }
11878
12216
  const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
11879
12217
  if (!observer) {
11880
- out("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
12218
+ say("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
11881
12219
  }
11882
12220
  const rewritesBefore = objectRewriteState(cwd);
11883
12221
  const spawnTicks = nowTicks();
@@ -11886,16 +12224,38 @@ function runEnvelope(opts) {
11886
12224
  cwd,
11887
12225
  opts.agentBudget,
11888
12226
  opts.linuxPythonCandidates,
11889
- opts.lifecycleTestMode
12227
+ opts.lifecycleTestMode,
12228
+ opts.json === true
11890
12229
  );
11891
12230
  const agentExit = agentRun.exit;
11892
12231
  const agentTimedOut = agentRun.timedOut;
12232
+ const emitRunJson = (verdict3, exitCode2, extra = {}, complete = false) => {
12233
+ if (!opts.json) return;
12234
+ out(JSON.stringify(machineOutput({
12235
+ verdict: verdict3,
12236
+ exit_code: exitCode2,
12237
+ complete,
12238
+ base,
12239
+ agent: {
12240
+ exit_code: agentExit,
12241
+ timed_out: agentTimedOut,
12242
+ lifecycle_owned: agentRun.lifecycleOwned,
12243
+ ...opts.agentBudget !== void 0 ? { budget_secs: opts.agentBudget } : {}
12244
+ },
12245
+ verifier_backend: verifierBackendReport(verifierBackend),
12246
+ dependency_environment: dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : { status: "verifier-owned", roots: [], image: verifierBackend.image },
12247
+ ...extra
12248
+ })));
12249
+ };
11893
12250
  if (agentRun.failure && !agentTimedOut) {
11894
12251
  err(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}).`);
11895
12252
  }
11896
- if (process.platform === "linux" && !agentRun.lifecycleOwned) {
11897
- err("tamperward run: the Linux agent lifecycle boundary was not established/drained \u2014 failing closed before adjudication.");
11898
- finishObserverAdvisory(observer);
12253
+ if (!agentRun.lifecycleOwned) {
12254
+ err("tamperward run: the agent lifecycle boundary was not established/drained \u2014 failing closed before adjudication.");
12255
+ finishObserverAdvisory(observer, say);
12256
+ emitRunJson("CANNOT_ADJUDICATE", 2, {
12257
+ reason: "AGENT_LIFECYCLE_NOT_OWNED"
12258
+ });
11899
12259
  return 2;
11900
12260
  }
11901
12261
  if (agentTimedOut) {
@@ -11908,9 +12268,10 @@ function runEnvelope(opts) {
11908
12268
  err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
11909
12269
  err("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
11910
12270
  err("read against the anchor we started from. That is the finding.");
11911
- out(`
12271
+ say(`
11912
12272
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
11913
- finishObserverAdvisory(observer);
12273
+ finishObserverAdvisory(observer, say);
12274
+ emitRunJson("OBJECT_REWRITE", 1);
11914
12275
  return 1;
11915
12276
  }
11916
12277
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
@@ -11918,36 +12279,41 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
11918
12279
  if (isAncestor.status !== 0) {
11919
12280
  err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
11920
12281
  err("the agent rewrote history out from under the anchor. That is the finding.");
11921
- out(`
12282
+ say(`
11922
12283
  tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
11923
- finishObserverAdvisory(observer);
12284
+ finishObserverAdvisory(observer, say);
12285
+ emitRunJson("HISTORY_REWRITE", 1, { head });
11924
12286
  return 1;
11925
12287
  }
11926
12288
  const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
11927
12289
  const fpBefore = treeFingerprint(cwd, protectedIgnored);
11928
12290
  let diffCode = 0;
11929
12291
  if (head !== base) {
11930
- out(`
12292
+ say(`
11931
12293
  [envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
11932
- diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy });
12294
+ diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy, silent: opts.json });
11933
12295
  }
11934
- out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
11935
- const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
12296
+ say("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
12297
+ const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true, silent: opts.json });
11936
12298
  const dependencyBeforeVerificationAttestation = dependencyEnvironment ? attestDependencyEnvironment(cwd, dependencyEnvironment) : void 0;
11937
12299
  const dependencyBeforeVerification = dependencyBeforeVerificationAttestation?.check ?? { ok: true };
11938
12300
  if (!dependencyBeforeVerification.ok) {
11939
12301
  err("tamperward run: the frozen dependency environment changed before verification began \u2014");
11940
12302
  err(`${dependencyBeforeVerification.reason ?? "dependency identity changed"}.`);
11941
12303
  if (!opts.allowDepDrift) {
11942
- out(`
12304
+ say(`
11943
12305
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
11944
- finishObserverAdvisory(observer);
12306
+ finishObserverAdvisory(observer, say);
12307
+ emitRunJson("DEPENDENCY_DRIFT", 1, {
12308
+ head,
12309
+ checks: { diff: diffCode, worktree: workCode }
12310
+ });
11945
12311
  return 1;
11946
12312
  }
11947
12313
  err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
11948
12314
  }
11949
12315
  let verifierFinalDependencyAttestation;
11950
- out("\n[envelope] pristine verification against the trusted base:");
12316
+ say("\n[envelope] pristine verification against the trusted base:");
11951
12317
  const verifyCode = runVerify({
11952
12318
  cwd,
11953
12319
  base,
@@ -11962,7 +12328,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11962
12328
  }
11963
12329
  } : {},
11964
12330
  allowDepDrift: opts.allowDepDrift,
11965
- verifierBackend
12331
+ verifierBackend,
12332
+ silent: opts.json
11966
12333
  });
11967
12334
  if (opts.settle && opts.settle > 0) {
11968
12335
  waitForSettleSync(opts.settle);
@@ -11975,7 +12342,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
11975
12342
  const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
11976
12343
  if (dependencyEnvironment && process.env.TAMPERWARD_DIAGNOSTICS === "1") {
11977
12344
  const metrics = dependencyEnvironmentDiagnostics(dependencyEnvironment);
11978
- out(
12345
+ say(
11979
12346
  `tamperward run \u2014 dependency attestation diagnostics: full_snapshots=${metrics.fullSnapshots} reused_snapshots=${metrics.reusedSnapshots} total_ms=${Number(metrics.totalMs.toFixed(3))} verifier_final_attestation=${verifierFinalDependencyAttestation ? "yes" : "no"} run_lifecycle_owned=${agentRun.lifecycleOwned ? "yes" : "no"} entry_reuse=${canReuseAdjacentDependencyAttestation(agentRun.lifecycleOwned) ? "yes" : "no"}`
11980
12347
  );
11981
12348
  }
@@ -12003,7 +12370,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
12003
12370
  cwd,
12004
12371
  base,
12005
12372
  head,
12006
- frozenPolicy
12373
+ frozenPolicy,
12374
+ say
12007
12375
  );
12008
12376
  observerBlocked = observed.blocking;
12009
12377
  }
@@ -12012,10 +12380,25 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
12012
12380
  const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
12013
12381
  const enforcement = cannot ? 2 : blocked ? 1 : 0;
12014
12382
  const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK \u2014 observed temporal weakening under strict transient policy" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentTimedOut ? "AGENT_TIMEOUT \u2014 runtime budget expired; post-timeout enforcement clean" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
12383
+ const machineVerdict = enforcement === 2 ? "CANNOT_ADJUDICATE" : rewrote ? "OBJECT_REWRITE" : depsDrifted ? "DEPENDENCY_DRIFT" : notQuiescent ? "NOT_QUIESCENT" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK" : enforcement === 1 ? "ENFORCEMENT_FAILED" : agentTimedOut ? "AGENT_TIMEOUT" : agentExit !== 0 ? "AGENT_FAILED" : "VERIFIED";
12015
12384
  const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
12016
- out(`
12385
+ const exitCode = enforcement !== 0 ? enforcement : agentTimedOut ? 124 : agentExit;
12386
+ if (opts.json) {
12387
+ const cannotReason = enforcement !== 2 ? null : verifyCode === 2 ? "VERIFY_CANNOT_VERIFY" : diffCode === 2 ? "CHECK_DIFF_UNJUDGEABLE" : "CHECK_WORKTREE_UNJUDGEABLE";
12388
+ emitRunJson(machineVerdict, exitCode, {
12389
+ head,
12390
+ checks: { diff: diffCode, worktree: workCode, verify: verifyCode },
12391
+ observer: {
12392
+ enabled: Boolean(opts.observeTransients),
12393
+ blocking: observerBlocked
12394
+ },
12395
+ ...cannotReason ? { reason: cannotReason } : {}
12396
+ }, true);
12397
+ } else {
12398
+ say(`
12017
12399
  tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
12018
- return enforcement !== 0 ? enforcement : agentTimedOut ? 124 : agentExit;
12400
+ }
12401
+ return exitCode;
12019
12402
  }
12020
12403
  function parseRun(args) {
12021
12404
  const o = { argv: [] };
@@ -12027,6 +12410,7 @@ function parseRun(args) {
12027
12410
  } else if (a === "--base") o.base = args[++i];
12028
12411
  else if (a === "--cmd") o.cmd = args[++i];
12029
12412
  else if (a === "--budget") o.budget = Number(args[++i]);
12413
+ else if (a === "--json") o.json = true;
12030
12414
  else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
12031
12415
  else if (a === "--allow-dirty") o.allowDirty = true;
12032
12416
  else if (a === "--settle") o.settle = Number(args[++i]);
@@ -12073,14 +12457,14 @@ function lifecyclePlatformCheck(platform = process.platform, linuxPython = platf
12073
12457
  var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
12074
12458
  function err2(message, opts, id = "doctor", checks = []) {
12075
12459
  if (opts?.json) {
12076
- const report2 = {
12460
+ const report2 = machineOutput({
12077
12461
  command: "doctor",
12078
12462
  authoritative: false,
12079
12463
  checks: [
12080
12464
  ...checks.filter((check2) => check2.id !== id),
12081
12465
  { id, state: "BROKEN", detail: message }
12082
12466
  ]
12083
- };
12467
+ });
12084
12468
  process.stdout.write(JSON.stringify(report2) + "\n");
12085
12469
  } else {
12086
12470
  process.stderr.write(`tamperward doctor: ${message}
@@ -12094,7 +12478,7 @@ function policyFor(opts, cwd) {
12094
12478
  return atBase ?? defaultPolicy();
12095
12479
  }
12096
12480
  function asMapping(value) {
12097
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
12481
+ return isRecord(value) ? value : null;
12098
12482
  }
12099
12483
  function wiringState(status, authority) {
12100
12484
  if (status === "ok") return "OK";
@@ -12293,7 +12677,7 @@ function observerCheck(cwd) {
12293
12677
  function emitReport(opts, checks) {
12294
12678
  const authoritative = !checks.some((x) => x.state === "BROKEN");
12295
12679
  if (opts.json) {
12296
- const report2 = { command: "doctor", authoritative, checks };
12680
+ const report2 = machineOutput({ command: "doctor", authoritative, checks });
12297
12681
  process.stdout.write(JSON.stringify(report2) + "\n");
12298
12682
  return;
12299
12683
  }
@@ -12415,8 +12799,8 @@ function githubApi(cwd, endpoint) {
12415
12799
  );
12416
12800
  return JSON.parse(stdout);
12417
12801
  } catch (e) {
12418
- const x = e;
12419
- const detail = x.stderr ? String(x.stderr).replace(/\s+/g, " ").trim() : x.message;
12802
+ const failure = execFailure(e);
12803
+ const detail = failure.stderr ? failure.stderr.replace(/\s+/g, " ").trim() : failure.message;
12420
12804
  throw new Error(
12421
12805
  "GitHub API " + endpoint + " failed: " + (detail || "unknown error")
12422
12806
  );
@@ -12727,7 +13111,7 @@ function summarizeTraceRuns(opts) {
12727
13111
  for (const item of run) {
12728
13112
  const rel = normalRepoPath(opts.root, item.path);
12729
13113
  const isTracked = rel !== null && opts.tracked.has(rel);
12730
- const keyPath = isTracked ? rel : rel === null ? resolve9(opts.root, item.path) : null;
13114
+ const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve9(opts.root, item.path);
12731
13115
  if (keyPath === null) continue;
12732
13116
  const target = isTracked ? repo : external;
12733
13117
  const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
@@ -12812,7 +13196,7 @@ function materializeBase(base, cwd, dest) {
12812
13196
  }
12813
13197
  function traceFiles(prefix) {
12814
13198
  const dir = resolve9(prefix, "..");
12815
- const base = prefix.split(sep3).at(-1);
13199
+ const base = prefix.split(sep3).at(-1) ?? prefix;
12816
13200
  return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join19(dir, name)).sort();
12817
13201
  }
12818
13202
  function rewriteTraceRoot(accesses, actualRoot) {
@@ -13149,7 +13533,7 @@ function validateCliArgs(cmd, args) {
13149
13533
  const prefix = args.slice(0, delimiter3);
13150
13534
  const command = args.slice(delimiter3 + 1);
13151
13535
  const parsed = validateFlatArgs(prefix, {
13152
- flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
13536
+ flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients", "--json"],
13153
13537
  values: {
13154
13538
  "--base": "string",
13155
13539
  "--cmd": "string",
@@ -13250,7 +13634,8 @@ Formats:
13250
13634
  github GitHub Actions view: one inline annotation per finding (so it shows up
13251
13635
  on the line in "Files changed") plus a job-summary table on the run
13252
13636
  page, alongside the same text output in the log.
13253
- json the findings verbatim, plus a summary count.
13637
+ json versioned machine-readable output. Top-level schema_version identifies
13638
+ the public JSON-schema major; published schemas live under schemas/.
13254
13639
  auto github when GITHUB_ACTIONS=true, otherwise text.
13255
13640
  tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
13256
13641
  tamperward sweep claude Stop sweep (re-scan the turn's working tree)
@@ -13279,11 +13664,13 @@ Formats:
13279
13664
  [--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
13280
13665
  [--allow-dirty] [--settle S] left (policy over base...HEAD and the
13281
13666
  [--allow-dep-drift] [--cwd D] worktree, plus verify).
13282
- [--observe-transients] --observe-transients starts a session-
13667
+ [--observe-transients] [--json] --observe-transients starts a session-
13283
13668
  scoped observer before the agent and
13284
13669
  reports temporal evidence/health after it.
13285
13670
  --agent-budget bounds the agent runtime
13286
13671
  itself and still adjudicates after timeout.
13672
+ --json emits one final versioned run verdict
13673
+ document on stdout after agent adjudication.
13287
13674
  Exit: agent's
13288
13675
  code when clean; 124 on clean AGENT_TIMEOUT;
13289
13676
  1 on any blocking finding/masked failure;