tamperward 1.1.0 → 1.2.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
@@ -92,14 +92,17 @@ with the same engine it ships.
92
92
  per-line old/new line numbers correct across multiple hunks.
93
93
  - `src/git/build.ts` — the git adapter: range / staged / worktree views, enriching
94
94
  `before`/`after` with full file content for the AST detectors.
95
- - `src/detectors/` — the **eight mechanical rules**: `no-verify`, `ts-any-cast`,
95
+ - `src/detectors/` — the **nine mechanical rules**: `no-verify`, `ts-any-cast`,
96
96
  `lint-suppression`, `test-skip`, `coverage-lowering`, `ci-tampering`,
97
97
  `hook-tampering`, `test-deletion` (the last counts `it()/test()` via the TS AST,
98
- and handles delete / rename-out-of-glob / shell mutation).
98
+ and handles delete / rename-out-of-glob / shell mutation), and `snapshot-rewrite`
99
+ (a `warn`: re-recording a snapshot/golden expectation from current output — the one
100
+ rule built from measured demand, after the affordance experiment put the move at a
101
+ 70% attempt and 100% through rate; see `harness/PREDICTION-affordance.md`).
99
102
  - `src/engine.ts` — runs the enabled rules over `Change[]`; honours `policy.ignore`.
100
103
  - `src/cli/` — `tamperward check --staged | --worktree | --diff <base>...<head>`,
101
104
  exit 1 on any blocking finding.
102
- - `test/` — 211 tests, including the AST-vs-regex, self-hosting precision, and
105
+ - `test/` — 226 tests, including the AST-vs-regex, self-hosting precision, and
103
106
  pre-go-live audit regression cases, and the renderer accessibility contract.
104
107
 
105
108
  - `src/adapters/claude/` + `src/cli/hook.ts` — the agent layer: `tamperward hook claude`
package/dist/cli/index.js CHANGED
@@ -425,6 +425,10 @@ function defaultPolicy() {
425
425
  "**/package.json"
426
426
  ],
427
427
  ci: [".github/workflows/**"],
428
+ // Recorded expected outputs. An assertion stored as data is still an assertion;
429
+ // rewriting it from current output is the snapshot-update move the affordance
430
+ // experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
431
+ snapshots: ["**/*.snap", "**/__snapshots__/**", "**/golden/**", "**/*.golden.*"],
428
432
  hooks: [".husky/**", "**/lefthook.*", ".tamperward.yml", "**/.tamperward.yml"]
429
433
  },
430
434
  rules: {
@@ -440,6 +444,9 @@ function defaultPolicy() {
440
444
  "ci-tampering": { severity: "block" },
441
445
  "hook-tampering": { severity: "block" },
442
446
  "no-verify": { severity: "block" },
447
+ // mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
448
+ // when intended output changes. WARN until the §7 evidence path earns block.
449
+ "snapshot-rewrite": { severity: "warn" },
443
450
  // heuristic — warn until precision is measured (SPEC §7)
444
451
  "assertion-weakening": { severity: "warn" },
445
452
  "guard-removal": { severity: "warn" }
@@ -1223,6 +1230,63 @@ var testDeletion = {
1223
1230
  }
1224
1231
  };
1225
1232
 
1233
+ // src/detectors/snapshot-rewrite.ts
1234
+ var RULE8 = "snapshot-rewrite";
1235
+ var RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
1236
+ var UPDATE_ANY = /--update-?[sS]napshots?\b/;
1237
+ var UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
1238
+ var REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
1239
+ var MUTATE = /(?:^|\s)(?:rm|cp|mv|tee|truncate|dd)(?:\s|$)|sed\s+-i|>{1,2}/;
1240
+ function namesProtectedSnapshot(seg, policy) {
1241
+ for (const t of tokens(seg)) {
1242
+ const path = unquote(t).replace(/^>{1,2}/, "");
1243
+ if (isProtected(path, policy, "snapshots")) return path;
1244
+ }
1245
+ return null;
1246
+ }
1247
+ var snapshotRewrite = {
1248
+ id: RULE8,
1249
+ surface: ["command", "file"],
1250
+ certainty: "mechanical",
1251
+ run(changes, policy) {
1252
+ const out = [];
1253
+ const warn = (input) => out.push(makeFinding(RULE8, policy, { ...input, defaultSeverity: "warn" }));
1254
+ for (const c of changes) {
1255
+ if (c.kind === "command") {
1256
+ for (const seg of segments(c.raw)) {
1257
+ let why = null;
1258
+ if (UPDATE_ANY.test(seg)) why = "the snapshot-update flag rewrites every failing snapshot to match current output";
1259
+ else if (RUNNER2.test(seg) && UPDATE_WITH_RUNNER.test(seg)) why = "running the test runner in update mode rewrites failing snapshots to match current output";
1260
+ else if (REGEN_SCRIPT.test(seg)) why = "a regeneration script rewrites the recorded expected output from current output";
1261
+ else {
1262
+ const path = MUTATE.test(seg) ? namesProtectedSnapshot(seg, policy) : null;
1263
+ if (path) why = `the command overwrites or removes the protected snapshot ${path}`;
1264
+ }
1265
+ if (why) {
1266
+ warn({
1267
+ message: `Snapshot rewrite: ${why}.`,
1268
+ evidence: seg,
1269
+ remediation: "A snapshot is an assertion. If the test failure is a bug, fix the code; only update the snapshot when the INTENDED output changed, and have a human confirm the new expectation is correct."
1270
+ });
1271
+ }
1272
+ }
1273
+ continue;
1274
+ }
1275
+ const wasSnap = c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
1276
+ const isSnap = isProtected(c.path, policy, "snapshots");
1277
+ if (c.op === "add" || !isSnap && !wasSnap) continue;
1278
+ const what = c.op === "delete" ? "deleted" : c.op === "rename" && !isSnap ? `renamed out of the protected snapshot set (to ${c.path})` : "rewritten";
1279
+ warn({
1280
+ file: c.op === "rename" && !isSnap ? c.oldPath : c.path,
1281
+ message: `A recorded expected output was ${what}.`,
1282
+ evidence: c.op === "rename" ? `${c.oldPath} -> ${c.path}` : c.path,
1283
+ 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."
1284
+ });
1285
+ }
1286
+ return out;
1287
+ }
1288
+ };
1289
+
1226
1290
  // src/detectors/index.ts
1227
1291
  var allDetectors = [
1228
1292
  noVerify,
@@ -1232,7 +1296,8 @@ var allDetectors = [
1232
1296
  coverageLowering,
1233
1297
  ciTampering,
1234
1298
  hookTampering,
1235
- testDeletion
1299
+ testDeletion,
1300
+ snapshotRewrite
1236
1301
  ];
1237
1302
 
1238
1303
  // src/engine.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",