tamperward 2.23.10 → 2.23.11

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +383 -169
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -4890,13 +4890,7 @@ var init_hook_wiring = __esm({
4890
4890
  }
4891
4891
  });
4892
4892
 
4893
- // src/detectors/ci-tampering.ts
4894
- function isActiveWorkflow(path) {
4895
- return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
4896
- }
4897
- function commandCore(line) {
4898
- return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
4899
- }
4893
+ // src/detectors/gh-expression.ts
4900
4894
  function foldExpressions(s) {
4901
4895
  return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
4902
4896
  const v = foldConst(e.trim());
@@ -4904,90 +4898,6 @@ function foldExpressions(s) {
4904
4898
  return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
4905
4899
  });
4906
4900
  }
4907
- function canonical(core) {
4908
- let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
4909
- s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
4910
- s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
4911
- s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
4912
- s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
4913
- s = s.replace(/^exec\s+/, "");
4914
- return s.replace(/\s+--\s*$/, "").trim();
4915
- }
4916
- function runnerNarrowed(cmd, args) {
4917
- const t = RUNNER_NARROWINGS.find((x) => x.runner.test(cmd));
4918
- return t !== void 0 && t.flags.test(" " + args.trim());
4919
- }
4920
- function coverageFloorLowered(removed, added) {
4921
- const was = removed.match(COVERAGE_FLOOR);
4922
- const now = added.match(COVERAGE_FLOOR);
4923
- return was !== null && now !== null && Number(now[1]) < Number(was[1]);
4924
- }
4925
- function inCommandPosition(core) {
4926
- return canonical(core).split(/\s*(?:&&|\|\||;|\|)\s*/).some((seg) => CHECK_HEAD.test(canonical(seg.replace(COMMAND_PREFIX, ""))));
4927
- }
4928
- function narrowedByPositional(args) {
4929
- const toks = args.trim().split(/\s+/).filter(Boolean);
4930
- for (let i = 0; i < toks.length; i++) {
4931
- const t = toks[i];
4932
- if (t === "--" || t.startsWith("-")) continue;
4933
- const prev = toks[i - 1];
4934
- if (prev && (prev.startsWith("-") && prev !== "--" && !prev.includes("=") || /[<>]/.test(prev))) continue;
4935
- if (/[<>|;&]/.test(t)) continue;
4936
- if (PATH_SHAPED.test(t)) return true;
4937
- }
4938
- return false;
4939
- }
4940
- function checkKind(core) {
4941
- if (!invokesCheck(core)) return null;
4942
- const s = canonical(core);
4943
- if (/\btamperward\b/.test(s)) return "gate";
4944
- if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
4945
- if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
4946
- return "test";
4947
- }
4948
- function survives(removedCore, isUses, afterCores, addedCores) {
4949
- if (isUses) {
4950
- const ref = usesRef(removedCore);
4951
- return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
4952
- }
4953
- const r = canonical(removedCore);
4954
- const kind = checkKind(removedCore);
4955
- const narrowed = (args) => kind === "test" && narrowedByPositional(args);
4956
- let neutralised;
4957
- for (const raw of afterCores) {
4958
- const a = canonical(raw);
4959
- if (a === r) return { state: "kept" };
4960
- if (a.startsWith(r + " ")) {
4961
- const rest = a.slice(r.length);
4962
- if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest) || runnerNarrowed(a, rest)) neutralised ??= raw;
4963
- else return { state: "kept" };
4964
- } else if (r.startsWith(a + " ") && invokesCheck(a)) {
4965
- return { state: "kept" };
4966
- } else {
4967
- const w = a.match(TIMEOUT_WRAP);
4968
- if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
4969
- }
4970
- }
4971
- let respelled = false;
4972
- for (const raw of addedCores) {
4973
- if (!kind || checkKind(raw) !== kind) continue;
4974
- const a = canonical(raw);
4975
- if (a.match(TIMEOUT_WRAP)) {
4976
- neutralised ??= raw;
4977
- continue;
4978
- }
4979
- if (!inCommandPosition(raw)) continue;
4980
- if (coverageFloorLowered(r, a)) {
4981
- neutralised ??= raw;
4982
- continue;
4983
- }
4984
- const args = a.slice(a.indexOf(" ") + 1 || a.length);
4985
- if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
4986
- else respelled = true;
4987
- }
4988
- if (respelled) return { state: "kept" };
4989
- return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
4990
- }
4991
4901
  function lex(src) {
4992
4902
  const out3 = [];
4993
4903
  let i = 0;
@@ -5177,6 +5087,329 @@ function callConst(name, args) {
5177
5087
  return void 0;
5178
5088
  }
5179
5089
  }
5090
+ var EXPR_TOKEN, MATRIX_TOKEN, TRUTHY, truthy;
5091
+ var init_gh_expression = __esm({
5092
+ "src/detectors/gh-expression.ts"() {
5093
+ "use strict";
5094
+ EXPR_TOKEN = "__expr__";
5095
+ MATRIX_TOKEN = "__matrix__";
5096
+ TRUTHY = Symbol("truthy");
5097
+ truthy = (v) => v === TRUTHY ? true : typeof v === "string" ? v !== "" : typeof v === "number" ? v !== 0 : v === true;
5098
+ }
5099
+ });
5100
+
5101
+ // src/detectors/invocation.ts
5102
+ function canonical(core) {
5103
+ let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
5104
+ s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
5105
+ s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
5106
+ s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
5107
+ s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
5108
+ s = s.replace(/^exec\s+/, "");
5109
+ return s.replace(/\s+--\s*$/, "").trim();
5110
+ }
5111
+ function runnerNarrowed(cmd, args) {
5112
+ return runnerNarrowing(cmd, args) !== null;
5113
+ }
5114
+ function runnerNarrowing(cmd, args) {
5115
+ const t = RUNNER_NARROWINGS.find((x) => x.runner.test(cmd));
5116
+ const m = t ? (" " + args.trim()).match(t.flags) : null;
5117
+ return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
5118
+ }
5119
+ function coverageFloorLowered(removed, added) {
5120
+ const was = removed.match(COVERAGE_FLOOR);
5121
+ const now = added.match(COVERAGE_FLOOR);
5122
+ return was !== null && now !== null && Number(now[1]) < Number(was[1]);
5123
+ }
5124
+ function inCommandPosition(core) {
5125
+ return canonical(core).split(/\s*(?:&&|\|\||;|\|)\s*/).some((seg) => CHECK_HEAD.test(canonical(seg.replace(COMMAND_PREFIX, ""))));
5126
+ }
5127
+ function narrowedByPositional(args) {
5128
+ return positionalOf(args) !== null;
5129
+ }
5130
+ function positionalOf(args) {
5131
+ const toks = args.trim().split(/\s+/).filter(Boolean);
5132
+ for (let i = 0; i < toks.length; i++) {
5133
+ const t = toks[i];
5134
+ if (t === "--" || t.startsWith("-")) continue;
5135
+ const prev = toks[i - 1];
5136
+ if (prev && (prev.startsWith("-") && prev !== "--" && !prev.includes("=") || /[<>]/.test(prev))) continue;
5137
+ if (/[<>|;&]/.test(t)) continue;
5138
+ if (PATH_SHAPED.test(t)) return t;
5139
+ }
5140
+ return null;
5141
+ }
5142
+ function checkKind(core) {
5143
+ if (!invokesCheck(core)) return null;
5144
+ const s = canonical(core);
5145
+ if (/\btamperward\b/.test(s)) return "gate";
5146
+ if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
5147
+ if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
5148
+ return "test";
5149
+ }
5150
+ function survives(removedCore, isUses, afterCores, addedCores) {
5151
+ if (isUses) {
5152
+ const ref = usesRef(removedCore);
5153
+ return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
5154
+ }
5155
+ const r = canonical(removedCore);
5156
+ const kind = checkKind(removedCore);
5157
+ const narrowed = (args) => kind === "test" && narrowedByPositional(args);
5158
+ let neutralised;
5159
+ for (const raw of afterCores) {
5160
+ const a = canonical(raw);
5161
+ if (a === r) return { state: "kept" };
5162
+ if (a.startsWith(r + " ")) {
5163
+ const rest = a.slice(r.length);
5164
+ if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest) || runnerNarrowed(a, rest)) neutralised ??= raw;
5165
+ else return { state: "kept" };
5166
+ } else if (r.startsWith(a + " ") && invokesCheck(a)) {
5167
+ return { state: "kept" };
5168
+ } else {
5169
+ const w = a.match(TIMEOUT_WRAP);
5170
+ if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
5171
+ }
5172
+ }
5173
+ let respelled = false;
5174
+ for (const raw of addedCores) {
5175
+ if (!kind || checkKind(raw) !== kind) continue;
5176
+ const a = canonical(raw);
5177
+ if (a.match(TIMEOUT_WRAP)) {
5178
+ neutralised ??= raw;
5179
+ continue;
5180
+ }
5181
+ if (!inCommandPosition(raw)) continue;
5182
+ if (coverageFloorLowered(r, a)) {
5183
+ neutralised ??= raw;
5184
+ continue;
5185
+ }
5186
+ const args = a.slice(a.indexOf(" ") + 1 || a.length);
5187
+ if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
5188
+ else respelled = true;
5189
+ }
5190
+ if (respelled) return { state: "kept" };
5191
+ return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
5192
+ }
5193
+ function unwrap(seg) {
5194
+ const s = seg.trim().replace(PYTHON_M, "").replace(NODE_BIN, "$1").replace(BIN_OF, "$1$2");
5195
+ if (invokesCheck(s) || PRINTER.test(s) || /^["']/.test(s)) return s;
5196
+ const toks = s.split(/\s+/);
5197
+ for (let i = 1; i < toks.length && i <= 16; i++) {
5198
+ const rest = toks.slice(i).join(" ");
5199
+ if (invokesCheck(rest)) return rest;
5200
+ }
5201
+ return s;
5202
+ }
5203
+ function scriptKind3(inner) {
5204
+ const kind = checkKind(inner);
5205
+ if (!kind) return null;
5206
+ const head = canonical(inner).match(/^(tests?|lint|type-?check|coverage)(?=[\s:]|$)/i)?.[1];
5207
+ if (!head) return kind;
5208
+ return /^type/i.test(head) ? "types" : /^lint/i.test(head) ? "lint" : "test";
5209
+ }
5210
+ function runnerOfSegment(canon) {
5211
+ const m = canon.match(RUNNER_NAME);
5212
+ if (!m) return null;
5213
+ const n = m[1];
5214
+ return n === "jest" || n === "vitest" || n === "mocha" || n === "pytest" || n === "go" || n === "cargo" ? n : "node";
5215
+ }
5216
+ function statements(raw) {
5217
+ const out3 = [];
5218
+ let single = false;
5219
+ let double = false;
5220
+ let last = 0;
5221
+ for (let i = 0; i < raw.length; i++) {
5222
+ const ch = raw[i];
5223
+ if (ch === "\\" && !single) {
5224
+ i++;
5225
+ continue;
5226
+ }
5227
+ if (ch === "'" && !double) {
5228
+ single = !single;
5229
+ continue;
5230
+ }
5231
+ if (ch === '"' && !single) {
5232
+ double = !double;
5233
+ continue;
5234
+ }
5235
+ if (single || double) continue;
5236
+ const prev = raw[i - 1];
5237
+ const next = raw[i + 1];
5238
+ let op = null;
5239
+ if (ch === "&" && next === "&") op = "&&";
5240
+ else if (ch === "|" && next === "|") op = "||";
5241
+ else if (ch === "|" && next === "&") op = "|&";
5242
+ else if (ch === ";" || ch === "\n") op = ";";
5243
+ else if (ch === "|" && prev !== ">") op = "|";
5244
+ else if (ch === "&" && prev !== ">" && next !== ">") op = "&";
5245
+ if (!op) continue;
5246
+ out3.push({ seg: raw.slice(last, i).trim(), op });
5247
+ i += op.length - 1;
5248
+ last = i + 1;
5249
+ }
5250
+ out3.push({ seg: raw.slice(last).trim(), op: null });
5251
+ return out3.filter((s) => s.seg !== "");
5252
+ }
5253
+ function descriptors(cmd, runner, opts) {
5254
+ const out3 = [];
5255
+ const stmts = statements(cmd);
5256
+ for (let i = 0; i < stmts.length; i++) {
5257
+ const { seg, op } = stmts[i];
5258
+ const wrapped = seg.match(TIMEOUT_WRAP);
5259
+ const inner = unwrap(wrapped ? wrapped[1] : seg);
5260
+ if (!invokesCheck(inner)) continue;
5261
+ const kind = scriptKind3(inner);
5262
+ const canon = canonical(inner);
5263
+ const rn = runner ?? runnerOfSegment(canon);
5264
+ const args = canon.includes(" ") ? " " + canon.slice(canon.indexOf(" ") + 1) : " ";
5265
+ if (wrapped) out3.push({ key: "timeout", what: "a timeout wrapper", direction: "added" });
5266
+ const following = stmts[i + 1]?.seg ?? "";
5267
+ if (op === "||" || op === "|" || op === "&" || op === ";" && !invokesCheck(unwrap(following))) {
5268
+ out3.push({ key: `op:${op}`, what: `${op} ${following.split(/\s+/).slice(0, 2).join(" ")}`.trim(), direction: "added" });
5269
+ }
5270
+ for (const re of MASKING_FLAGS) {
5271
+ const f = spelled(re, args);
5272
+ if (f) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
5273
+ }
5274
+ for (const re of GATE_FLAGS) {
5275
+ const f = spelled(re, " " + seg);
5276
+ if (f) out3.push({ key: `gate:${f}`, what: f, direction: "removed" });
5277
+ }
5278
+ if (kind !== "test" || opts.sliceByDesign || opts.extended?.has(kind)) continue;
5279
+ const flags = [SUITE_NARROWING_FLAGS, ...NARROWING_BY_RUNNER["*"], ...rn ? NARROWING_BY_RUNNER[rn] : []];
5280
+ for (const re of flags) {
5281
+ const f = spelled(re, args);
5282
+ if (f && !opts.excuseFlag?.(f, canon)) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
5283
+ }
5284
+ if (rn === "cargo" && CARGO_SKIP.test(args)) out3.push({ key: "flag:-- --skip", what: "-- --skip", direction: "added" });
5285
+ const shared = runnerNarrowing(canon, args);
5286
+ if (shared && !opts.excuseFlag?.(shared, canon)) out3.push({ key: `flag:${shared}`, what: shared, direction: "added" });
5287
+ if (opts.fresh) continue;
5288
+ const cfg = args.match(CONFIG_FLAG);
5289
+ if (cfg && !opts.configReviewed?.(cfg[3])) out3.push({ key: `config:${cfg[3]}`, what: `${cfg[1]} ${cfg[3]}`, direction: "added" });
5290
+ const pos = positionalOf(args);
5291
+ if (pos) out3.push({ key: "positional", what: `a path positional (${pos})`, direction: "added" });
5292
+ }
5293
+ return out3;
5294
+ }
5295
+ function checkStatements(cmd) {
5296
+ const out3 = [];
5297
+ for (const seg of segments(cmd)) {
5298
+ const w = seg.match(TIMEOUT_WRAP);
5299
+ const inner = unwrap(w ? w[1] : seg);
5300
+ const kind = scriptKind3(inner);
5301
+ if (kind) out3.push({ kind, canon: canonical(inner) });
5302
+ }
5303
+ return out3;
5304
+ }
5305
+ function checkKinds(cmd) {
5306
+ return new Set(checkStatements(cmd).map((s) => s.kind));
5307
+ }
5308
+ function failsLoudly(cmd) {
5309
+ const last = segments(cmd).pop();
5310
+ return last !== void 0 && FAILS_LOUDLY.test(last.trim());
5311
+ }
5312
+ function invocationWeakening(before, after, runner, opts = {}) {
5313
+ const was = checkKinds(before);
5314
+ const now = checkKinds(after);
5315
+ if (now.size === 0 && failsLoudly(after)) return { state: "kept" };
5316
+ for (const k of was) {
5317
+ if (now.has(k) || opts.relocated?.(k)) continue;
5318
+ const instead = [...now].filter((x) => !was.has(x));
5319
+ return { state: "removed", kind: k, what: instead.length ? `runs a ${instead.join("/")} check instead` : now.size ? "the check is gone" : "no check is run" };
5320
+ }
5321
+ const survived = new Set(checkStatements(before).map((s) => s.canon));
5322
+ const extended = new Set(checkStatements(after).filter((s) => survived.has(s.canon)).map((s) => s.kind));
5323
+ const read = { ...opts, fresh: was.size === 0, extended };
5324
+ const beforeDs = descriptors(before, runner, read);
5325
+ const afterDs = descriptors(after, runner, read);
5326
+ const seenBefore = new Set(beforeDs.map((d) => d.key));
5327
+ const added = afterDs.find((d) => d.direction === "added" && !seenBefore.has(d.key));
5328
+ if (added) return { state: "neutralised", what: added.what, direction: "added" };
5329
+ if (coverageFloorLowered(before, after)) return { state: "neutralised", what: "a lowered --cov-fail-under", direction: "added" };
5330
+ const seenAfter = new Set(afterDs.map((d) => d.key));
5331
+ const dropped = beforeDs.find((d) => d.direction === "removed" && !seenAfter.has(d.key));
5332
+ if (dropped) return { state: "neutralised", what: dropped.what, direction: "removed" };
5333
+ return { state: "kept" };
5334
+ }
5335
+ var INVOKES_TOOL, INVOKES_SCRIPT, invokesCheck, MATRIX_FLAG, usesRef, SUITE_NARROWING_FLAGS, NEUTRALISING_SUFFIX, TIMEOUT_WRAP, RUNNER_NARROWINGS, COVERAGE_FLOOR, COMMAND_PREFIX, CHECK_HEAD, PATH_SHAPED, NARROWING_BY_RUNNER, CARGO_SKIP, MASKING_FLAGS, GATE_FLAGS, CONFIG_FLAG, PRINTER, PYTHON_M, NODE_BIN, BIN_OF, RUNNER_NAME, spelled, FAILS_LOUDLY;
5336
+ var init_invocation = __esm({
5337
+ "src/detectors/invocation.ts"() {
5338
+ "use strict";
5339
+ init_gh_expression();
5340
+ init_command();
5341
+ INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint|node\s+--test|biome\s+(?:ci|check|lint)|deno\s+(?:test|lint|check)|ruff\s+check)\b/;
5342
+ 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/;
5343
+ invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
5344
+ MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
5345
+ usesRef = (core) => core.replace(/@.*$/, "");
5346
+ SUITE_NARROWING_FLAGS = /--testPathIgnorePatterns\b|--testPathPattern\b|--testNamePattern\b|(?:^|\s)-t\s|--onlyChanged\b|--changed\b|--findRelatedTests\b|--exclude\b|--dir\b|--project\b/;
5347
+ NEUTRALISING_SUFFIX = new RegExp(
5348
+ `\\|\\||;|(?<!&)&(?!&)|\\|(?!\\|)|--passWithNoTests\\b|--coverage=false\\b|--coverageThreshold\\b|${SUITE_NARROWING_FLAGS.source}`
5349
+ );
5350
+ TIMEOUT_WRAP = /^timeout\s+(?:-{1,2}\S+\s+)*\d\S*\s+(.+)$/;
5351
+ RUNNER_NARROWINGS = [
5352
+ {
5353
+ // a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
5354
+ // `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
5355
+ runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
5356
+ flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
5357
+ },
5358
+ {
5359
+ runner: /^(?:jest|vitest)\b/,
5360
+ flags: /\s--shard(?:=|\s)|\s--root(?:Dir)?(?:=|\s)|\s--testMatch(?:=|\s)|\s--testRegex(?:=|\s)|\s--modulePathIgnorePatterns(?:=|\s)|\s--selectProjects(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)/
5361
+ },
5362
+ { runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
5363
+ { runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
5364
+ { runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
5365
+ { runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
5366
+ {
5367
+ // `-p no:<plugin>` disables collection when the plugin is the collector
5368
+ // (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
5369
+ runner: /^pytest\b/,
5370
+ flags: /\s-k(?:=|\s)|\s-m(?:=|\s)|\s--deselect(?:=|\s)|\s--ignore(?:-glob)?(?:=|\s)|\s-p\s*no:(?!(?:cacheprovider|warnings|faulthandler|randomly|logging|legacypath|pastebin|stepwise|tmpdir|junitxml|terminal|terminalreporter)(?=\s|$))|\s--collect-only(?=\s|$)|\s--co(?=\s|$)/
5371
+ },
5372
+ { runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
5373
+ { runner: /^go test\b/, flags: /\s-run(?:=|\s)|\s-skip(?:=|\s)/ }
5374
+ ];
5375
+ COVERAGE_FLOOR = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
5376
+ COMMAND_PREFIX = /^(?:(?:[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S*)|sudo|env|nice|time|exec|command|nohup|xvfb-run|dbus-launch|cross-env|dotenv|poetry\s+run|pipenv\s+run|uv\s+run|hatch\s+run|pdm\s+run|bundle\s+exec)\s+(?:-{1,2}\S+\s+)*)*/;
5377
+ CHECK_HEAD = /^(?:(?:test|tests|lint|typecheck|type-check|coverage)(?::\S+)?(?=\s|$)|(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint)\b|node --test\b|biome (?:ci|check|lint)\b|deno (?:test|lint|check)\b|ruff check\b|(?:make|cargo|go) test\b|gradle\w* (?:test|check)\b)/;
5378
+ PATH_SHAPED = /\/|\.(?:test|spec)\./;
5379
+ NARROWING_BY_RUNNER = {
5380
+ "*": [/--shard\b/],
5381
+ jest: [/--root\b/, /--rootDir\b/, /--testMatch\b/, /--testRegex\b/, /--modulePathIgnorePatterns\b/, /--selectProjects\b/, /--ignoreProjects\b/, /--filter\b/],
5382
+ vitest: [/--grep\b/],
5383
+ mocha: [/--grep\b/, /(?:^|\s)-g(?:\s|=)/, /--ignore\b/, /(?:^|\s)-f(?:\s|=)/, /--fgrep\b/],
5384
+ pytest: [/(?:^|\s)-k(?:\s|=)/, /(?:^|\s)-m(?:\s|=)/, /--deselect\b/, /--ignore\b/, /--ignore-glob\b/, /--last-failed\b/, /(?:^|\s)--lf(?:\s|$)/, /--collect-only\b/, /(?:^|\s)--co(?:\s|$)/],
5385
+ go: [/(?:^|\s)-run(?:\s|=)/, /(?:^|\s)-skip(?:\s|=)/],
5386
+ cargo: [],
5387
+ node: [/--test-name-pattern\b/, /--test-skip-pattern\b/]
5388
+ };
5389
+ CARGO_SKIP = /\s--\s+(?:\S+\s+)*--skip\b/;
5390
+ MASKING_FLAGS = [/--passWithNoTests\b/, /--coverage=false\b/, /--coverageThreshold\b/, /--if-present\b/];
5391
+ GATE_FLAGS = [/--check-coverage\b/];
5392
+ CONFIG_FLAG = /(?:^|\s)(--config|-c)(?:=|\s+)(['"]?)([^\s'"]+)\2(?=\s|$)/;
5393
+ PRINTER = /^(?:echo|printf|true|false|:|cat|exit|return|sleep|test|\[|node|git|curl|wget)(?=\s|$)/;
5394
+ PYTHON_M = /^python\d?(?:\.\d+)?\s+-m\s+(?=pytest\b)/;
5395
+ NODE_BIN = /^node(?:\s+-\S+)*\s+\S*node_modules\/(?:\.bin\/)?(jest|vitest|mocha|ava)\b\S*/;
5396
+ BIN_OF = /\$\((?:yarn|npm|pnpm)\s+bin\s+(\w+)\)|\$\((?:yarn|npm|pnpm)\s+bin\)\/(\w+)/g;
5397
+ RUNNER_NAME = /^(jest|vitest|mocha|pytest|go|cargo|node)\b/;
5398
+ spelled = (re, s) => {
5399
+ const m = s.match(re);
5400
+ return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
5401
+ };
5402
+ FAILS_LOUDLY = /^(?:exit\s+(?:[1-9]\d*)|false)\s*$/;
5403
+ }
5404
+ });
5405
+
5406
+ // src/detectors/ci-tampering.ts
5407
+ function isActiveWorkflow(path) {
5408
+ return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
5409
+ }
5410
+ function commandCore(line) {
5411
+ return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
5412
+ }
5180
5413
  function expressionOf(raw) {
5181
5414
  const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
5182
5415
  const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
@@ -5425,7 +5658,7 @@ function ancestorOf(lines, i) {
5425
5658
  while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
5426
5659
  return h >= 0 ? h : null;
5427
5660
  }
5428
- var RULE6, USES, CHECK, YAML_KEY, INVOKES_TOOL, INVOKES_SCRIPT, invokesCheck, EXPR_TOKEN, MATRIX_TOKEN, MATRIX_FLAG, usesRef, SUITE_NARROWING_FLAGS, NEUTRALISING_SUFFIX, TIMEOUT_WRAP, RUNNER_NARROWINGS, COVERAGE_FLOOR, COMMAND_PREFIX, CHECK_HEAD, PATH_SHAPED, uncommented, TRUTHY, truthy, 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;
5661
+ 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;
5429
5662
  var init_ci_tampering = __esm({
5430
5663
  "src/detectors/ci-tampering.ts"() {
5431
5664
  "use strict";
@@ -5435,60 +5668,22 @@ var init_ci_tampering = __esm({
5435
5668
  init_files();
5436
5669
  init_hook_wiring();
5437
5670
  init_repo();
5671
+ init_gh_expression();
5672
+ init_invocation();
5673
+ init_invocation();
5438
5674
  RULE6 = "ci-tampering";
5439
5675
  USES = /^\s*-\s*uses:/;
5440
5676
  CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
5441
5677
  YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
5442
- INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint|node\s+--test|biome\s+(?:ci|check|lint)|deno\s+(?:test|lint|check)|ruff\s+check)\b/;
5443
- 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/;
5444
- invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line) || INVOKES_TOOL.test(commandCore(line));
5445
- EXPR_TOKEN = "__expr__";
5446
- MATRIX_TOKEN = "__matrix__";
5447
- MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
5448
- usesRef = (core) => core.replace(/@.*$/, "");
5449
- SUITE_NARROWING_FLAGS = /--testPathIgnorePatterns\b|--testPathPattern\b|--testNamePattern\b|(?:^|\s)-t\s|--onlyChanged\b|--changed\b|--findRelatedTests\b|--exclude\b|--dir\b|--project\b/;
5450
- NEUTRALISING_SUFFIX = new RegExp(
5451
- `\\|\\||;|(?<!&)&(?!&)|\\|(?!\\|)|--passWithNoTests\\b|--coverage=false\\b|--coverageThreshold\\b|${SUITE_NARROWING_FLAGS.source}`
5452
- );
5453
- TIMEOUT_WRAP = /^timeout\s+(?:-{1,2}\S+\s+)*\d\S*\s+(.+)$/;
5454
- RUNNER_NARROWINGS = [
5455
- {
5456
- // a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
5457
- // `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
5458
- runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
5459
- flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
5460
- },
5461
- {
5462
- runner: /^(?:jest|vitest)\b/,
5463
- flags: /\s--shard(?:=|\s)|\s--root(?:Dir)?(?:=|\s)|\s--testMatch(?:=|\s)|\s--testRegex(?:=|\s)|\s--modulePathIgnorePatterns(?:=|\s)|\s--selectProjects(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)/
5464
- },
5465
- { runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
5466
- { runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
5467
- { runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
5468
- { runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
5469
- {
5470
- // `-p no:<plugin>` disables collection when the plugin is the collector
5471
- // (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
5472
- runner: /^pytest\b/,
5473
- flags: /\s-k(?:=|\s)|\s-m(?:=|\s)|\s--deselect(?:=|\s)|\s--ignore(?:-glob)?(?:=|\s)|\s-p\s*no:(?!(?:cacheprovider|warnings|faulthandler|randomly|logging|legacypath|pastebin|stepwise|tmpdir|junitxml|terminal|terminalreporter)(?=\s|$))|\s--collect-only(?=\s|$)|\s--co(?=\s|$)/
5474
- },
5475
- { runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
5476
- { runner: /^go test\b/, flags: /\s-run(?:=|\s)|\s-skip(?:=|\s)/ }
5477
- ];
5478
- COVERAGE_FLOOR = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
5479
- COMMAND_PREFIX = /^(?:(?:[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S*)|sudo|env|nice|time|exec|command|nohup|xvfb-run|dbus-launch|cross-env|dotenv|poetry\s+run|pipenv\s+run|uv\s+run|hatch\s+run|pdm\s+run|bundle\s+exec)\s+(?:-{1,2}\S+\s+)*)*/;
5480
- CHECK_HEAD = /^(?:(?:test|tests|lint|typecheck|type-check|coverage)(?::\S+)?(?=\s|$)|(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint)\b|node --test\b|biome (?:ci|check|lint)\b|deno (?:test|lint|check)\b|ruff check\b|(?:make|cargo|go) test\b|gradle\w* (?:test|check)\b)/;
5481
- PATH_SHAPED = /\/|\.(?:test|spec)\./;
5678
+ invokesCheck2 = (line) => invokesCheck(line) || INVOKES_TOOL.test(commandCore(line));
5482
5679
  uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
5483
- TRUTHY = Symbol("truthy");
5484
- truthy = (v) => v === TRUTHY ? true : typeof v === "string" ? v !== "" : typeof v === "number" ? v !== 0 : v === true;
5485
5680
  SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
5486
5681
  indentOf = (l) => l.length - l.trimStart().length;
5487
5682
  STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
5488
5683
  REPORTER_ACTION = /^\s*-?\s*uses:\s*\S*?(?:[\w-]*-reporter|[\w-]*-results?[\w-]*|upload-[\w-]*|codecov[\w-]*|coveralls[\w-]*)(?:@|\s|$)/i;
5489
5684
  usesCheck = (l) => USES.test(l) && CHECK.test(l) && !REPORTER_ACTION.test(l);
5490
- stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck(l) || usesCheck(l));
5491
- jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
5685
+ stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l));
5686
+ jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
5492
5687
  NEUTRALISERS = [
5493
5688
  { re: /^\s*set\s+\+e\b/, what: "set +e \u2014 a failing check no longer fails the step" },
5494
5689
  { re: /^\s*exit\s+0\s*(?:$|[;#])/, what: "exit 0 \u2014 the step ends green whatever the check did" },
@@ -5574,7 +5769,7 @@ var init_ci_tampering = __esm({
5574
5769
  }
5575
5770
  if (afterLines) {
5576
5771
  for (const b of runBlocks(afterLines)) {
5577
- if (!b.body.some(invokesCheck)) continue;
5772
+ if (!b.body.some(invokesCheck2)) continue;
5578
5773
  const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
5579
5774
  for (let k = 0; k < b.body.length; k++) {
5580
5775
  const idx = b.start + 1 + k;
@@ -5621,7 +5816,7 @@ var init_ci_tampering = __esm({
5621
5816
  const d = defaultsOf(afterLines, i);
5622
5817
  if (d === null) continue;
5623
5818
  const job = jobOf(afterLines, d);
5624
- if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck(x) || usesCheck(x))) continue;
5819
+ if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
5625
5820
  }
5626
5821
  out3.push(
5627
5822
  makeFinding(RULE6, policy, {
@@ -5678,7 +5873,7 @@ var init_ci_tampering = __esm({
5678
5873
  for (const l of removedLines(c)) {
5679
5874
  if (/^\s*#/.test(l.content)) continue;
5680
5875
  const isUsesLine = usesCheck(l.content);
5681
- const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
5876
+ const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck2(l.content);
5682
5877
  if (!isUsesLine && !isCheckCommand) continue;
5683
5878
  const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
5684
5879
  if (s.state === "kept") continue;
@@ -7324,33 +7519,43 @@ function excludesOtherRunner(flag, cmd) {
7324
7519
  const values = [...cmd.matchAll(/--exclude(?:=|\s+)(['"]?)(\S+?)\1(?=\s|$)/g)].map((m) => m[2]);
7325
7520
  return values.length > 0 && values.every((v) => OTHER_RUNNER_DIR.test(v.replace(/^\.\//, "").replace(/(?:\/\*+)*\/?$/, "") + "/"));
7326
7521
  }
7327
- function scriptNarrowings(before, after, path) {
7522
+ function scriptsOf(src) {
7523
+ try {
7524
+ const pkg = JSON.parse(src ?? "{}");
7525
+ const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
7526
+ return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
7527
+ } catch {
7528
+ return {};
7529
+ }
7530
+ }
7531
+ function scriptWeakenings(before, after, path, policy, addedPaths) {
7328
7532
  if ((path.split("/").pop() ?? path) !== "package.json") return [];
7329
- const scriptsOf = (src) => {
7330
- try {
7331
- const pkg = JSON.parse(src ?? "{}");
7332
- const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
7333
- return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
7334
- } catch {
7335
- return {};
7336
- }
7337
- };
7338
7533
  const was = scriptsOf(before);
7339
7534
  const now = scriptsOf(after);
7535
+ const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/") + 1) : "";
7536
+ const configReviewed = (file) => {
7537
+ const f = dir + file.replace(/^\.\//, "");
7538
+ return isProtected(f, policy, "config") && !addedPaths.has(f);
7539
+ };
7540
+ const relocated = (kind) => Object.keys(now).some((n) => !(n in was) && checkKinds(now[n]).has(kind));
7340
7541
  const out3 = [];
7341
- for (const [name, cmd] of Object.entries(now)) {
7342
- if (!WHOLE_SUITE_SCRIPT.test(name) || !/\b(?:jest|vitest)\b/.test(cmd)) continue;
7343
- const prev = was[name] ?? "";
7344
- const flags = new RegExp(SUITE_NARROWING_FLAGS.source, "g");
7345
- for (const m of cmd.matchAll(flags)) {
7346
- const flag = m[0].trim();
7347
- if (!flag) continue;
7348
- const one = new RegExp(SUITE_NARROWING_FLAGS.source);
7349
- if (one.test(prev) && prev.includes(flag)) continue;
7350
- if (excludesOtherRunner(flag, cmd)) continue;
7351
- out3.push({ script: name, flag, before: prev, after: cmd });
7352
- break;
7542
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])) {
7543
+ const whole = WHOLE_SUITE_SCRIPT.test(name);
7544
+ if (!whole && !SLICE_SCRIPT.test(name)) continue;
7545
+ const prev = was[name];
7546
+ const cmd = now[name];
7547
+ if (prev === cmd) continue;
7548
+ if (prev === void 0 && !whole) continue;
7549
+ if (cmd === void 0) {
7550
+ if (name !== "pretest" || prev === void 0) continue;
7551
+ const gone = [...checkKinds(prev)].filter((k) => !relocated(k));
7552
+ if (gone.length === 0) continue;
7553
+ out3.push({ script: name, weakening: { state: "removed", kind: gone[0], what: "the script is gone" }, before: prev, after: "" });
7554
+ continue;
7353
7555
  }
7556
+ const w = invocationWeakening(prev ?? "", cmd, void 0, { configReviewed, excuseFlag: excludesOtherRunner, sliceByDesign: !whole, relocated });
7557
+ if (w.state === "kept") continue;
7558
+ out3.push({ script: name, weakening: w, before: prev ?? "", after: cmd });
7354
7559
  }
7355
7560
  return out3;
7356
7561
  }
@@ -7412,7 +7617,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
7412
7617
  })
7413
7618
  );
7414
7619
  }
7415
- var RULE8, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SED_INPLACE, testDeletion;
7620
+ var RULE8, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
7416
7621
  var init_test_deletion = __esm({
7417
7622
  "src/detectors/test-deletion.ts"() {
7418
7623
  "use strict";
@@ -7423,7 +7628,7 @@ var init_test_deletion = __esm({
7423
7628
  init_files();
7424
7629
  init_repo();
7425
7630
  init_suite_config();
7426
- init_ci_tampering();
7631
+ init_invocation();
7427
7632
  init_narrow();
7428
7633
  RULE8 = "test-deletion";
7429
7634
  JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
@@ -7441,7 +7646,8 @@ var init_test_deletion = __esm({
7441
7646
  isEachOf = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
7442
7647
  isDescribeEach = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
7443
7648
  OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
7444
- WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
7649
+ WHOLE_SUITE_SCRIPT = /^(?:test|test:ci|check|pretest)$/;
7650
+ SLICE_SCRIPT = /^test:(?:unit|integration)$/;
7445
7651
  SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
7446
7652
  testDeletion = {
7447
7653
  id: RULE8,
@@ -7450,6 +7656,7 @@ var init_test_deletion = __esm({
7450
7656
  run(changes, policy, _view, ctx) {
7451
7657
  const out3 = [];
7452
7658
  const isSpec = (p) => isProtected(p, policy, "tests") && !isProtected(p, policy, "snapshots");
7659
+ const addedPaths = new Set(changes.filter((c) => c.kind === "file" && c.op === "add").map((c) => c.path));
7453
7660
  const addedTestLines = /* @__PURE__ */ new Set();
7454
7661
  let addedTestBlocks = 0;
7455
7662
  for (const c of changes) {
@@ -7529,13 +7736,20 @@ var init_test_deletion = __esm({
7529
7736
  })
7530
7737
  );
7531
7738
  }
7532
- for (const n of scriptNarrowings(c.before, c.after, c.path)) {
7739
+ for (const n of scriptWeakenings(c.before, c.after, c.path, policy, addedPaths)) {
7740
+ const w = n.weakening;
7741
+ const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
7533
7742
  out3.push(
7534
- makeFinding(RULE8, policy, {
7743
+ w.state === "removed" ? makeFinding(RULE8, policy, {
7535
7744
  file: c.path,
7536
- message: `The test script now narrows the suite: ${n.flag} added to scripts.${n.script}.`,
7537
- evidence: `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${JSON.stringify(n.after)}`,
7538
- remediation: "Keep the test script running the whole suite; a runner flag that selects fewer specs removes the rest from every run."
7745
+ message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
7746
+ evidence,
7747
+ 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."
7748
+ }) : makeFinding(RULE8, policy, {
7749
+ file: c.path,
7750
+ message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
7751
+ evidence,
7752
+ remediation: "Keep the test script running the whole suite and letting it decide; a masked status, a cut-short run or a runner flag that selects fewer specs removes the rest from every run."
7539
7753
  })
7540
7754
  );
7541
7755
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.10",
3
+ "version": "2.23.11",
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",