tamperward 2.23.20 → 2.24.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
@@ -626,11 +626,11 @@ option can never be reinterpreted as the agent command.
626
626
 
627
627
  ### The rules
628
628
 
629
- Eighteen rules are specified and seventeen ship (see the table in
629
+ Nineteen rules are specified and eighteen ship (see the table in
630
630
  [SPEC.md](./SPEC.md)). The families: test protection (`test-deletion`,
631
631
  `test-skip`, `test-content-removal`, plus the warning-only JS/TS
632
632
  `assertion-weakening` heuristic), verification-signal protection
633
- (`coverage-lowering`, `snapshot-rewrite`, `snapshot-only-rewrite`), suppression
633
+ (`coverage-lowering`, `coverage-exclusion`, `snapshot-rewrite`, `snapshot-only-rewrite`), suppression
634
634
  (`ts-any-cast`, `ts-any-launder`, `ts-cast-growth`, `lint-suppression`), pipeline protection
635
635
  (`ci-tampering`, `hook-tampering`, `no-verify`), and the effect/outcome layers
636
636
  (`transient-protected-mutation`, `pristine-verification`, plus the `run` envelope).
@@ -653,6 +653,13 @@ change removed, reported as a warning that never blocks by default. It fires on
653
653
  of legitimate mainline pairs across four real TypeScript libraries
654
654
  (`harness/fp-study/CAST-GROWTH-CORPUS.md`), so block is closed by corpus; the same
655
655
  pass removed every assertion from TamperWard's own `src/` (`docs/CAST-INVENTORY.md`).
656
+ `coverage-exclusion` (2.24.0) is the per-function form of `coverage-lowering`: an
657
+ inline `istanbul ignore` / `c8 ignore` / `v8 ignore` / `node:coverage ignore` /
658
+ `# pragma: no cover` / `#[coverage(off)]` added to non-test source, or a `//go:build`
659
+ constraint added to an existing Go file. It fired on 0 of the same 460 mainline
660
+ pairs, but every real add in the deeper histories is an honest unreachable-branch
661
+ marker (`harness/fp-study/COVERAGE-EXCLUSION-CORPUS.md`), so it warns and never
662
+ blocks by default.
656
663
 
657
664
  ## What Tamperward does not do
658
665
 
package/dist/cli/index.js CHANGED
@@ -571,6 +571,12 @@ function defaultPolicy(version = 1) {
571
571
  "ts-cast-growth": { severity: "warn" },
572
572
  "lint-suppression": { severity: "block" },
573
573
  "coverage-lowering": { severity: "block" },
574
+ // the per-function form of coverage-lowering: an inline `istanbul ignore` /
575
+ // `c8 ignore` / `v8 ignore` / `node:coverage ignore` / `# pragma: no cover` /
576
+ // `#[coverage(off)]` added to non-test source (#438). Also the ordinary way to
577
+ // mark a genuinely unreachable branch, so it WARNs until the corpus in
578
+ // harness/fp-study/coverage-exclusion-corpus.json earns block (SPEC §7).
579
+ "coverage-exclusion": { severity: "warn" },
574
580
  "ci-tampering": { severity: "block" },
575
581
  "hook-tampering": { severity: "block" },
576
582
  "no-verify": { severity: "block" },
@@ -4590,7 +4596,7 @@ function literals(e) {
4590
4596
  return { items, opaque };
4591
4597
  }
4592
4598
  function parseLists(src) {
4593
- const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
4599
+ const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], ignoreRe: [], opaqueCollect: false, opaqueIgnore: false };
4594
4600
  try {
4595
4601
  const sf = sourceOf(src);
4596
4602
  if (!sf) return res;
@@ -4606,7 +4612,7 @@ function parseLists(src) {
4606
4612
  if (opaque) res.opaqueCollect = true;
4607
4613
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
4608
4614
  const { items, opaque } = literals(resolve16(node.initializer));
4609
- res.ignore.push(...items);
4615
+ (k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
4610
4616
  if (opaque) res.opaqueIgnore = true;
4611
4617
  }
4612
4618
  }
@@ -4628,6 +4634,8 @@ function parseSwitches(src) {
4628
4634
  const k = keyName(node.name);
4629
4635
  const owner = ownerKey(node);
4630
4636
  if (k === "enabled" && owner === "coverage") res.enabled = bool2(node.initializer);
4637
+ else if (k === "all" && owner === "coverage") res.all = bool2(node.initializer);
4638
+ else if (k === "collectCoverage" && (owner === null || owner === "jest")) res.collectCoverage = bool2(node.initializer);
4631
4639
  else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool2(node.initializer);
4632
4640
  }
4633
4641
  ts.forEachChild(node, visit);
@@ -4736,13 +4744,19 @@ function narrowings(before, after, sources) {
4736
4744
  }
4737
4745
  const had = new Set(before.ignore);
4738
4746
  for (const g of after.ignore) {
4739
- if (!had.has(g) && !BENIGN_EXEMPTION.test(g)) out3.push(`coverage now exempts ${JSON.stringify(g)} (coveragePathIgnorePatterns / coverage.exclude)`);
4747
+ if (!had.has(g) && !BENIGN_EXEMPTION.test(g)) out3.push(`coverage now exempts ${JSON.stringify(g)} (coverage.exclude)`);
4748
+ }
4749
+ const hadRe = new Set(before.ignoreRe);
4750
+ for (const g of after.ignoreRe) {
4751
+ if (!hadRe.has(g) && !BENIGN_EXEMPTION_RE.test(g)) out3.push(`coverage now exempts ${JSON.stringify(g)} (coveragePathIgnorePatterns)`);
4740
4752
  }
4741
4753
  return out3;
4742
4754
  }
4743
4755
  function switchWeakenings(before, after) {
4744
4756
  const out3 = [];
4745
4757
  if (before.enabled === true && after.enabled === false) out3.push("coverage.enabled switched off \u2014 the thresholds are never checked");
4758
+ if (before.collectCoverage === true && after.collectCoverage === false) out3.push("collectCoverage switched off \u2014 coverage is no longer collected, so the thresholds are never checked");
4759
+ if (before.all === true && after.all === false) out3.push("coverage.all switched off \u2014 a file no test imports is no longer measured");
4746
4760
  if (before.perFile === true && after.perFile !== true) out3.push("thresholds.perFile removed \u2014 a single uncovered file now hides behind the aggregate");
4747
4761
  return out3;
4748
4762
  }
@@ -4897,7 +4911,49 @@ function gateMovedElsewhere(c, changes, policy) {
4897
4911
  });
4898
4912
  });
4899
4913
  }
4900
- var RULE5, METRICS, isMetric, norm, BENIGN_EXEMPTION, globs, globList, CANONICAL_SOURCES, NON_SOURCE, NYC_KEYS, COVERAGE_PY, CODECOV, SIMPLE_FILES, lessThan, numOf, SWITCHES, isJsConfig, coverageLowering;
4914
+ function flagWeakenings(c, added, removed, policy) {
4915
+ const out3 = [];
4916
+ const removedCheck = removed.find((l) => CHECK_COVERAGE.test(l.content));
4917
+ if (removedCheck && !added.some((l) => CHECK_COVERAGE.test(l.content))) {
4918
+ out3.push(
4919
+ makeFinding(RULE5, policy, {
4920
+ file: c.path,
4921
+ message: "nyc --check-coverage was removed \u2014 the coverage thresholds are no longer enforced.",
4922
+ evidence: removedCheck.content.trim(),
4923
+ remediation: "Keep --check-coverage on; raise real coverage instead of switching the gate off."
4924
+ })
4925
+ );
4926
+ }
4927
+ const was = floorsOf(removed);
4928
+ const now = floorsOf(added);
4929
+ if (was.length === 0) return out3;
4930
+ const high = was.reduce((a, b) => b.v > a.v ? b : a);
4931
+ if (now.length === 0) {
4932
+ out3.push(
4933
+ makeFinding(RULE5, policy, {
4934
+ file: c.path,
4935
+ message: `--cov-fail-under removed (was ${high.v}) \u2014 the coverage floor is no longer enforced.`,
4936
+ evidence: high.l.content.trim(),
4937
+ remediation: "Keep the --cov-fail-under floor; raise real coverage instead of removing the gate."
4938
+ })
4939
+ );
4940
+ return out3;
4941
+ }
4942
+ const low = now.reduce((a, b) => b.v < a.v ? b : a);
4943
+ if (low.v < high.v) {
4944
+ out3.push(
4945
+ makeFinding(RULE5, policy, {
4946
+ file: c.path,
4947
+ line: low.l.newLine ?? void 0,
4948
+ message: `--cov-fail-under lowered ${high.v} \u2192 ${low.v}.`,
4949
+ evidence: low.l.content.trim(),
4950
+ remediation: "Restore the coverage floor and raise real coverage; do not lower the gate to pass."
4951
+ })
4952
+ );
4953
+ }
4954
+ return out3;
4955
+ }
4956
+ var RULE5, METRICS, isMetric, norm, BENIGN_DIRS, BENIGN_EXEMPTION, BENIGN_EXEMPTION_RE, globs, globList, CANONICAL_SOURCES, NON_SOURCE, NYC_KEYS, COVERAGE_PY, CODECOV, SIMPLE_FILES, lessThan, numOf, SWITCHES, isJsConfig, COV_FAIL_UNDER, CHECK_COVERAGE, floorsOf, coverageLowering;
4901
4957
  var init_coverage_lowering = __esm({
4902
4958
  "src/detectors/coverage-lowering.ts"() {
4903
4959
  "use strict";
@@ -4912,7 +4968,9 @@ var init_coverage_lowering = __esm({
4912
4968
  METRICS = ["branches", "functions", "lines", "statements"];
4913
4969
  isMetric = (k) => METRICS.some((m) => m === k);
4914
4970
  norm = (p) => p.replace(/^\.\//, "");
4915
- BENIGN_EXEMPTION = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings|generated|__generated__|codegen|migrations?)(?:\/|$|\*)|\.(?:test|spec|stories|d|gen|generated|pb|e2e-spec)\.|\.(?:config|setup)\.|(?:^|[/!*])(?:manage|wsgi|asgi|__init__)\.py$|(?:^|[/!*])main\.[jt]sx?$|\.git\b|\.cache|\.next|\.nuxt|\.output|\.idea|\.d\.ts|\.json$|\.md$|\.css$|\.s[ac]ss$/i;
4971
+ BENIGN_DIRS = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings|generated|__generated__|codegen|migrations?)(?:\/|$|\*)|\.(?:config|setup)\.|(?:^|[/!*])(?:manage|wsgi|asgi|__init__)\.py$|(?:^|[/!*])main\.[jt]sx?$|\.git\b|\.cache|\.next|\.nuxt|\.output|\.idea/i;
4972
+ BENIGN_EXEMPTION = new RegExp(`${BENIGN_DIRS.source}|\\.(?:test|spec|stories|d|gen|generated|pb|e2e-spec)\\.|\\.d\\.ts|\\.json$|\\.md$|\\.css$|\\.s[ac]ss$`, "i");
4973
+ BENIGN_EXEMPTION_RE = new RegExp(`${BENIGN_DIRS.source}|\\\\\\.(?:test|spec|stories|gen|generated|pb|e2e-spec)\\\\\\.|\\\\\\.(?:d\\\\\\.ts|json|md|css|s[ac]ss)\\$?$`, "i");
4916
4974
  globs = (patterns) => {
4917
4975
  const ms = patterns.map((g) => {
4918
4976
  try {
@@ -4935,12 +4993,12 @@ var init_coverage_lowering = __esm({
4935
4993
  label: m,
4936
4994
  weakens: "lower"
4937
4995
  }));
4938
- COVERAGE_PY = /(?:^|\/)(?:\.coveragerc|pyproject\.toml)$/;
4996
+ COVERAGE_PY = /(?:^|\/)(?:\.coveragerc|pyproject\.toml|setup\.cfg|tox\.ini)$/;
4939
4997
  CODECOV = /(?:^|\/)\.?codecov\.yml$/;
4940
4998
  SIMPLE_FILES = [
4941
4999
  {
4942
- // coverage.py: `fail_under = 90` under [report] (.coveragerc, setup.cfg-style) or
4943
- // [tool.coverage.report] (pyproject.toml)
5000
+ // coverage.py: `fail_under = 90` under [report] (.coveragerc), [coverage:report]
5001
+ // (setup.cfg, tox.ini) or [tool.coverage.report] (pyproject.toml)
4944
5002
  file: COVERAGE_PY,
4945
5003
  keys: [{ key: /^\s*fail[_-]under\s*=\s*"?(\d+(?:\.\d+)?)/, label: "fail_under", weakens: "lower" }],
4946
5004
  removable: true
@@ -4999,6 +5057,12 @@ var init_coverage_lowering = __esm({
4999
5057
  }
5000
5058
  ];
5001
5059
  isJsConfig = (path) => /\.(?:[cm]?[jt]sx?|json)$/.test(path);
5060
+ COV_FAIL_UNDER = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
5061
+ CHECK_COVERAGE = /--check-coverage\b/;
5062
+ floorsOf = (lines) => lines.flatMap((l) => {
5063
+ const m = l.content.match(COV_FAIL_UNDER);
5064
+ return m ? [{ v: Number(m[1]), l }] : [];
5065
+ });
5002
5066
  coverageLowering = {
5003
5067
  id: RULE5,
5004
5068
  surface: ["file"],
@@ -5007,7 +5071,10 @@ var init_coverage_lowering = __esm({
5007
5071
  const out3 = [];
5008
5072
  for (const c of changes) {
5009
5073
  if (c.kind !== "file") continue;
5010
- if (!isProtected(c.path, policy, "config")) continue;
5074
+ if (!isProtected(c.path, policy, "config")) {
5075
+ if (isProtected(c.path, policy, "ci")) out3.push(...flagWeakenings(c, addedLines(c), removedLines(c), policy));
5076
+ continue;
5077
+ }
5011
5078
  if (c.after != null && c.op !== "delete") {
5012
5079
  const reasons = [];
5013
5080
  if (isJsConfig(c.path)) {
@@ -5057,6 +5124,7 @@ var init_coverage_lowering = __esm({
5057
5124
  })
5058
5125
  );
5059
5126
  }
5127
+ out3.push(...flagWeakenings(c, added, removed, policy));
5060
5128
  for (const l of added) {
5061
5129
  for (const s of SWITCHES) {
5062
5130
  if (!s.re.test(l.content)) continue;
@@ -5110,6 +5178,93 @@ var init_coverage_lowering = __esm({
5110
5178
  }
5111
5179
  });
5112
5180
 
5181
+ // src/detectors/coverage-exclusion.ts
5182
+ function isText(line, idx, lang) {
5183
+ const opener = LINE_COMMENT[lang];
5184
+ let quote = null;
5185
+ for (let j = 0; j < idx; j++) {
5186
+ const ch = line[j];
5187
+ if (quote) {
5188
+ if (ch === "\\") j++;
5189
+ else if (ch === quote) quote = null;
5190
+ continue;
5191
+ }
5192
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
5193
+ else if (opener && opener.test(line.slice(j, j + 2))) return true;
5194
+ }
5195
+ return quote !== null;
5196
+ }
5197
+ var RULE6, PATTERNS3, OUT_OF_SCOPE_PATH, LINE_COMMENT, coverageExclusion;
5198
+ var init_coverage_exclusion = __esm({
5199
+ "src/detectors/coverage-exclusion.ts"() {
5200
+ "use strict";
5201
+ init_select();
5202
+ init_files();
5203
+ init_policy();
5204
+ init_finding();
5205
+ RULE6 = "coverage-exclusion";
5206
+ PATTERNS3 = {
5207
+ js: [
5208
+ { re: /\/\*\s*istanbul\s+ignore\b/, why: "`istanbul ignore` takes the next statement, branch or the whole file out of the coverage measurement" },
5209
+ { re: /\/\*\s*c8\s+ignore\b(?!\s*stop\b)/, why: "`c8 ignore` takes the next statement or the marked range out of the coverage measurement" },
5210
+ { re: /\/\*\s*v8\s+ignore\b(?!\s*stop\b)/, why: "`v8 ignore` takes the next statement or the marked range out of the coverage measurement" },
5211
+ { re: /\/\*\s*node:coverage\s+(?:ignore|disable)\b/, why: "`node:coverage ignore/disable` takes the following code out of the coverage measurement" }
5212
+ ],
5213
+ py: [{ re: /#\s*pragma[:\s]\s*no\s*cover\b/i, why: "`# pragma: no cover` takes the line or block out of the coverage measurement" }],
5214
+ rs: [{ re: /#\s*!?\[[^\]]*\bcoverage\s*\(\s*off\s*\)/, why: "`#[coverage(off)]` takes the item out of the coverage measurement" }],
5215
+ go: [
5216
+ {
5217
+ re: /^\s*\/\/\s*(?:go:build\b|\+build\b)/,
5218
+ why: "a build constraint added to an existing file takes it out of the default build, and out of `go test -cover`",
5219
+ existingOnly: true
5220
+ }
5221
+ ]
5222
+ };
5223
+ OUT_OF_SCOPE_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|out|coverage|generated|__generated__|codegen|examples?|docs?|scripts?|fixtures?|__fixtures__|__mocks__)\/|\.d\.[cm]?ts$|\.(?:generated|gen|pb)\.[^/]*$/;
5224
+ LINE_COMMENT = { js: /^\/\//, go: /^\/\//, rs: /^\/\//, py: /^#/ };
5225
+ coverageExclusion = {
5226
+ id: RULE6,
5227
+ surface: ["file"],
5228
+ certainty: "mechanical",
5229
+ run(changes, policy) {
5230
+ const out3 = [];
5231
+ for (const c of changes) {
5232
+ if (c.kind !== "file" || c.op === "delete") continue;
5233
+ const lang = langOf(c.path);
5234
+ if (!lang) continue;
5235
+ const patterns = PATTERNS3[lang];
5236
+ if (!patterns) continue;
5237
+ const category = protectedCategory(c.path, policy);
5238
+ if (OUT_OF_SCOPE_PATH.test(c.path) || category === "tests" || category === "config") continue;
5239
+ const removed = new Set(removedLines(c).map((l) => l.content.trim()));
5240
+ const before = c.before ?? "";
5241
+ for (const l of addedLines(c)) {
5242
+ const trimmed = l.content.trim();
5243
+ if (removed.has(trimmed)) continue;
5244
+ for (const p of patterns) {
5245
+ const m = p.re.exec(l.content);
5246
+ if (!m || isText(l.content, m.index, lang)) continue;
5247
+ if (p.existingOnly && (c.op === "add" || c.before == null || before.split("\n").some((b) => p.re.test(b)))) continue;
5248
+ out3.push(
5249
+ makeFinding(RULE6, policy, {
5250
+ file: c.path,
5251
+ line: l.newLine ?? void 0,
5252
+ message: `Coverage exclusion added: ${p.why}.`,
5253
+ evidence: trimmed,
5254
+ remediation: "Cover the code with a test instead of excluding it from the measurement; if it is genuinely unreachable, say so in review.",
5255
+ defaultSeverity: "warn"
5256
+ })
5257
+ );
5258
+ break;
5259
+ }
5260
+ }
5261
+ }
5262
+ return out3;
5263
+ }
5264
+ };
5265
+ }
5266
+ });
5267
+
5113
5268
  // src/detectors/gh-expression.ts
5114
5269
  function foldExpressions(s) {
5115
5270
  return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
@@ -5878,7 +6033,7 @@ function ancestorOf(lines, i) {
5878
6033
  while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
5879
6034
  return h >= 0 ? h : null;
5880
6035
  }
5881
- var RULE6, USES, CHECK, YAML_KEY, invokesCheck2, uncommented, SHELL_ERREXIT, indentOf, STEP_START, REPORTER_ACTION, usesCheck, stepHasCheck, jobHasCheck, NEUTRALISERS, LIST_KEYS, DEFAULT_BRANCH, ALL, CODE_EVENTS, PR_CODE_TYPES, pathsMatchSource, ignoresEverySource, CODE_PROBES, REUSED_WORKFLOW, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
6036
+ var RULE7, USES, CHECK, YAML_KEY, invokesCheck2, uncommented, SHELL_ERREXIT, indentOf, STEP_START, REPORTER_ACTION, usesCheck, stepHasCheck, jobHasCheck, NEUTRALISERS, LIST_KEYS, DEFAULT_BRANCH, ALL, CODE_EVENTS, PR_CODE_TYPES, pathsMatchSource, ignoresEverySource, CODE_PROBES, REUSED_WORKFLOW, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
5882
6037
  var init_ci_tampering = __esm({
5883
6038
  "src/detectors/ci-tampering.ts"() {
5884
6039
  "use strict";
@@ -5891,7 +6046,7 @@ var init_ci_tampering = __esm({
5891
6046
  init_gh_expression();
5892
6047
  init_invocation();
5893
6048
  init_invocation();
5894
- RULE6 = "ci-tampering";
6049
+ RULE7 = "ci-tampering";
5895
6050
  USES = /^\s*-\s*uses:/;
5896
6051
  CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
5897
6052
  YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
@@ -5931,7 +6086,7 @@ var init_ci_tampering = __esm({
5931
6086
  REF_LINE = /^\s*ref:\s*(.+?)\s*$/;
5932
6087
  CHECKOUT = /^\s*-?\s*uses:\s*actions\/checkout(?:@|\s|$)/;
5933
6088
  ciTampering = {
5934
- id: RULE6,
6089
+ id: RULE7,
5935
6090
  surface: ["file"],
5936
6091
  certainty: "mechanical",
5937
6092
  run(changes, policy, _view, ctx) {
@@ -5943,7 +6098,7 @@ var init_ci_tampering = __esm({
5943
6098
  if (c.kind !== "file") continue;
5944
6099
  if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
5945
6100
  out3.push(
5946
- makeFinding(RULE6, policy, {
6101
+ makeFinding(RULE7, policy, {
5947
6102
  file: c.path,
5948
6103
  message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
5949
6104
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -5967,7 +6122,7 @@ var init_ci_tampering = __esm({
5967
6122
  }
5968
6123
  if (coe && isTruthy(coe[1])) {
5969
6124
  out3.push(
5970
- makeFinding(RULE6, policy, {
6125
+ makeFinding(RULE7, policy, {
5971
6126
  file: c.path,
5972
6127
  line: l.newLine ?? void 0,
5973
6128
  message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
@@ -5977,7 +6132,7 @@ var init_ci_tampering = __esm({
5977
6132
  );
5978
6133
  } else if (cond && isAlwaysFalse(cond[1])) {
5979
6134
  out3.push(
5980
- makeFinding(RULE6, policy, {
6135
+ makeFinding(RULE7, policy, {
5981
6136
  file: c.path,
5982
6137
  line: l.newLine ?? void 0,
5983
6138
  message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
@@ -5997,7 +6152,7 @@ var init_ci_tampering = __esm({
5997
6152
  const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
5998
6153
  if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
5999
6154
  out3.push(
6000
- makeFinding(RULE6, policy, {
6155
+ makeFinding(RULE7, policy, {
6001
6156
  file: c.path,
6002
6157
  line: idx + 1,
6003
6158
  message: `A check's run block was neutralised: ${n.what}.`,
@@ -6013,7 +6168,7 @@ var init_ci_tampering = __esm({
6013
6168
  const step = stepOf(afterLines, l.newLine - 1);
6014
6169
  if (step && !stepHasCheck(afterLines, step)) continue;
6015
6170
  out3.push(
6016
- makeFinding(RULE6, policy, {
6171
+ makeFinding(RULE7, policy, {
6017
6172
  file: c.path,
6018
6173
  line: l.newLine,
6019
6174
  message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
@@ -6039,7 +6194,7 @@ var init_ci_tampering = __esm({
6039
6194
  if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
6040
6195
  }
6041
6196
  out3.push(
6042
- makeFinding(RULE6, policy, {
6197
+ makeFinding(RULE7, policy, {
6043
6198
  file: c.path,
6044
6199
  line: l.newLine,
6045
6200
  message: `A check was pointed at ${dir} by working-directory \u2014 a directory holding no code or package the check could run.`,
@@ -6056,7 +6211,7 @@ var init_ci_tampering = __esm({
6056
6211
  const target = literalValue(ref[1]);
6057
6212
  if (target === null) continue;
6058
6213
  out3.push(
6059
- makeFinding(RULE6, policy, {
6214
+ makeFinding(RULE7, policy, {
6060
6215
  file: c.path,
6061
6216
  line: l.newLine,
6062
6217
  message: `actions/checkout was pinned to ref: ${target} \u2014 the checks run against that ref, not against the change under review.`,
@@ -6069,7 +6224,7 @@ var init_ci_tampering = __esm({
6069
6224
  triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
6070
6225
  for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
6071
6226
  out3.push(
6072
- makeFinding(RULE6, policy, {
6227
+ makeFinding(RULE7, policy, {
6073
6228
  file: c.path,
6074
6229
  message: `The workflow's triggers were narrowed: ${reason}.`,
6075
6230
  evidence: reason,
@@ -6099,7 +6254,7 @@ var init_ci_tampering = __esm({
6099
6254
  if (s.state === "kept") continue;
6100
6255
  if (s.state === "neutralised") {
6101
6256
  out3.push(
6102
- makeFinding(RULE6, policy, {
6257
+ makeFinding(RULE7, policy, {
6103
6258
  file: c.path,
6104
6259
  message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
6105
6260
  evidence: `${l.content.trim()} \u2192 ${s.by}`,
@@ -6109,7 +6264,7 @@ var init_ci_tampering = __esm({
6109
6264
  continue;
6110
6265
  }
6111
6266
  out3.push(
6112
- makeFinding(RULE6, policy, {
6267
+ makeFinding(RULE7, policy, {
6113
6268
  file: c.path,
6114
6269
  message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
6115
6270
  evidence: l.content.trim(),
@@ -7139,7 +7294,7 @@ function claudeSettingsFindings(c, policy) {
7139
7294
  if (c.after == null) return null;
7140
7295
  const after = parseSettings(c.after);
7141
7296
  const before = c.before != null ? parseSettings(c.before) : null;
7142
- const finding = (reason) => makeFinding(RULE7, policy, {
7297
+ const finding = (reason) => makeFinding(RULE8, policy, {
7143
7298
  file: c.path,
7144
7299
  message: `The Claude Code hook wiring was weakened: ${reason}.`,
7145
7300
  evidence: reason,
@@ -7164,7 +7319,7 @@ function policyFindings(c, policy, ctx) {
7164
7319
  const reasons = c.before == null ? policyAddWeakening(c.after, probe) : policyWeakening(c.before, c.after, probe);
7165
7320
  if (reasons === null) return null;
7166
7321
  return reasons.map(
7167
- (reason) => makeFinding(RULE7, policy, {
7322
+ (reason) => makeFinding(RULE8, policy, {
7168
7323
  file: c.path,
7169
7324
  message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
7170
7325
  evidence: reason,
@@ -7227,7 +7382,7 @@ function configFindings(c, changes, policy, ctx) {
7227
7382
  reasons = after === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : isLefthook(c.path) ? lefthookWeakening(before, after) : preCommitWeakening(before, after);
7228
7383
  }
7229
7384
  return reasons.map(
7230
- (reason) => makeFinding(RULE7, policy, {
7385
+ (reason) => makeFinding(RULE8, policy, {
7231
7386
  file: c.path,
7232
7387
  message: `The hook configuration was weakened: ${reason}.`,
7233
7388
  evidence: reason,
@@ -7278,7 +7433,7 @@ function huskyRuntimeWrite(path, content, ctx) {
7278
7433
  (i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
7279
7434
  );
7280
7435
  }
7281
- var RULE7, ownerExec2, EVENTS, isObj, declares, GATE_KEYS, HOOK_TYPES, SIBLING_KEYS, MENTIONS_GATE, FORGERY, ENV_MECHANISM, envMechanism, mentionsGate, carriesGate, HUSKY_SHIM, HUSKY_SHIM_NAMES, HUSKY_H_KNOWN, HUSKY_DEPRECATION, stripTrailingNl, isHuskyScript, isHookNote, hookTampering;
7436
+ var RULE8, ownerExec2, EVENTS, isObj, declares, GATE_KEYS, HOOK_TYPES, SIBLING_KEYS, MENTIONS_GATE, FORGERY, ENV_MECHANISM, envMechanism, mentionsGate, carriesGate, HUSKY_SHIM, HUSKY_SHIM_NAMES, HUSKY_H_KNOWN, HUSKY_DEPRECATION, stripTrailingNl, isHuskyScript, isHookNote, hookTampering;
7282
7437
  var init_hook_tampering = __esm({
7283
7438
  "src/detectors/hook-tampering.ts"() {
7284
7439
  "use strict";
@@ -7291,7 +7446,7 @@ var init_hook_tampering = __esm({
7291
7446
  init_command();
7292
7447
  init_repo();
7293
7448
  init_hook_wiring();
7294
- RULE7 = "hook-tampering";
7449
+ RULE8 = "hook-tampering";
7295
7450
  ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
7296
7451
  EVENTS = ["PreToolUse", "Stop"];
7297
7452
  isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -7355,7 +7510,7 @@ They WILL FAIL in v10.0.0
7355
7510
  isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
7356
7511
  isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
7357
7512
  hookTampering = {
7358
- id: RULE7,
7513
+ id: RULE8,
7359
7514
  surface: ["file", "command"],
7360
7515
  certainty: "mechanical",
7361
7516
  run(changes, policy, _view, ctx) {
@@ -7368,7 +7523,7 @@ They WILL FAIL in v10.0.0
7368
7523
  if (isPackageJson(c.path) && c.before != null && c.after != null) {
7369
7524
  for (const reason of packageJsonWeakening(c.before, c.after)) {
7370
7525
  out3.push(
7371
- makeFinding(RULE7, policy, {
7526
+ makeFinding(RULE8, policy, {
7372
7527
  file: c.path,
7373
7528
  message: `The hook installation was weakened: ${reason}.`,
7374
7529
  evidence: reason,
@@ -7381,7 +7536,7 @@ They WILL FAIL in v10.0.0
7381
7536
  }
7382
7537
  if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
7383
7538
  out3.push(
7384
- makeFinding(RULE7, policy, {
7539
+ makeFinding(RULE8, policy, {
7385
7540
  file: c.path,
7386
7541
  message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
7387
7542
  evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
@@ -7391,7 +7546,7 @@ They WILL FAIL in v10.0.0
7391
7546
  }
7392
7547
  if (c.op === "delete") {
7393
7548
  out3.push(
7394
- makeFinding(RULE7, policy, {
7549
+ makeFinding(RULE8, policy, {
7395
7550
  file: c.path,
7396
7551
  message: "A protected hook/policy file was deleted.",
7397
7552
  evidence: c.path,
@@ -7400,7 +7555,7 @@ They WILL FAIL in v10.0.0
7400
7555
  );
7401
7556
  } else if (c.op === "rename" && !renameKept(c)) {
7402
7557
  out3.push(
7403
- makeFinding(RULE7, policy, {
7558
+ makeFinding(RULE8, policy, {
7404
7559
  file: c.path,
7405
7560
  message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
7406
7561
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -7414,7 +7569,7 @@ They WILL FAIL in v10.0.0
7414
7569
  if (c.before != null && c.after != null) {
7415
7570
  for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
7416
7571
  out3.push(
7417
- makeFinding(RULE7, policy, {
7572
+ makeFinding(RULE8, policy, {
7418
7573
  file: c.path,
7419
7574
  message: `CODEOWNERS was weakened: ${reason}.`,
7420
7575
  evidence: reason,
@@ -7435,7 +7590,7 @@ They WILL FAIL in v10.0.0
7435
7590
  const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
7436
7591
  if (!kept) {
7437
7592
  out3.push(
7438
- makeFinding(RULE7, policy, {
7593
+ makeFinding(RULE8, policy, {
7439
7594
  file: c.path,
7440
7595
  message: "A protected hook script no longer matches the shape `tamperward init` writes.",
7441
7596
  evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
@@ -7458,7 +7613,7 @@ They WILL FAIL in v10.0.0
7458
7613
  if (shebang || !live) {
7459
7614
  const why = shebang ?? (gate.length ? `its \`${gate[0].identity}\` is ${gate[0].state}${gate[0].why ? ` (${gate[0].why})` : ""}` : "it runs no `tamperward check`");
7460
7615
  out3.push(
7461
- makeFinding(RULE7, policy, {
7616
+ makeFinding(RULE8, policy, {
7462
7617
  file: c.path,
7463
7618
  message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
7464
7619
  evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or make the gate live in it`,
@@ -7467,7 +7622,7 @@ They WILL FAIL in v10.0.0
7467
7622
  );
7468
7623
  } else if (earlyExit) {
7469
7624
  out3.push(
7470
- makeFinding(RULE7, policy, {
7625
+ makeFinding(RULE8, policy, {
7471
7626
  file: c.path,
7472
7627
  line: earlyExit.newLine ?? void 0,
7473
7628
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -7489,7 +7644,7 @@ They WILL FAIL in v10.0.0
7489
7644
  deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
7490
7645
  ].filter((d) => d !== null);
7491
7646
  out3.push(
7492
- makeFinding(RULE7, policy, {
7647
+ makeFinding(RULE8, policy, {
7493
7648
  file: c.path,
7494
7649
  message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
7495
7650
  evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or restore it`,
@@ -7501,7 +7656,7 @@ They WILL FAIL in v10.0.0
7501
7656
  }
7502
7657
  if (shebangChanged && c.after != null) {
7503
7658
  out3.push(
7504
- makeFinding(RULE7, policy, {
7659
+ makeFinding(RULE8, policy, {
7505
7660
  file: c.path,
7506
7661
  line: 1,
7507
7662
  message: `A protected hook's interpreter was changed: ${shebang}.`,
@@ -7512,7 +7667,7 @@ They WILL FAIL in v10.0.0
7512
7667
  }
7513
7668
  if (earlyExit) {
7514
7669
  out3.push(
7515
- makeFinding(RULE7, policy, {
7670
+ makeFinding(RULE8, policy, {
7516
7671
  file: c.path,
7517
7672
  line: earlyExit.newLine ?? void 0,
7518
7673
  message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
@@ -7524,7 +7679,7 @@ They WILL FAIL in v10.0.0
7524
7679
  const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
7525
7680
  if (deadGuard) {
7526
7681
  out3.push(
7527
- makeFinding(RULE7, policy, {
7682
+ makeFinding(RULE8, policy, {
7528
7683
  file: c.path,
7529
7684
  line: deadGuard.newLine ?? void 0,
7530
7685
  message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
@@ -7536,7 +7691,7 @@ They WILL FAIL in v10.0.0
7536
7691
  const lost = scriptWeakening(beforeLines, afterLines, opts);
7537
7692
  if (lost.length) {
7538
7693
  out3.push(
7539
- makeFinding(RULE7, policy, {
7694
+ makeFinding(RULE8, policy, {
7540
7695
  file: c.path,
7541
7696
  message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
7542
7697
  evidence: lost[0].evidence,
@@ -7555,7 +7710,7 @@ They WILL FAIL in v10.0.0
7555
7710
  const m = l.content.match(INLINE);
7556
7711
  if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
7557
7712
  out3.push(
7558
- makeFinding(RULE7, policy, {
7713
+ makeFinding(RULE8, policy, {
7559
7714
  file: c.path,
7560
7715
  line: l.newLine ?? void 0,
7561
7716
  message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
@@ -7565,7 +7720,7 @@ They WILL FAIL in v10.0.0
7565
7720
  );
7566
7721
  } else if (/enabled:\s*false\b/.test(l.content)) {
7567
7722
  out3.push(
7568
- makeFinding(RULE7, policy, {
7723
+ makeFinding(RULE8, policy, {
7569
7724
  file: c.path,
7570
7725
  line: l.newLine ?? void 0,
7571
7726
  message: "A rule was disabled (enabled: false) in the policy.",
@@ -7583,7 +7738,7 @@ They WILL FAIL in v10.0.0
7583
7738
  const hiding = updateIndexHiding(toks, policy);
7584
7739
  if (hiding) {
7585
7740
  out3.push(
7586
- makeFinding(RULE7, policy, {
7741
+ makeFinding(RULE8, policy, {
7587
7742
  file: repoRelative(hiding.file, ctx),
7588
7743
  message: `Hook tampering via shell: ${hiding.reason}.`,
7589
7744
  evidence: seg,
@@ -7599,7 +7754,7 @@ They WILL FAIL in v10.0.0
7599
7754
  const file = target ? repoRelative(target, ctx) : void 0;
7600
7755
  const outside = file !== void 0 && (isAbsolute6(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
7601
7756
  out3.push(
7602
- makeFinding(RULE7, policy, {
7757
+ makeFinding(RULE8, policy, {
7603
7758
  ...file !== void 0 ? { file } : {},
7604
7759
  message: `Hook tampering via shell: ${why}${outside}.`,
7605
7760
  evidence: seg,
@@ -7835,7 +7990,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
7835
7990
  if (!was || !now || was.path === now.path) return [];
7836
7991
  const samples = runnerSamples(now.path, policy, ctx, "pytest");
7837
7992
  return suiteNarrowings(was.content, now.content, now.path, samples).map(
7838
- (n) => makeFinding(RULE8, policy, {
7993
+ (n) => makeFinding(RULE9, policy, {
7839
7994
  file: now.path,
7840
7995
  message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
7841
7996
  evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
@@ -7843,7 +7998,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
7843
7998
  })
7844
7999
  );
7845
8000
  }
7846
- var RULE8, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
8001
+ var RULE9, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
7847
8002
  var init_test_deletion = __esm({
7848
8003
  "src/detectors/test-deletion.ts"() {
7849
8004
  "use strict";
@@ -7856,7 +8011,7 @@ var init_test_deletion = __esm({
7856
8011
  init_suite_config();
7857
8012
  init_invocation();
7858
8013
  init_narrow();
7859
- RULE8 = "test-deletion";
8014
+ RULE9 = "test-deletion";
7860
8015
  JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
7861
8016
  TEST_DEFS = {
7862
8017
  py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
@@ -7876,7 +8031,7 @@ var init_test_deletion = __esm({
7876
8031
  SLICE_SCRIPT = /^test:(?:unit|integration)$/;
7877
8032
  SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
7878
8033
  testDeletion = {
7879
- id: RULE8,
8034
+ id: RULE9,
7880
8035
  surface: ["file", "command"],
7881
8036
  certainty: "mechanical",
7882
8037
  run(changes, policy, _view, ctx) {
@@ -7908,7 +8063,7 @@ var init_test_deletion = __esm({
7908
8063
  if (c.op === "delete" && isTest) {
7909
8064
  if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
7910
8065
  out3.push(
7911
- makeFinding(RULE8, policy, {
8066
+ makeFinding(RULE9, policy, {
7912
8067
  file: c.path,
7913
8068
  message: "A test file was deleted.",
7914
8069
  evidence: c.path,
@@ -7917,7 +8072,7 @@ var init_test_deletion = __esm({
7917
8072
  );
7918
8073
  } else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
7919
8074
  out3.push(
7920
- makeFinding(RULE8, policy, {
8075
+ makeFinding(RULE9, policy, {
7921
8076
  file: c.path,
7922
8077
  message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
7923
8078
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -7926,7 +8081,7 @@ var init_test_deletion = __esm({
7926
8081
  );
7927
8082
  } else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
7928
8083
  out3.push(
7929
- makeFinding(RULE8, policy, {
8084
+ makeFinding(RULE9, policy, {
7930
8085
  file: c.path,
7931
8086
  message: `A test file was renamed into a path the runner does not walk (${c.oldPath} \u2192 ${c.path}): ${leavesWalk(c.oldPath, c.path)}.`,
7932
8087
  evidence: `${c.oldPath} \u2192 ${c.path}`,
@@ -7941,7 +8096,7 @@ var init_test_deletion = __esm({
7941
8096
  const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
7942
8097
  if (isRelocation(removed, before.min - after.min)) continue;
7943
8098
  out3.push(
7944
- makeFinding(RULE8, policy, {
8099
+ makeFinding(RULE9, policy, {
7945
8100
  file: c.path,
7946
8101
  message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
7947
8102
  evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
@@ -7954,7 +8109,7 @@ var init_test_deletion = __esm({
7954
8109
  const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
7955
8110
  for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
7956
8111
  out3.push(
7957
- makeFinding(RULE8, policy, {
8112
+ makeFinding(RULE9, policy, {
7958
8113
  file: c.path,
7959
8114
  message: `The runner no longer selects ${n.path}: ${n.reason}.`,
7960
8115
  evidence: `${n.path} dropped from the suite by ${c.path}`,
@@ -7966,12 +8121,12 @@ var init_test_deletion = __esm({
7966
8121
  const w = n.weakening;
7967
8122
  const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
7968
8123
  out3.push(
7969
- w.state === "removed" ? makeFinding(RULE8, policy, {
8124
+ w.state === "removed" ? makeFinding(RULE9, policy, {
7970
8125
  file: c.path,
7971
8126
  message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
7972
8127
  evidence,
7973
8128
  remediation: "Keep the script running its check. Replacing the runner in package.json removes every test from every `npm test` while the workflow line stays untouched."
7974
- }) : makeFinding(RULE8, policy, {
8129
+ }) : makeFinding(RULE9, policy, {
7975
8130
  file: c.path,
7976
8131
  message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
7977
8132
  evidence,
@@ -8062,7 +8217,7 @@ var init_test_deletion = __esm({
8062
8217
  }
8063
8218
  if (why) {
8064
8219
  out3.push(
8065
- makeFinding(RULE8, policy, {
8220
+ makeFinding(RULE9, policy, {
8066
8221
  message: `Test removed via shell: ${why}.`,
8067
8222
  evidence,
8068
8223
  remediation: "Fix the code under test. Mutating a spec from the shell is still deleting the test \u2014 and is blocked the same way."
@@ -8118,7 +8273,7 @@ function significantLinesOrdered(src, path) {
8118
8273
  }
8119
8274
  return out3;
8120
8275
  }
8121
- var RULE9, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
8276
+ var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
8122
8277
  var init_test_content_removal = __esm({
8123
8278
  "src/detectors/test-content-removal.ts"() {
8124
8279
  "use strict";
@@ -8127,13 +8282,13 @@ var init_test_content_removal = __esm({
8127
8282
  init_finding();
8128
8283
  init_test_deletion();
8129
8284
  init_files();
8130
- RULE9 = "test-content-removal";
8285
+ RULE10 = "test-content-removal";
8131
8286
  MIN_REMOVED_LINES = 3;
8132
8287
  MIN_REMOVED_ROWS = 2;
8133
8288
  ws = (s) => s.replace(/\s+/g, "");
8134
8289
  TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
8135
8290
  testContentRemoval = {
8136
- id: RULE9,
8291
+ id: RULE10,
8137
8292
  surface: ["file"],
8138
8293
  certainty: "mechanical",
8139
8294
  run(changes, policy) {
@@ -8174,7 +8329,7 @@ var init_test_content_removal = __esm({
8174
8329
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
8175
8330
  if (goneRows.length >= MIN_REMOVED_ROWS) {
8176
8331
  out3.push(
8177
- makeFinding(RULE9, policy, {
8332
+ makeFinding(RULE10, policy, {
8178
8333
  file: c.path,
8179
8334
  message: `Rows removed from a test table: ${goneRows.length} case(s) gone from an each-table that spreads from elsewhere, and they reappear nowhere in the change.`,
8180
8335
  evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -8185,7 +8340,7 @@ var init_test_content_removal = __esm({
8185
8340
  }
8186
8341
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
8187
8342
  out3.push(
8188
- makeFinding(RULE9, policy, {
8343
+ makeFinding(RULE10, policy, {
8189
8344
  file: c.path,
8190
8345
  message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
8191
8346
  evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
@@ -8208,14 +8363,14 @@ function namesProtectedSnapshot(seg, policy) {
8208
8363
  }
8209
8364
  return null;
8210
8365
  }
8211
- var RULE10, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
8366
+ var RULE11, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
8212
8367
  var init_snapshot_rewrite = __esm({
8213
8368
  "src/detectors/snapshot-rewrite.ts"() {
8214
8369
  "use strict";
8215
8370
  init_policy();
8216
8371
  init_finding();
8217
8372
  init_command();
8218
- RULE10 = "snapshot-rewrite";
8373
+ RULE11 = "snapshot-rewrite";
8219
8374
  RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
8220
8375
  PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
8221
8376
  UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
@@ -8225,12 +8380,12 @@ var init_snapshot_rewrite = __esm({
8225
8380
  REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
8226
8381
  MUTATE = /(?:^|\s)(?:rm|cp|mv|tee|truncate|dd)(?:\s|$)|sed\s+-i|>{1,2}|\bfind\b.*\s-(?:delete\b|exec\s+rm\b)|\bgit\s+(?:checkout|restore)\b/;
8227
8382
  snapshotRewrite = {
8228
- id: RULE10,
8383
+ id: RULE11,
8229
8384
  surface: ["command", "file"],
8230
8385
  certainty: "mechanical",
8231
8386
  run(changes, policy) {
8232
8387
  const out3 = [];
8233
- const warn = (input) => out3.push(makeFinding(RULE10, policy, { ...input, defaultSeverity: "warn" }));
8388
+ const warn = (input) => out3.push(makeFinding(RULE11, policy, { ...input, defaultSeverity: "warn" }));
8234
8389
  for (const c of changes) {
8235
8390
  if (c.kind === "command") {
8236
8391
  for (const seg of segments(c.raw)) {
@@ -8278,16 +8433,16 @@ function isSnapshotChange(c, policy) {
8278
8433
  if (isProtected(c.path, policy, "snapshots")) return true;
8279
8434
  return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
8280
8435
  }
8281
- var RULE11, COMMIT_VIEWS, snapshotOnlyRewrite;
8436
+ var RULE12, COMMIT_VIEWS, snapshotOnlyRewrite;
8282
8437
  var init_snapshot_only = __esm({
8283
8438
  "src/detectors/snapshot-only.ts"() {
8284
8439
  "use strict";
8285
8440
  init_policy();
8286
8441
  init_finding();
8287
- RULE11 = "snapshot-only-rewrite";
8442
+ RULE12 = "snapshot-only-rewrite";
8288
8443
  COMMIT_VIEWS = ["staged", "range"];
8289
8444
  snapshotOnlyRewrite = {
8290
- id: RULE11,
8445
+ id: RULE12,
8291
8446
  surface: ["file"],
8292
8447
  certainty: "mechanical",
8293
8448
  run(changes, policy, view) {
@@ -8300,7 +8455,7 @@ var init_snapshot_only = __esm({
8300
8455
  if (!first) return [];
8301
8456
  const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
8302
8457
  return [
8303
- makeFinding(RULE11, policy, {
8458
+ makeFinding(RULE12, policy, {
8304
8459
  file: first.path,
8305
8460
  message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
8306
8461
  evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
@@ -8474,7 +8629,7 @@ function findingForBlock(path, before, after, policy) {
8474
8629
  const weakened2 = directionalWeakening(before.assertions, after.assertions);
8475
8630
  if (weakened2) {
8476
8631
  const message = weakened2.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened2.before.matcher} was replaced by weaker ${weakened2.after.matcher} on the same subject.`;
8477
- return makeFinding(RULE12, policy, {
8632
+ return makeFinding(RULE13, policy, {
8478
8633
  file: path,
8479
8634
  message,
8480
8635
  evidence: `${weakened2.before.text} -> ${weakened2.after.text}`,
@@ -8483,7 +8638,7 @@ function findingForBlock(path, before, after, policy) {
8483
8638
  }
8484
8639
  const removed = pureRemoval(before.assertions, after.assertions);
8485
8640
  if (removed) {
8486
- return makeFinding(RULE12, policy, {
8641
+ return makeFinding(RULE13, policy, {
8487
8642
  file: path,
8488
8643
  message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
8489
8644
  evidence: removed.text,
@@ -8492,7 +8647,7 @@ function findingForBlock(path, before, after, policy) {
8492
8647
  }
8493
8648
  return null;
8494
8649
  }
8495
- var RULE12, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
8650
+ var RULE13, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
8496
8651
  var init_assertion_weakening = __esm({
8497
8652
  "src/detectors/assertion-weakening.ts"() {
8498
8653
  "use strict";
@@ -8500,14 +8655,14 @@ var init_assertion_weakening = __esm({
8500
8655
  init_policy();
8501
8656
  init_finding();
8502
8657
  init_files();
8503
- RULE12 = "assertion-weakening";
8658
+ RULE13 = "assertion-weakening";
8504
8659
  TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
8505
8660
  SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
8506
8661
  EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
8507
8662
  WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
8508
8663
  compact = (s) => s.replace(/\s+/g, "");
8509
8664
  assertionWeakening = {
8510
- id: RULE12,
8665
+ id: RULE13,
8511
8666
  surface: ["file"],
8512
8667
  certainty: "heuristic",
8513
8668
  run(changes, policy) {
@@ -8542,6 +8697,7 @@ var init_detectors = __esm({
8542
8697
  init_lint_suppression();
8543
8698
  init_test_skip();
8544
8699
  init_coverage_lowering();
8700
+ init_coverage_exclusion();
8545
8701
  init_ci_tampering();
8546
8702
  init_hook_tampering();
8547
8703
  init_test_deletion();
@@ -8556,6 +8712,7 @@ var init_detectors = __esm({
8556
8712
  lintSuppression,
8557
8713
  testSkip,
8558
8714
  coverageLowering,
8715
+ coverageExclusion,
8559
8716
  ciTampering,
8560
8717
  hookTampering,
8561
8718
  testDeletion,
@@ -10027,7 +10184,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
10027
10184
  };
10028
10185
  }
10029
10186
  function transientFindings(events, persistentPaths, policy, finalHash) {
10030
- if (!isEnabled(RULE13, policy)) return [];
10187
+ if (!isEnabled(RULE14, policy)) return [];
10031
10188
  const byPath = /* @__PURE__ */ new Map();
10032
10189
  for (const e of events) {
10033
10190
  if (!isProtected(e.path, policy)) continue;
@@ -10047,7 +10204,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
10047
10204
  const modeChurn = modes.size > 1;
10048
10205
  if (!contentChurn && !modeChurn) continue;
10049
10206
  const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
10050
- const f = makeFinding(RULE13, policy, {
10207
+ const f = makeFinding(RULE14, policy, {
10051
10208
  file: path,
10052
10209
  message: `Transient protected mutation: ${what}, leaving no net diff.`,
10053
10210
  evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
@@ -10062,14 +10219,14 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
10062
10219
  }
10063
10220
  return out3;
10064
10221
  }
10065
- var RULE13, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
10222
+ var RULE14, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
10066
10223
  var init_fs_events = __esm({
10067
10224
  "src/detectors/fs-events.ts"() {
10068
10225
  "use strict";
10069
10226
  init_policy();
10070
10227
  init_finding();
10071
10228
  init_narrow();
10072
- RULE13 = "transient-protected-mutation";
10229
+ RULE14 = "transient-protected-mutation";
10073
10230
  MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
10074
10231
  MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
10075
10232
  DEFAULT_EVENT_READ_IO = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.20",
3
+ "version": "2.24.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",