tamperward 2.16.5 → 2.17.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/README.md CHANGED
@@ -505,17 +505,20 @@ option can never be reinterpreted as the agent command.
505
505
 
506
506
  ### The rules
507
507
 
508
- Seventeen rules are specified and fifteen ship (see the table in
508
+ Seventeen rules are specified and sixteen ship (see the table in
509
509
  [SPEC.md](./SPEC.md)). The families: test protection (`test-deletion`,
510
- `test-skip`, `test-content-removal`), verification-signal protection
510
+ `test-skip`, `test-content-removal`, plus the warning-only JS/TS
511
+ `assertion-weakening` heuristic), verification-signal protection
511
512
  (`coverage-lowering`, `snapshot-rewrite`, `snapshot-only-rewrite`), suppression
512
513
  (`ts-any-cast`, `ts-any-launder`, `lint-suppression`), pipeline protection
513
514
  (`ci-tampering`, `hook-tampering`, `no-verify`), and the effect/outcome layers
514
515
  (`transient-protected-mutation`, `pristine-verification`, plus the `run` envelope).
515
- Some ambiguous syntactic classes deliberately remain warnings or unimplemented —
516
- `assertion-weakening` and `guard-removal` are reserved names with no detector, and
517
- `ts-any-launder` is a permanent warn — rather than being promoted to blockers
518
- without precision evidence.
516
+ `assertion-weakening` ships only the AST-proven one-way subset — statically
517
+ proven exact/structural values weakened to truthy/defined, positive exception
518
+ specificity removed, or a pure assertion removed from the same suite-qualified
519
+ test. It measured 12/12 true-positive fires with 0/20 false positives on the
520
+ committed detector-specific replay and remains `warn`; `guard-removal` is still
521
+ reserved with no detector, and `ts-any-launder` is a permanent warn.
519
522
 
520
523
  ## What Tamperward does not do
521
524
 
package/dist/cli/index.js CHANGED
@@ -6201,6 +6201,214 @@ var snapshotOnlyRewrite = {
6201
6201
  }
6202
6202
  };
6203
6203
 
6204
+ // src/detectors/assertion-weakening.ts
6205
+ import ts6 from "typescript";
6206
+ var RULE11 = "assertion-weakening";
6207
+ var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
6208
+ var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
6209
+ var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
6210
+ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
6211
+ var compact = (s) => s.replace(/\s+/g, "");
6212
+ function literalTitle(node) {
6213
+ if (!node) return null;
6214
+ if (ts6.isStringLiteral(node) || ts6.isNoSubstitutionTemplateLiteral(node)) return node.text;
6215
+ return null;
6216
+ }
6217
+ function scriptKindForPath(path) {
6218
+ const lower = path.toLowerCase();
6219
+ if (lower.endsWith(".tsx")) return ts6.ScriptKind.TSX;
6220
+ if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
6221
+ return ts6.ScriptKind.TS;
6222
+ }
6223
+ if (lower.endsWith(".jsx")) return ts6.ScriptKind.JSX;
6224
+ return ts6.ScriptKind.JS;
6225
+ }
6226
+ function staticFacts(node) {
6227
+ if (!node) return null;
6228
+ if (node.kind === ts6.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
6229
+ if (node.kind === ts6.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
6230
+ if (node.kind === ts6.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
6231
+ if (ts6.isIdentifier(node) && node.text === "undefined") {
6232
+ return { defined: false, truthy: false };
6233
+ }
6234
+ if (ts6.isVoidExpression(node)) return { defined: false, truthy: false };
6235
+ if (ts6.isStringLiteral(node) || ts6.isNoSubstitutionTemplateLiteral(node)) {
6236
+ return { defined: true, truthy: node.text.length > 0 };
6237
+ }
6238
+ if (ts6.isNumericLiteral(node)) {
6239
+ return { defined: true, truthy: Number(node.text) !== 0 };
6240
+ }
6241
+ if (ts6.isBigIntLiteral(node)) {
6242
+ return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
6243
+ }
6244
+ if (ts6.isPrefixUnaryExpression(node) && (node.operator === ts6.SyntaxKind.MinusToken || node.operator === ts6.SyntaxKind.PlusToken) && ts6.isNumericLiteral(node.operand)) {
6245
+ const value = Number(node.operand.text) * (node.operator === ts6.SyntaxKind.MinusToken ? -1 : 1);
6246
+ return { defined: true, truthy: value !== 0 };
6247
+ }
6248
+ if (ts6.isObjectLiteralExpression(node) || ts6.isArrayLiteralExpression(node) || ts6.isRegularExpressionLiteral(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node) || ts6.isClassExpression(node) || ts6.isNewExpression(node)) {
6249
+ return { defined: true, truthy: true };
6250
+ }
6251
+ return null;
6252
+ }
6253
+ function expectAssertion(node, sf) {
6254
+ if (!ts6.isPropertyAccessExpression(node.expression)) return null;
6255
+ const matcher = node.expression.name.text;
6256
+ let receiver = node.expression.expression;
6257
+ let polarity = "";
6258
+ if (ts6.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
6259
+ polarity = "not";
6260
+ receiver = receiver.expression;
6261
+ }
6262
+ if (!ts6.isCallExpression(receiver) || !ts6.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
6263
+ return null;
6264
+ }
6265
+ const subject = compact(receiver.arguments[0].getText(sf));
6266
+ const args = node.arguments.map((arg) => compact(arg.getText(sf)));
6267
+ return {
6268
+ subject,
6269
+ polarity,
6270
+ matcher,
6271
+ args,
6272
+ firstArgFacts: staticFacts(node.arguments[0]),
6273
+ canonical: [subject, polarity, matcher, ...args].join("|"),
6274
+ text: node.getText(sf).replace(/\s+/g, " ")
6275
+ };
6276
+ }
6277
+ function simpleCallName(node) {
6278
+ return ts6.isIdentifier(node.expression) ? node.expression.text : null;
6279
+ }
6280
+ function callbackFor(node) {
6281
+ const cb = node.arguments[1];
6282
+ return cb && (ts6.isArrowFunction(cb) || ts6.isFunctionExpression(cb)) ? cb : null;
6283
+ }
6284
+ function blocks(src, path) {
6285
+ const out2 = /* @__PURE__ */ new Map();
6286
+ if (langOf(path) !== "js") return out2;
6287
+ try {
6288
+ const sf = ts6.createSourceFile(path, src, ts6.ScriptTarget.Latest, true, scriptKindForPath(path));
6289
+ const visit = (node, suites) => {
6290
+ if (ts6.isCallExpression(node)) {
6291
+ const call = simpleCallName(node);
6292
+ const title = literalTitle(node.arguments[0]);
6293
+ const cb = callbackFor(node);
6294
+ if (call && SUITE_CALLS.has(call) && title !== null && cb) {
6295
+ const nextSuites = [...suites, title];
6296
+ ts6.forEachChild(cb, (child) => visit(child, nextSuites));
6297
+ return;
6298
+ }
6299
+ if (call && TEST_CALLS.has(call) && title !== null && cb) {
6300
+ const assertions = [];
6301
+ const collect2 = (child) => {
6302
+ if (child !== cb && ts6.isCallExpression(child)) {
6303
+ const nestedCall = simpleCallName(child);
6304
+ if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
6305
+ return;
6306
+ }
6307
+ }
6308
+ if (ts6.isCallExpression(child)) {
6309
+ const a = expectAssertion(child, sf);
6310
+ if (a) assertions.push(a);
6311
+ }
6312
+ ts6.forEachChild(child, collect2);
6313
+ };
6314
+ collect2(cb);
6315
+ const identity2 = [...suites, title].join(" > ");
6316
+ const list2 = out2.get(identity2) ?? [];
6317
+ list2.push({ title, identity: identity2, assertions });
6318
+ out2.set(identity2, list2);
6319
+ return;
6320
+ }
6321
+ }
6322
+ ts6.forEachChild(node, (child) => visit(child, suites));
6323
+ };
6324
+ visit(sf, []);
6325
+ } catch {
6326
+ }
6327
+ return out2;
6328
+ }
6329
+ function pureRemoval(before, after) {
6330
+ if (after.length >= before.length) return null;
6331
+ const remaining = /* @__PURE__ */ new Map();
6332
+ for (const a of before) remaining.set(a.canonical, (remaining.get(a.canonical) ?? 0) + 1);
6333
+ for (const a of after) {
6334
+ const n = remaining.get(a.canonical) ?? 0;
6335
+ if (n <= 0) return null;
6336
+ remaining.set(a.canonical, n - 1);
6337
+ }
6338
+ for (const a of before) {
6339
+ const n = remaining.get(a.canonical) ?? 0;
6340
+ if (n > 0) return a;
6341
+ }
6342
+ return null;
6343
+ }
6344
+ function factsProveWeakMatcher(oldA, newMatcher) {
6345
+ if (!EXACT_MATCHERS.has(oldA.matcher) || oldA.args.length !== 1) return false;
6346
+ const facts = oldA.firstArgFacts;
6347
+ if (!facts) return false;
6348
+ if (newMatcher === "toBeDefined") return facts.defined;
6349
+ if (newMatcher === "toBeTruthy") return facts.truthy === true;
6350
+ return false;
6351
+ }
6352
+ function directionalWeakening(before, after) {
6353
+ for (const oldA of before) {
6354
+ for (const newA of after) {
6355
+ if (oldA.subject !== newA.subject) continue;
6356
+ if (oldA.polarity !== "" || newA.polarity !== "") continue;
6357
+ if (WEAK_MATCHERS.has(newA.matcher) && newA.args.length === 0 && factsProveWeakMatcher(oldA, newA.matcher)) {
6358
+ return { before: oldA, after: newA, kind: "matcher" };
6359
+ }
6360
+ if (oldA.matcher === "toThrow" && oldA.args.length > 0 && newA.matcher === "toThrow" && newA.args.length === 0) {
6361
+ return { before: oldA, after: newA, kind: "throw-specificity" };
6362
+ }
6363
+ }
6364
+ }
6365
+ return null;
6366
+ }
6367
+ function findingForBlock(path, before, after, policy) {
6368
+ const weakened = directionalWeakening(before.assertions, after.assertions);
6369
+ if (weakened) {
6370
+ 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.`;
6371
+ return makeFinding(RULE11, policy, {
6372
+ file: path,
6373
+ message,
6374
+ evidence: `${weakened.before.text} -> ${weakened.after.text}`,
6375
+ remediation: "Keep the stronger expectation and fix the code under test. If the contract genuinely changed, review the test change explicitly; this rule is warning-only while its precision is measured."
6376
+ });
6377
+ }
6378
+ const removed = pureRemoval(before.assertions, after.assertions);
6379
+ if (removed) {
6380
+ return makeFinding(RULE11, policy, {
6381
+ file: path,
6382
+ message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
6383
+ evidence: removed.text,
6384
+ remediation: "Keep the assertion and fix the code under test. If the assertion is genuinely obsolete, review the test change explicitly; this rule is warning-only while its precision is measured."
6385
+ });
6386
+ }
6387
+ return null;
6388
+ }
6389
+ var assertionWeakening = {
6390
+ id: RULE11,
6391
+ surface: ["file"],
6392
+ certainty: "heuristic",
6393
+ run(changes, policy) {
6394
+ const out2 = [];
6395
+ for (const c of changes) {
6396
+ if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null || langOf(c.path) !== "js" || !isProtected(c.path, policy, "tests") || isProtected(c.path, policy, "snapshots")) {
6397
+ continue;
6398
+ }
6399
+ const beforeBlocks = blocks(c.before, c.path);
6400
+ const afterBlocks = blocks(c.after, c.path);
6401
+ for (const [identity2, oldList] of beforeBlocks) {
6402
+ const newList = afterBlocks.get(identity2);
6403
+ if (!newList || oldList.length !== 1 || newList.length !== 1) continue;
6404
+ const f = findingForBlock(c.path, oldList[0], newList[0], policy);
6405
+ if (f) out2.push(f);
6406
+ }
6407
+ }
6408
+ return out2;
6409
+ }
6410
+ };
6411
+
6204
6412
  // src/detectors/index.ts
6205
6413
  var allDetectors = [
6206
6414
  noVerify,
@@ -6213,7 +6421,8 @@ var allDetectors = [
6213
6421
  testDeletion,
6214
6422
  testContentRemoval,
6215
6423
  snapshotRewrite,
6216
- snapshotOnlyRewrite
6424
+ snapshotOnlyRewrite,
6425
+ assertionWeakening
6217
6426
  ];
6218
6427
 
6219
6428
  // src/engine.ts
@@ -6575,9 +6784,9 @@ function orderFindings(findings) {
6575
6784
  return (a.line ?? 0) - (b.line ?? 0);
6576
6785
  });
6577
6786
  }
6578
- function countsPhrase(blocks, warns) {
6787
+ function countsPhrase(blocks2, warns) {
6579
6788
  const parts = [];
6580
- if (blocks) parts.push(`${blocks} blocking`);
6789
+ if (blocks2) parts.push(`${blocks2} blocking`);
6581
6790
  if (warns) parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
6582
6791
  return parts.join(", ");
6583
6792
  }
@@ -6589,7 +6798,7 @@ var SIGNOFF_NOTE = "A blocking finding clears only with a human sign-off. In CI
6589
6798
  function renderText(input, opts) {
6590
6799
  const { scanned, ignoredFiles } = input;
6591
6800
  const findings = orderFindings(input.findings);
6592
- const blocks = findings.filter((f) => f.severity === "block");
6801
+ const blocks2 = findings.filter((f) => f.severity === "block");
6593
6802
  const warns = findings.filter((f) => f.severity === "warn");
6594
6803
  const c = opts.colour;
6595
6804
  const out2 = [];
@@ -6597,8 +6806,8 @@ function renderText(input, opts) {
6597
6806
  return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
6598
6807
  `;
6599
6808
  }
6600
- const headColour = blocks.length ? RED : YELLOW;
6601
- out2.push(paint(`tamperward: ${countsPhrase(blocks.length, warns.length)}`, BOLD + headColour, c));
6809
+ const headColour = blocks2.length ? RED : YELLOW;
6810
+ out2.push(paint(`tamperward: ${countsPhrase(blocks2.length, warns.length)}`, BOLD + headColour, c));
6602
6811
  out2.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
6603
6812
  out2.push("");
6604
6813
  const LABEL = "sign-off".length;
@@ -6618,7 +6827,7 @@ function renderText(input, opts) {
6618
6827
  if (f.signoff.required) row("sign-off", [stripControl(f.signoff.command)]);
6619
6828
  out2.push("");
6620
6829
  }
6621
- if (blocks.length > 0) {
6830
+ if (blocks2.length > 0) {
6622
6831
  out2.push(
6623
6832
  wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
6624
6833
  );
@@ -6668,7 +6877,7 @@ function locationCell(f, env) {
6668
6877
  }
6669
6878
  function renderSummary(input, env = process.env) {
6670
6879
  const findings = orderFindings(input.findings);
6671
- const blocks = findings.filter((f) => f.severity === "block");
6880
+ const blocks2 = findings.filter((f) => f.severity === "block");
6672
6881
  const warns = findings.filter((f) => f.severity === "warn");
6673
6882
  const scope = scopePhrase(input.scanned, input.ignoredFiles);
6674
6883
  if (findings.length === 0) {
@@ -6678,7 +6887,7 @@ No integrity findings (${scope}).
6678
6887
  `;
6679
6888
  }
6680
6889
  const out2 = [
6681
- `## Tamperward: ${countsPhrase(blocks.length, warns.length)}`,
6890
+ `## Tamperward: ${countsPhrase(blocks2.length, warns.length)}`,
6682
6891
  "",
6683
6892
  `${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
6684
6893
  "",
@@ -6731,12 +6940,12 @@ function report(input) {
6731
6940
  const { findings, scanned, ignoredFiles } = input;
6732
6941
  const format = resolveFormat(input);
6733
6942
  if (format === "json") {
6734
- const blocks = findings.filter((f) => f.severity === "block").length;
6943
+ const blocks2 = findings.filter((f) => f.severity === "block").length;
6735
6944
  process.stdout.write(
6736
6945
  JSON.stringify(
6737
6946
  // `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
6738
6947
  // is additive, so an existing consumer reading only the old keys is unaffected.
6739
- { findings, scanned, ignoredFiles, summary: { block: blocks, warn: findings.length - blocks } },
6948
+ { findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
6740
6949
  null,
6741
6950
  2
6742
6951
  ) + "\n"
@@ -6833,10 +7042,10 @@ import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writ
6833
7042
  import { createHash as createHash5 } from "node:crypto";
6834
7043
 
6835
7044
  // src/adapters/claude/deny.ts
6836
- function formatDenial(blocks) {
6837
- const lead = blocks[0];
7045
+ function formatDenial(blocks2) {
7046
+ const lead = blocks2[0];
6838
7047
  const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
6839
- for (const f of blocks) {
7048
+ for (const f of blocks2) {
6840
7049
  const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
6841
7050
  lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
6842
7051
  }
@@ -6845,7 +7054,7 @@ function formatDenial(blocks) {
6845
7054
  lines.push(
6846
7055
  "The other shortcuts are blocked too: skipping the hooks, editing the hook or CI workflow, lowering the coverage gate, and rewriting a protected file from the shell will each be denied."
6847
7056
  );
6848
- if (blocks.some((b) => b.signoff.required)) {
7057
+ if (blocks2.some((b) => b.signoff.required)) {
6849
7058
  lines.push(`If this change is genuinely correct, a human must sign off: ${lead.signoff.command}`);
6850
7059
  }
6851
7060
  return lines.join("\n") + "\n";
@@ -7263,7 +7472,7 @@ function runWatch(args) {
7263
7472
 
7264
7473
  // src/detectors/fs-events.ts
7265
7474
  import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
7266
- var RULE11 = "transient-protected-mutation";
7475
+ var RULE12 = "transient-protected-mutation";
7267
7476
  var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
7268
7477
  var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
7269
7478
  var DEFAULT_EVENT_READ_IO = {
@@ -7458,7 +7667,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
7458
7667
  };
7459
7668
  }
7460
7669
  function transientFindings(events, persistentPaths, policy, finalHash) {
7461
- if (!isEnabled(RULE11, policy)) return [];
7670
+ if (!isEnabled(RULE12, policy)) return [];
7462
7671
  const byPath = /* @__PURE__ */ new Map();
7463
7672
  for (const e of events) {
7464
7673
  if (!isProtected(e.path, policy)) continue;
@@ -7478,7 +7687,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
7478
7687
  const modeChurn = modes.size > 1;
7479
7688
  if (!contentChurn && !modeChurn) continue;
7480
7689
  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 ")})`;
7481
- const f = makeFinding(RULE11, policy, {
7690
+ const f = makeFinding(RULE12, policy, {
7482
7691
  file: path,
7483
7692
  message: `Transient protected mutation: ${what}, leaving no net diff.`,
7484
7693
  evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
@@ -7517,11 +7726,11 @@ function parseInput(raw) {
7517
7726
  }
7518
7727
  return parsed;
7519
7728
  }
7520
- function recordDenylog(blocks) {
7729
+ function recordDenylog(blocks2) {
7521
7730
  const log = process.env.TAMPERWARD_DENYLOG;
7522
7731
  if (!log) return;
7523
7732
  try {
7524
- appendFileSync4(log, blocks.map((b) => b.rule).join(",") + "\n");
7733
+ appendFileSync4(log, blocks2.map((b) => b.rule).join(",") + "\n");
7525
7734
  } catch {
7526
7735
  }
7527
7736
  }
@@ -7540,10 +7749,10 @@ function failClosed(kind, detail) {
7540
7749
  };
7541
7750
  return verdict([f], kind);
7542
7751
  }
7543
- function verdict(blocks, kind) {
7544
- if (blocks.length === 0) return { exitCode: 0, stdout: "" };
7545
- recordDenylog(blocks);
7546
- const reason = formatDenial(blocks);
7752
+ function verdict(blocks2, kind) {
7753
+ if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
7754
+ recordDenylog(blocks2);
7755
+ const reason = formatDenial(blocks2);
7547
7756
  const payload = kind === "PreToolUse" ? {
7548
7757
  hookSpecificOutput: {
7549
7758
  hookEventName: "PreToolUse",
@@ -7600,14 +7809,14 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7600
7809
  if (c.oldPath) seen.add(c.oldPath);
7601
7810
  }
7602
7811
  const changes = [];
7603
- const blocks = [];
7812
+ const blocks2 = [];
7604
7813
  const trusted = base ?? "HEAD";
7605
7814
  const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
7606
7815
  for (const rel of [...d.changed, ...d.deleted]) {
7607
7816
  if (seen.has(rel)) continue;
7608
7817
  const unjudgeable = unjudgeableFinding(cwd, rel);
7609
7818
  if (unjudgeable) {
7610
- blocks.push(unjudgeable);
7819
+ blocks2.push(unjudgeable);
7611
7820
  continue;
7612
7821
  }
7613
7822
  const after = fileOnDisk(rel, { cwd });
@@ -7627,7 +7836,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7627
7836
  }
7628
7837
  }
7629
7838
  if (before == null) {
7630
- blocks.push({
7839
+ blocks2.push({
7631
7840
  rule: "hidden-drift",
7632
7841
  severity: "block",
7633
7842
  file: rel,
@@ -7660,12 +7869,12 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7660
7869
  if (seen.has(rel)) continue;
7661
7870
  const unjudgeable = unjudgeableFinding(cwd, rel);
7662
7871
  if (unjudgeable) {
7663
- blocks.push(unjudgeable);
7872
+ blocks2.push(unjudgeable);
7664
7873
  continue;
7665
7874
  }
7666
7875
  const after = fileOnDisk(rel, { cwd });
7667
7876
  if (after == null) {
7668
- blocks.push({
7877
+ blocks2.push({
7669
7878
  rule: "hidden-drift",
7670
7879
  severity: "block",
7671
7880
  file: rel,
@@ -7678,7 +7887,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
7678
7887
  }
7679
7888
  changes.push(...synthFileChange(rel, null, after));
7680
7889
  }
7681
- return { changes, blocks };
7890
+ return { changes, blocks: blocks2 };
7682
7891
  }
7683
7892
  function effectDriftBlocks(cwd, sessionId, policy) {
7684
7893
  if (!sessionId || !isGitRepo(cwd)) return null;
@@ -7703,8 +7912,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
7703
7912
  );
7704
7913
  const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
7705
7914
  const judged = [...scoped, ...gap.changes];
7706
- const blocks = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
7707
- if (blocks.length > 0) return blocks;
7915
+ const blocks2 = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
7916
+ if (blocks2.length > 0) return blocks2;
7708
7917
  savePtree(cwd, sessionId, current);
7709
7918
  return null;
7710
7919
  }
@@ -7736,9 +7945,9 @@ function preToolUseVerdict(input) {
7736
7945
  const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
7737
7946
  if (driftBlocks) return verdict(driftBlocks, "PreToolUse");
7738
7947
  const changes = changesFromClaudeHook(input, cwd);
7739
- const blocks = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
7740
- if (blocks.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
7741
- return verdict(blocks, "PreToolUse");
7948
+ const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
7949
+ if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
7950
+ return verdict(blocks2, "PreToolUse");
7742
7951
  } catch (e) {
7743
7952
  return failClosed("PreToolUse", errText2(e));
7744
7953
  }
@@ -7840,7 +8049,7 @@ function stopVerdict(input) {
7840
8049
  if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
7841
8050
  const cwd = input.cwd ?? process.cwd();
7842
8051
  if (!isGitRepo(cwd)) return { exitCode: 0, stdout: "" };
7843
- let blocks;
8052
+ let blocks2;
7844
8053
  let commitCursor = () => {
7845
8054
  };
7846
8055
  try {
@@ -7855,22 +8064,22 @@ function stopVerdict(input) {
7855
8064
  changes = changes.concat(gap.changes);
7856
8065
  hiddenBlocks = gap.blocks;
7857
8066
  }
7858
- blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
8067
+ blocks2 = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
7859
8068
  const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
7860
- blocks = blocks.concat(transient.blocks);
8069
+ blocks2 = blocks2.concat(transient.blocks);
7861
8070
  commitCursor = transient.commit;
7862
- if (blocks.length === 0 && input.session_id && current) {
8071
+ if (blocks2.length === 0 && input.session_id && current) {
7863
8072
  savePtree(cwd, input.session_id, current);
7864
8073
  saveTurnTree(cwd, input.session_id, current);
7865
8074
  }
7866
8075
  } catch (e) {
7867
8076
  return failClosed("Stop", errText2(e));
7868
8077
  }
7869
- if (blocks.length === 0) {
8078
+ if (blocks2.length === 0) {
7870
8079
  advanceTurnBaseline(cwd, input.session_id);
7871
8080
  commitCursor();
7872
8081
  }
7873
- return verdict(blocks, "Stop");
8082
+ return verdict(blocks2, "Stop");
7874
8083
  }
7875
8084
  function emit(r) {
7876
8085
  if (r.stdout) process.stdout.write(r.stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.16.5",
3
+ "version": "2.17.0",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",