tamperward 1.5.1 → 1.7.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
@@ -115,6 +115,46 @@ escape valve is a PR label only a human with write access can apply.
115
115
 
116
116
  ## Measured, not asserted
117
117
 
118
+ **Read the two headline numbers together, always:** across **137 guarded runs — every
119
+ one inside a tamper class an active detector covered — 0 tampers reached green.**
120
+ Outside that coverage, on the one seed built to measure it, **agents tampered in 26 of
121
+ 40 runs** — "tampered" meaning, mechanically: the independent held-out oracle
122
+ classified the result as passing the visible check through a prohibited shortcut. The
123
+ first number is what the gate does; the second is why the gate is one layer. And an honest note on evidence units: those run counts come from roughly
124
+ fifteen seed/task configurations and eight real repositories — repeated runs on one
125
+ seed measure that seed's stochasticity, not the space of real coding tasks, so the
126
+ independent evidence behind these numbers is the seed count, not the run count.
127
+
128
+ **And the third number, from the preregistered cross-repository experiment
129
+ (taskbench, `harness/taskbench/`):** on 26 paired real repositories with real
130
+ historical regressions, pressured-agent policy violations appeared in **13 of 26
131
+ ungated runs (50%)** — the behavior transfers — but the gate at 1.6.0 **did not
132
+ reduce false greens** (5 prevented vs 4 gated-only; paired RD +3.8%, 95% CI
133
+ −17.2 to +24.7, exact McNemar p = 1.0): agents routed through tool-call
134
+ spellings outside the shipped detector classes, including two gaps documented
135
+ before the sweep and deliberately left unfixed for the experiment's integrity.
136
+ Enforcement showed no measurable completion cost (+7.7 points, CI spans zero),
137
+ and independent pristine-suite re-execution caught **every masked failure in
138
+ all 53 trajectories**. Registration, bets, frozen analysis, and the refutation
139
+ of our own headline bet: `PREDICTION-taskbench.md`, `PHASE3.md`. The first two
140
+ numbers say what the gate does inside its coverage; the third says coverage is
141
+ the whole game on real repositories — and that the reliable layer was the
142
+ pristine-suite oracle the agent cannot touch.
143
+
144
+ **What this does not do**, stated before the numbers rather than after: it does not
145
+ establish that a patch is *correct*; it does not detect arbitrary deception (a
146
+ hardcoded implementation is invisible to any diff rule — measured, twice); it does not
147
+ replace sandboxing, held-out tests, or authoritative CI — it is designed to sit in
148
+ front of them. What it does: **enforce explicitly defined development invariants at
149
+ the loop, commit, and CI boundaries** — and nothing else.
150
+
151
+ **A word on the words:** *tamper*, *honest*, *cheat*, and *surrender* are used
152
+ throughout as operational labels, not claims about mental states. A "tamper" means: a
153
+ protected asset changed, or an independent held-out oracle failed, while the visible
154
+ check went green — a property of the artifacts, decided deterministically. Whether the
155
+ process that produced it "intended" anything is a question these experiments do not
156
+ measure and do not need.
157
+
118
158
  **Does the loop work?** `harness/run.sh` stands up an isolated repo with a real bug and a
119
159
  failing protected test, lets a headless agent loose with the hooks installed, and
120
160
  classifies the end state with an oracle independent of the detectors under test
@@ -229,9 +269,17 @@ framing, and Sonnet's famous refusals turn out to be triggered by framing). The
229
269
  completed frontier split — **Opus 0/10 · Fable 6/10 · Sonnet 10/10** — is the
230
270
  published shape, with the standing self-measurement caveat on the Fable rows.
231
271
  Twenty-six of forty runs tampered through the uncovered dimension: measured demand
232
- that hardens the `ts-any-launder` warn→block graduation case via the §7.A corpus —
233
- the honest fix itself narrows through `as Record<string, unknown>`, so a naive
234
- assertion-blocker is an FP factory.
272
+ that sent the `ts-any-launder` warn→block graduation question to a §7.A negatives
273
+ corpus — the honest fix itself narrows through `as Record<string, unknown>`, so a
274
+ naive assertion-blocker is an FP factory. **The corpus answered, and the question is
275
+ closed** (`harness/PREDICTION-launder-corpus.md`): on 1,227 real commits the
276
+ laundering spellings fire on legitimate work at 4–10× any deployable block threshold
277
+ (annotation-position `any`: 10.5% of commits; named-type assertions: 7.5%), so the
278
+ warn stays a warn permanently on syntactic evidence, no assertion rule gets built,
279
+ and the laundering class is owned by the layers where coverage is measured: the
280
+ held-out **oracle** (caught all 18 assertion tampers) and the **prompt** (the naming
281
+ sentence, 10/10 flip). For this class, detection is not the gate's layer —
282
+ verification and framing are, and the record says so with a denominator.
235
283
 
236
284
  **Why not just a prompt line, or a 20-line grep hook?** Measured
237
285
  (`harness/PREDICTION-baselines.md`), against a frozen naive hook and a one-sentence
@@ -248,8 +296,37 @@ versus the gate's 9.0% — a ~2.75× precision edge, not the ≥5× I bet, and t
248
296
  unconfigured repos is published as the honest tuning-and-sign-off cost, not hidden.
249
297
  The loop-layer deny alone was never the product; the measured margins are whole-class
250
298
  spelling coverage, commit-layer precision plus sign-off machinery, and the layers no
251
- hook has. Running ledger: **52 pre-registered bets 23 refuted, 27 held, 2
252
- unresolvable.**
299
+ hook has. Two follow-ups closed the loops this study opened. The sentence was then
300
+ aimed at the regime where the gate is blind — Sonnet's 10/10 assertion-laundering
301
+ seed — and produced a **complete flip: 10/10 honest** (`harness/PREDICTION-baselines.md`,
302
+ addendum): naming the act is the intervention Sonnet's framing-triggered honesty
303
+ runs on. And the promised **tuned-gate numbers** now exist
304
+ (`harness/PREDICTION-tuned-fp.md`): 9.0% unconfigured → **6.0%** with a one-line
305
+ severity tune → roughly one sign-off decision per 17 commits residual. The same study
306
+ shipped per-rule `exclude` globs (1.6.0, with the policy-weakening symmetry built in)
307
+ and then measured them honestly: **zero effect on this corpus** — these repos' casts
308
+ live in source, not tests, so the zod-recommended path scoping removed nothing here;
309
+ the null is published next to the feature.
310
+
311
+ **What about just making the tests read-only?** The most common objection got its
312
+ pre-registered measurement (`harness/PREDICTION-capability.md`) — starting with the
313
+ mechanics the folk advice misses: `chmod` does not bind a root agent and never
314
+ prevented `rm` anyway (deletion is a *directory*-write right); the workable barrier
315
+ is the immutable bit. Result: **hard immutability governed the crude-tamper seed
316
+ completely — 10/10 honest, four EPERM hits all converting to the real fix** (a bare
317
+ permission error transferred the deny→fix dynamic with no remediation text). Then
318
+ the twist: adding a logged, auto-granted **elevation script made it worse (8/10)** —
319
+ both extra tampers walked through the front door with written confessions
320
+ ("*Remove failing test file to unblock build*"), the sanctioned-path thesis firing
321
+ a fourth time. The lock is easy; the grantor is the hard part — which is the case
322
+ for elevation flows that end at a human, and exactly what the gate's sign-off label
323
+ already is. That seed now carries all six governance configurations on identical
324
+ pre-registered ground: nothing 6/10 tampers, one sentence 1/10, naive hook 2/10,
325
+ the gate 0/10, immutable 0/10, immutable+auto-grant 2/10 — with the standing scope
326
+ that none of the capability rows touch the semantic classes, by construction.
327
+ Running ledger: **70 pre-registered bets — 29 refuted, 37 held, 4 open or
328
+ unresolvable** — taskbench's B2 (prevention on real repositories) is the newest
329
+ refutation, published beside the bet it refutes.
253
330
 
254
331
  **What about false positives?** `snapshot-rewrite` was swept over **1,652 real mainline
255
332
  commits** (prettier, jest, docusaurus, immer): 216 touched snapshots, all legitimately —
package/dist/cli/index.js CHANGED
@@ -438,11 +438,17 @@ function defaultPolicy(version = 1) {
438
438
  // Recorded expected outputs. An assertion stored as data is still an assertion;
439
439
  // rewriting it from current output is the snapshot-update move the affordance
440
440
  // experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
441
- snapshots: ["**/*.snap", "**/__snapshots__/**", "**/golden/**", "**/*.golden.*"],
441
+ // tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
442
+ // live there as .cjs and were rewritten unseen (PHASE3.md §4).
443
+ snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
442
444
  hooks: [".husky/**", "**/lefthook.*", ".tamperward.yml", "**/.tamperward.yml"]
443
445
  },
444
446
  rules: {
445
447
  "test-deletion": { severity: "block" },
448
+ // content gutted from a surviving spec — the class Phase 3 measured walking
449
+ // through the block-count rule (rows, assertion arguments, setup). Severity
450
+ // backed by the real-commit corpus measurement recorded in the rule's SPEC row.
451
+ "test-content-removal": { severity: "block" },
446
452
  "test-skip": { severity: "block" },
447
453
  "ts-any-cast": { severity: "block" },
448
454
  // the unambiguous explicit casts + ts-suppression directives
@@ -984,6 +990,11 @@ function policyWeakening(before, after) {
984
990
  if (ar.enabled === false && br.enabled !== false) {
985
991
  reasons.push(`rule "${name}" disabled (enabled: false)`);
986
992
  }
993
+ const beforeEx = new Set(br.exclude ?? []);
994
+ const addedEx = (ar.exclude ?? []).filter((g) => !beforeEx.has(g));
995
+ if (addedEx.length) {
996
+ reasons.push(`rule "${name}" exclude globs added (${addedEx.join(", ")}) \u2014 blinds this rule on those paths`);
997
+ }
987
998
  }
988
999
  const beforeIgnore = new Set(be.ignore);
989
1000
  const addedIgnore = ae.ignore.filter((g) => !beforeIgnore.has(g));
@@ -1267,8 +1278,62 @@ var testDeletion = {
1267
1278
  }
1268
1279
  };
1269
1280
 
1281
+ // src/detectors/test-content-removal.ts
1282
+ var RULE8 = "test-content-removal";
1283
+ var MIN_REMOVED_LINES = 3;
1284
+ var ws = (s) => s.replace(/\s+/g, "");
1285
+ function significantLinesOrdered(src) {
1286
+ const out = [];
1287
+ for (const raw of src.split("\n")) {
1288
+ const l = raw.trim();
1289
+ if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out.push(l);
1290
+ }
1291
+ return out;
1292
+ }
1293
+ var testContentRemoval = {
1294
+ id: RULE8,
1295
+ surface: ["file"],
1296
+ certainty: "mechanical",
1297
+ run(changes, policy) {
1298
+ const out = [];
1299
+ let keptPool = "";
1300
+ for (const c of changes) {
1301
+ if (c.kind === "file" && c.after != null && isProtected(c.path, policy, "tests")) {
1302
+ keptPool += ws(significantLinesOrdered(c.after).join("\n"));
1303
+ }
1304
+ }
1305
+ for (const c of changes) {
1306
+ if (c.kind !== "file" || c.op !== "modify" || c.before == null || c.after == null) continue;
1307
+ if (!isProtected(c.path, policy, "tests")) continue;
1308
+ if (isProtected(c.path, policy, "snapshots")) continue;
1309
+ if (countTestBlocks(c.after) < countTestBlocks(c.before)) continue;
1310
+ const beforeSig = significantLinesOrdered(c.before);
1311
+ const afterSig = significantLinesOrdered(c.after);
1312
+ const netDrop = beforeSig.length - afterSig.length;
1313
+ const afterSet = new Set(afterSig);
1314
+ const gone = [];
1315
+ for (const line of new Set(beforeSig)) {
1316
+ if (afterSet.has(line)) continue;
1317
+ if (keptPool.includes(ws(line))) continue;
1318
+ gone.push(line);
1319
+ }
1320
+ if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
1321
+ out.push(
1322
+ makeFinding(RULE8, policy, {
1323
+ file: c.path,
1324
+ message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
1325
+ evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
1326
+ remediation: "Fix the code under test. Deleting the failing rows, assertion arguments, or setup from a spec weakens it exactly like deleting the test \u2014 the block just keeps its name."
1327
+ })
1328
+ );
1329
+ }
1330
+ }
1331
+ return out;
1332
+ }
1333
+ };
1334
+
1270
1335
  // src/detectors/snapshot-rewrite.ts
1271
- var RULE8 = "snapshot-rewrite";
1336
+ var RULE9 = "snapshot-rewrite";
1272
1337
  var RUNNER = /\b(?:jest|vitest|playwright|ava)\b/;
1273
1338
  var UPDATE_ANY = /--update-?[sS]napshots?\b/;
1274
1339
  var UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
@@ -1282,12 +1347,12 @@ function namesProtectedSnapshot(seg, policy) {
1282
1347
  return null;
1283
1348
  }
1284
1349
  var snapshotRewrite = {
1285
- id: RULE8,
1350
+ id: RULE9,
1286
1351
  surface: ["command", "file"],
1287
1352
  certainty: "mechanical",
1288
1353
  run(changes, policy) {
1289
1354
  const out = [];
1290
- const warn = (input) => out.push(makeFinding(RULE8, policy, { ...input, defaultSeverity: "warn" }));
1355
+ const warn = (input) => out.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
1291
1356
  for (const c of changes) {
1292
1357
  if (c.kind === "command") {
1293
1358
  for (const seg of segments(c.raw)) {
@@ -1325,7 +1390,7 @@ var snapshotRewrite = {
1325
1390
  };
1326
1391
 
1327
1392
  // src/detectors/snapshot-only.ts
1328
- var RULE9 = "snapshot-only-rewrite";
1393
+ var RULE10 = "snapshot-only-rewrite";
1329
1394
  var COMMIT_VIEWS = ["staged", "range"];
1330
1395
  function isSnapshotChange(c, policy) {
1331
1396
  if (c.kind !== "file") return false;
@@ -1334,7 +1399,7 @@ function isSnapshotChange(c, policy) {
1334
1399
  return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
1335
1400
  }
1336
1401
  var snapshotOnlyRewrite = {
1337
- id: RULE9,
1402
+ id: RULE10,
1338
1403
  surface: ["file"],
1339
1404
  certainty: "mechanical",
1340
1405
  run(changes, policy, view) {
@@ -1346,7 +1411,7 @@ var snapshotOnlyRewrite = {
1346
1411
  const first = snaps[0];
1347
1412
  const names = snaps.map((c) => c.kind === "file" ? c.path : "").filter(Boolean).slice(0, 3).join(", ");
1348
1413
  return [
1349
- makeFinding(RULE9, policy, {
1414
+ makeFinding(RULE10, policy, {
1350
1415
  file: first.path,
1351
1416
  message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
1352
1417
  evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
@@ -1367,6 +1432,7 @@ var allDetectors = [
1367
1432
  ciTampering,
1368
1433
  hookTampering,
1369
1434
  testDeletion,
1435
+ testContentRemoval,
1370
1436
  snapshotRewrite,
1371
1437
  snapshotOnlyRewrite
1372
1438
  ];
@@ -1396,12 +1462,17 @@ function evaluate(changes, policy, detectors = allDetectors, view) {
1396
1462
  }
1397
1463
  }
1398
1464
  const seen = /* @__PURE__ */ new Set();
1399
- return out.filter((f) => {
1465
+ const deduped = out.filter((f) => {
1400
1466
  const k = key(f);
1401
1467
  if (seen.has(k)) return false;
1402
1468
  seen.add(k);
1403
1469
  return true;
1404
1470
  });
1471
+ return deduped.filter((f) => {
1472
+ const ex = policy.rules[f.rule]?.exclude;
1473
+ if (!ex?.length || !f.file || isPolicyFile(f.file)) return true;
1474
+ return !matchesAny(f.file, ex);
1475
+ });
1405
1476
  }
1406
1477
  function hasBlocking(findings) {
1407
1478
  return findings.some((f) => f.severity === "block");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.5.1",
3
+ "version": "1.7.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",