tamperward 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/` — 235 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" }
@@ -822,10 +829,15 @@ var coverageLowering = {
822
829
 
823
830
  // src/detectors/ci-tampering.ts
824
831
  var RULE5 = "ci-tampering";
825
- var STEP = /^\s*-?\s*(?:run|uses):/;
832
+ var USES = /^\s*-\s*uses:/;
826
833
  var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
827
834
  var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
828
- var RUNNER = /\b(npm|npx|pnpm|yarn|bun|make|cargo|pytest|python|jest|vitest|eslint|tsc|playwright|tamperward|gradle|mvn|dotnet)\b/;
835
+ var INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward)\b/;
836
+ var INVOKES_SCRIPT = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|tests|lint|typecheck|type-check|coverage)\b|\b(?:make|cargo|go)\s+test\b|\bgradle\w*\s+(?:test|check)\b/;
837
+ var invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
838
+ function commandCore(line) {
839
+ return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
840
+ }
829
841
  var uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
830
842
  function isAlwaysFalse(raw) {
831
843
  const v = uncommented(raw);
@@ -878,16 +890,18 @@ var ciTampering = {
878
890
  );
879
891
  }
880
892
  }
881
- const stillPresent = new Set(addedLines(c).map((l) => l.content.trim()));
893
+ const afterCores = new Set(
894
+ (c.after != null ? c.after.split("\n") : addedLines(c).map((l) => l.content)).map(commandCore)
895
+ );
882
896
  for (const l of removedLines(c)) {
883
- if (stillPresent.has(l.content.trim())) continue;
884
- const isStepLine = STEP.test(l.content) && CHECK.test(l.content);
885
- const isRunBodyLine = !YAML_KEY.test(l.content) && RUNNER.test(l.content) && CHECK.test(l.content);
886
- if (!isStepLine && !isRunBodyLine) continue;
897
+ const isUsesLine = USES.test(l.content) && CHECK.test(l.content);
898
+ const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
899
+ if (!isUsesLine && !isCheckCommand) continue;
900
+ if (afterCores.has(commandCore(l.content))) continue;
887
901
  out.push(
888
902
  makeFinding(RULE5, policy, {
889
903
  file: c.path,
890
- message: isStepLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
904
+ message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
891
905
  evidence: l.content.trim(),
892
906
  remediation: "Restore the step. Removing the check that protects main is itself the tamper."
893
907
  })
@@ -1223,6 +1237,63 @@ var testDeletion = {
1223
1237
  }
1224
1238
  };
1225
1239
 
1240
+ // src/detectors/snapshot-rewrite.ts
1241
+ var RULE8 = "snapshot-rewrite";
1242
+ var RUNNER = /\b(?:jest|vitest|playwright|ava)\b/;
1243
+ var UPDATE_ANY = /--update-?[sS]napshots?\b/;
1244
+ var UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
1245
+ var REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
1246
+ var MUTATE = /(?:^|\s)(?:rm|cp|mv|tee|truncate|dd)(?:\s|$)|sed\s+-i|>{1,2}/;
1247
+ function namesProtectedSnapshot(seg, policy) {
1248
+ for (const t of tokens(seg)) {
1249
+ const path = unquote(t).replace(/^>{1,2}/, "");
1250
+ if (isProtected(path, policy, "snapshots")) return path;
1251
+ }
1252
+ return null;
1253
+ }
1254
+ var snapshotRewrite = {
1255
+ id: RULE8,
1256
+ surface: ["command", "file"],
1257
+ certainty: "mechanical",
1258
+ run(changes, policy) {
1259
+ const out = [];
1260
+ const warn = (input) => out.push(makeFinding(RULE8, policy, { ...input, defaultSeverity: "warn" }));
1261
+ for (const c of changes) {
1262
+ if (c.kind === "command") {
1263
+ for (const seg of segments(c.raw)) {
1264
+ let why = null;
1265
+ if (UPDATE_ANY.test(seg)) why = "the snapshot-update flag rewrites every failing snapshot to match current output";
1266
+ else if (RUNNER.test(seg) && UPDATE_WITH_RUNNER.test(seg)) why = "running the test runner in update mode rewrites failing snapshots to match current output";
1267
+ else if (REGEN_SCRIPT.test(seg)) why = "a regeneration script rewrites the recorded expected output from current output";
1268
+ else {
1269
+ const path = MUTATE.test(seg) ? namesProtectedSnapshot(seg, policy) : null;
1270
+ if (path) why = `the command overwrites or removes the protected snapshot ${path}`;
1271
+ }
1272
+ if (why) {
1273
+ warn({
1274
+ message: `Snapshot rewrite: ${why}.`,
1275
+ evidence: seg,
1276
+ 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."
1277
+ });
1278
+ }
1279
+ }
1280
+ continue;
1281
+ }
1282
+ const wasSnap = c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
1283
+ const isSnap = isProtected(c.path, policy, "snapshots");
1284
+ if (c.op === "add" || !isSnap && !wasSnap) continue;
1285
+ const what = c.op === "delete" ? "deleted" : c.op === "rename" && !isSnap ? `renamed out of the protected snapshot set (to ${c.path})` : "rewritten";
1286
+ warn({
1287
+ file: c.op === "rename" && !isSnap ? c.oldPath : c.path,
1288
+ message: `A recorded expected output was ${what}.`,
1289
+ evidence: c.op === "rename" ? `${c.oldPath} -> ${c.path}` : c.path,
1290
+ 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."
1291
+ });
1292
+ }
1293
+ return out;
1294
+ }
1295
+ };
1296
+
1226
1297
  // src/detectors/index.ts
1227
1298
  var allDetectors = [
1228
1299
  noVerify,
@@ -1232,7 +1303,8 @@ var allDetectors = [
1232
1303
  coverageLowering,
1233
1304
  ciTampering,
1234
1305
  hookTampering,
1235
- testDeletion
1306
+ testDeletion,
1307
+ snapshotRewrite
1236
1308
  ];
1237
1309
 
1238
1310
  // 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.1",
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",