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 +1 -1
- package/dist/cli/index.js +51 -16
- package/package.json +1 -1
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/` —
|
|
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
|
-
|
|
412
|
-
version
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
if (
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
|
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",
|