tamperward 1.2.0 → 1.3.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
@@ -102,7 +102,7 @@ with the same engine it ships.
102
102
  - `src/engine.ts` — runs the enabled rules over `Change[]`; honours `policy.ignore`.
103
103
  - `src/cli/` — `tamperward check --staged | --worktree | --diff <base>...<head>`,
104
104
  exit 1 on any blocking finding.
105
- - `test/` — 226 tests, including the AST-vs-regex, self-hosting precision, and
105
+ - `test/` — 248 tests, including the AST-vs-regex, self-hosting precision, and
106
106
  pre-go-live audit regression cases, and the renderer accessibility contract.
107
107
 
108
108
  - `src/adapters/claude/` + `src/cli/hook.ts` — the agent layer: `tamperward hook claude`
package/dist/cli/index.js CHANGED
@@ -371,6 +371,16 @@ function removedLines(c) {
371
371
  // src/policy.ts
372
372
  import picomatch from "picomatch";
373
373
  var POLICY_FILE = ".tamperward.yml";
374
+ var BLOCK_SINCE = {};
375
+ function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
376
+ const out = { ...rules };
377
+ for (const [rule, since] of Object.entries(gates)) {
378
+ if (since > version && out[rule]?.severity === "block") {
379
+ out[rule] = { ...out[rule], severity: "warn" };
380
+ }
381
+ }
382
+ return out;
383
+ }
374
384
  var cache = /* @__PURE__ */ new Map();
375
385
  function matcher(glob) {
376
386
  let m = cache.get(glob);
@@ -407,9 +417,9 @@ function mergeProtected(base, user) {
407
417
  }
408
418
  return out;
409
419
  }
410
- function defaultPolicy() {
411
- return {
412
- version: 1,
420
+ function defaultPolicy(version = 1) {
421
+ const p = {
422
+ version,
413
423
  protected: {
414
424
  // Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
415
425
  // .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
@@ -454,6 +464,8 @@ function defaultPolicy() {
454
464
  ignore: [],
455
465
  signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
456
466
  };
467
+ p.rules = applyVersionGates(p.rules, version);
468
+ return p;
457
469
  }
458
470
 
459
471
  // src/detectors/files.ts
@@ -829,10 +841,15 @@ var coverageLowering = {
829
841
 
830
842
  // src/detectors/ci-tampering.ts
831
843
  var RULE5 = "ci-tampering";
832
- var STEP = /^\s*-?\s*(?:run|uses):/;
844
+ var USES = /^\s*-\s*uses:/;
833
845
  var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
834
846
  var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
835
- var RUNNER = /\b(npm|npx|pnpm|yarn|bun|make|cargo|pytest|python|jest|vitest|eslint|tsc|playwright|tamperward|gradle|mvn|dotnet)\b/;
847
+ var INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward)\b/;
848
+ 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/;
849
+ var invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
850
+ function commandCore(line) {
851
+ return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
852
+ }
836
853
  var uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
837
854
  function isAlwaysFalse(raw) {
838
855
  const v = uncommented(raw);
@@ -885,16 +902,18 @@ var ciTampering = {
885
902
  );
886
903
  }
887
904
  }
888
- const stillPresent = new Set(addedLines(c).map((l) => l.content.trim()));
905
+ const afterCores = new Set(
906
+ (c.after != null ? c.after.split("\n") : addedLines(c).map((l) => l.content)).map(commandCore)
907
+ );
889
908
  for (const l of removedLines(c)) {
890
- if (stillPresent.has(l.content.trim())) continue;
891
- const isStepLine = STEP.test(l.content) && CHECK.test(l.content);
892
- const isRunBodyLine = !YAML_KEY.test(l.content) && RUNNER.test(l.content) && CHECK.test(l.content);
893
- if (!isStepLine && !isRunBodyLine) continue;
909
+ const isUsesLine = USES.test(l.content) && CHECK.test(l.content);
910
+ const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
911
+ if (!isUsesLine && !isCheckCommand) continue;
912
+ if (afterCores.has(commandCore(l.content))) continue;
894
913
  out.push(
895
914
  makeFinding(RULE5, policy, {
896
915
  file: c.path,
897
- message: isStepLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
916
+ message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
898
917
  evidence: l.content.trim(),
899
918
  remediation: "Restore the step. Removing the check that protects main is itself the tamper."
900
919
  })
@@ -917,8 +936,11 @@ function safeParse(src) {
917
936
  }
918
937
  }
919
938
  function effective(raw) {
920
- const base = defaultPolicy();
939
+ const v = raw.version;
940
+ const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
941
+ const base = defaultPolicy(version);
921
942
  return {
943
+ version,
922
944
  rules: { ...base.rules, ...raw.rules ?? {} },
923
945
  ignore: raw.ignore ?? base.ignore ?? [],
924
946
  protected: mergeProtected(base.protected, raw.protected),
@@ -933,6 +955,11 @@ function policyWeakening(before, after) {
933
955
  const be = effective(b);
934
956
  const ae = effective(a);
935
957
  const reasons = [];
958
+ if (ae.version < be.version) {
959
+ reasons.push(
960
+ `policy version lowered ${be.version} \u2192 ${ae.version} \u2014 un-opts this repo from rule graduations gated above ${ae.version}`
961
+ );
962
+ }
936
963
  for (const name of /* @__PURE__ */ new Set([...Object.keys(be.rules), ...Object.keys(ae.rules)])) {
937
964
  const br = be.rules[name];
938
965
  const ar = ae.rules[name];
@@ -1232,7 +1259,7 @@ var testDeletion = {
1232
1259
 
1233
1260
  // src/detectors/snapshot-rewrite.ts
1234
1261
  var RULE8 = "snapshot-rewrite";
1235
- var RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
1262
+ var RUNNER = /\b(?:jest|vitest|playwright|ava)\b/;
1236
1263
  var UPDATE_ANY = /--update-?[sS]napshots?\b/;
1237
1264
  var UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
1238
1265
  var REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
@@ -1256,7 +1283,7 @@ var snapshotRewrite = {
1256
1283
  for (const seg of segments(c.raw)) {
1257
1284
  let why = null;
1258
1285
  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";
1286
+ 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";
1260
1287
  else if (REGEN_SCRIPT.test(seg)) why = "a regeneration script rewrites the recorded expected output from current output";
1261
1288
  else {
1262
1289
  const path = MUTATE.test(seg) ? namesProtectedSnapshot(seg, policy) : null;
@@ -1342,11 +1369,19 @@ import { join as join2 } from "node:path";
1342
1369
  import { parse as parse2 } from "yaml";
1343
1370
  var PolicyError = class extends Error {
1344
1371
  };
1372
+ function normalizeVersion(v, where = POLICY_FILE) {
1373
+ if (v === void 0 || v === null) return 1;
1374
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
1375
+ throw new PolicyError(`${where}: version must be a positive integer, got ${JSON.stringify(v)}`);
1376
+ }
1377
+ return v;
1378
+ }
1345
1379
  function parsePolicy(raw) {
1346
- const base = defaultPolicy();
1347
1380
  const r = raw ?? {};
1381
+ const version = normalizeVersion(r.version);
1382
+ const base = defaultPolicy(version);
1348
1383
  return {
1349
- version: 1,
1384
+ version,
1350
1385
  // Merge with the baseline, never replace. For an integrity tool, a config that sets
1351
1386
  // one rule's severity must NOT silently drop the other nine — nor may naming one
1352
1387
  // protected glob wipe out the rest of its category (see mergeProtected).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",