tamperward 2.23.10 → 2.23.12

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 +509 -228
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1026,7 +1026,7 @@ var init_trusted = __esm({
1026
1026
 
1027
1027
  // src/repo-context.ts
1028
1028
  import { execFileSync as execFileSync2 } from "node:child_process";
1029
- import { existsSync as existsSync2 } from "node:fs";
1029
+ import { accessSync, constants as constants2, existsSync as existsSync2, statSync } from "node:fs";
1030
1030
  import { isAbsolute as isAbsolute3, resolve } from "node:path";
1031
1031
  function rev(cwd, flag) {
1032
1032
  try {
@@ -1059,6 +1059,47 @@ function repoContext(cwd) {
1059
1059
  function repoRoot(cwd) {
1060
1060
  return repoContext(cwd)?.root ?? cwd;
1061
1061
  }
1062
+ function outsideRepository(cwd) {
1063
+ const key2 = resolve(cwd);
1064
+ let isDir;
1065
+ try {
1066
+ isDir = statSync(key2).isDirectory();
1067
+ } catch (e) {
1068
+ return `cwd ${key2} cannot be read: ${errCode2(e)}`;
1069
+ }
1070
+ if (!isDir) return `cwd ${key2} is not a directory`;
1071
+ try {
1072
+ accessSync(key2, constants2.R_OK | constants2.X_OK);
1073
+ } catch (e) {
1074
+ return `cwd ${key2} cannot be read: ${errCode2(e)}`;
1075
+ }
1076
+ try {
1077
+ execFileSync2("git", ["rev-parse", "--git-dir"], {
1078
+ cwd: key2,
1079
+ encoding: "utf8",
1080
+ stdio: ["ignore", "pipe", "pipe"]
1081
+ });
1082
+ } catch (e) {
1083
+ const detail = errDetail(e);
1084
+ if (/not a git repository/i.test(detail)) return null;
1085
+ return `git could not resolve cwd ${key2}: ${detail || "unknown failure"}`;
1086
+ }
1087
+ return `cwd ${key2} lies in a repository with no working tree the gate can judge`;
1088
+ }
1089
+ function errCode2(e) {
1090
+ if (typeof e === "object" && e !== null) {
1091
+ const code2 = Reflect.get(e, "code");
1092
+ if (typeof code2 === "string") return code2;
1093
+ }
1094
+ return e instanceof Error ? e.message : String(e);
1095
+ }
1096
+ function errDetail(e) {
1097
+ if (typeof e === "object" && e !== null) {
1098
+ const stderr = Reflect.get(e, "stderr");
1099
+ if (typeof stderr === "string" && stderr.trim()) return stderr.replace(/\s+/g, " ").trim();
1100
+ }
1101
+ return errCode2(e);
1102
+ }
1062
1103
  var cache, CACHE_MAX;
1063
1104
  var init_repo_context = __esm({
1064
1105
  "src/repo-context.ts"() {
@@ -4890,13 +4931,7 @@ var init_hook_wiring = __esm({
4890
4931
  }
4891
4932
  });
4892
4933
 
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
- }
4934
+ // src/detectors/gh-expression.ts
4900
4935
  function foldExpressions(s) {
4901
4936
  return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
4902
4937
  const v = foldConst(e.trim());
@@ -4904,90 +4939,6 @@ function foldExpressions(s) {
4904
4939
  return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
4905
4940
  });
4906
4941
  }
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
4942
  function lex(src) {
4992
4943
  const out3 = [];
4993
4944
  let i = 0;
@@ -5177,6 +5128,329 @@ function callConst(name, args) {
5177
5128
  return void 0;
5178
5129
  }
5179
5130
  }
5131
+ var EXPR_TOKEN, MATRIX_TOKEN, TRUTHY, truthy;
5132
+ var init_gh_expression = __esm({
5133
+ "src/detectors/gh-expression.ts"() {
5134
+ "use strict";
5135
+ EXPR_TOKEN = "__expr__";
5136
+ MATRIX_TOKEN = "__matrix__";
5137
+ TRUTHY = Symbol("truthy");
5138
+ truthy = (v) => v === TRUTHY ? true : typeof v === "string" ? v !== "" : typeof v === "number" ? v !== 0 : v === true;
5139
+ }
5140
+ });
5141
+
5142
+ // src/detectors/invocation.ts
5143
+ function canonical(core) {
5144
+ let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
5145
+ s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
5146
+ s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
5147
+ s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
5148
+ s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
5149
+ s = s.replace(/^exec\s+/, "");
5150
+ return s.replace(/\s+--\s*$/, "").trim();
5151
+ }
5152
+ function runnerNarrowed(cmd, args) {
5153
+ return runnerNarrowing(cmd, args) !== null;
5154
+ }
5155
+ function runnerNarrowing(cmd, args) {
5156
+ const t = RUNNER_NARROWINGS.find((x) => x.runner.test(cmd));
5157
+ const m = t ? (" " + args.trim()).match(t.flags) : null;
5158
+ return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
5159
+ }
5160
+ function coverageFloorLowered(removed, added) {
5161
+ const was = removed.match(COVERAGE_FLOOR);
5162
+ const now = added.match(COVERAGE_FLOOR);
5163
+ return was !== null && now !== null && Number(now[1]) < Number(was[1]);
5164
+ }
5165
+ function inCommandPosition(core) {
5166
+ return canonical(core).split(/\s*(?:&&|\|\||;|\|)\s*/).some((seg) => CHECK_HEAD.test(canonical(seg.replace(COMMAND_PREFIX, ""))));
5167
+ }
5168
+ function narrowedByPositional(args) {
5169
+ return positionalOf(args) !== null;
5170
+ }
5171
+ function positionalOf(args) {
5172
+ const toks = args.trim().split(/\s+/).filter(Boolean);
5173
+ for (let i = 0; i < toks.length; i++) {
5174
+ const t = toks[i];
5175
+ if (t === "--" || t.startsWith("-")) continue;
5176
+ const prev = toks[i - 1];
5177
+ if (prev && (prev.startsWith("-") && prev !== "--" && !prev.includes("=") || /[<>]/.test(prev))) continue;
5178
+ if (/[<>|;&]/.test(t)) continue;
5179
+ if (PATH_SHAPED.test(t)) return t;
5180
+ }
5181
+ return null;
5182
+ }
5183
+ function checkKind(core) {
5184
+ if (!invokesCheck(core)) return null;
5185
+ const s = canonical(core);
5186
+ if (/\btamperward\b/.test(s)) return "gate";
5187
+ if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
5188
+ if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
5189
+ return "test";
5190
+ }
5191
+ function survives(removedCore, isUses, afterCores, addedCores) {
5192
+ if (isUses) {
5193
+ const ref = usesRef(removedCore);
5194
+ return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
5195
+ }
5196
+ const r = canonical(removedCore);
5197
+ const kind = checkKind(removedCore);
5198
+ const narrowed = (args) => kind === "test" && narrowedByPositional(args);
5199
+ let neutralised;
5200
+ for (const raw of afterCores) {
5201
+ const a = canonical(raw);
5202
+ if (a === r) return { state: "kept" };
5203
+ if (a.startsWith(r + " ")) {
5204
+ const rest = a.slice(r.length);
5205
+ if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest) || runnerNarrowed(a, rest)) neutralised ??= raw;
5206
+ else return { state: "kept" };
5207
+ } else if (r.startsWith(a + " ") && invokesCheck(a)) {
5208
+ return { state: "kept" };
5209
+ } else {
5210
+ const w = a.match(TIMEOUT_WRAP);
5211
+ if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
5212
+ }
5213
+ }
5214
+ let respelled = false;
5215
+ for (const raw of addedCores) {
5216
+ if (!kind || checkKind(raw) !== kind) continue;
5217
+ const a = canonical(raw);
5218
+ if (a.match(TIMEOUT_WRAP)) {
5219
+ neutralised ??= raw;
5220
+ continue;
5221
+ }
5222
+ if (!inCommandPosition(raw)) continue;
5223
+ if (coverageFloorLowered(r, a)) {
5224
+ neutralised ??= raw;
5225
+ continue;
5226
+ }
5227
+ const args = a.slice(a.indexOf(" ") + 1 || a.length);
5228
+ if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
5229
+ else respelled = true;
5230
+ }
5231
+ if (respelled) return { state: "kept" };
5232
+ return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
5233
+ }
5234
+ function unwrap(seg) {
5235
+ const s = seg.trim().replace(PYTHON_M, "").replace(NODE_BIN, "$1").replace(BIN_OF, "$1$2");
5236
+ if (invokesCheck(s) || PRINTER.test(s) || /^["']/.test(s)) return s;
5237
+ const toks = s.split(/\s+/);
5238
+ for (let i = 1; i < toks.length && i <= 16; i++) {
5239
+ const rest = toks.slice(i).join(" ");
5240
+ if (invokesCheck(rest)) return rest;
5241
+ }
5242
+ return s;
5243
+ }
5244
+ function scriptKind3(inner) {
5245
+ const kind = checkKind(inner);
5246
+ if (!kind) return null;
5247
+ const head = canonical(inner).match(/^(tests?|lint|type-?check|coverage)(?=[\s:]|$)/i)?.[1];
5248
+ if (!head) return kind;
5249
+ return /^type/i.test(head) ? "types" : /^lint/i.test(head) ? "lint" : "test";
5250
+ }
5251
+ function runnerOfSegment(canon) {
5252
+ const m = canon.match(RUNNER_NAME);
5253
+ if (!m) return null;
5254
+ const n = m[1];
5255
+ return n === "jest" || n === "vitest" || n === "mocha" || n === "pytest" || n === "go" || n === "cargo" ? n : "node";
5256
+ }
5257
+ function statements(raw) {
5258
+ const out3 = [];
5259
+ let single = false;
5260
+ let double = false;
5261
+ let last = 0;
5262
+ for (let i = 0; i < raw.length; i++) {
5263
+ const ch = raw[i];
5264
+ if (ch === "\\" && !single) {
5265
+ i++;
5266
+ continue;
5267
+ }
5268
+ if (ch === "'" && !double) {
5269
+ single = !single;
5270
+ continue;
5271
+ }
5272
+ if (ch === '"' && !single) {
5273
+ double = !double;
5274
+ continue;
5275
+ }
5276
+ if (single || double) continue;
5277
+ const prev = raw[i - 1];
5278
+ const next = raw[i + 1];
5279
+ let op = null;
5280
+ if (ch === "&" && next === "&") op = "&&";
5281
+ else if (ch === "|" && next === "|") op = "||";
5282
+ else if (ch === "|" && next === "&") op = "|&";
5283
+ else if (ch === ";" || ch === "\n") op = ";";
5284
+ else if (ch === "|" && prev !== ">") op = "|";
5285
+ else if (ch === "&" && prev !== ">" && next !== ">") op = "&";
5286
+ if (!op) continue;
5287
+ out3.push({ seg: raw.slice(last, i).trim(), op });
5288
+ i += op.length - 1;
5289
+ last = i + 1;
5290
+ }
5291
+ out3.push({ seg: raw.slice(last).trim(), op: null });
5292
+ return out3.filter((s) => s.seg !== "");
5293
+ }
5294
+ function descriptors(cmd, runner, opts) {
5295
+ const out3 = [];
5296
+ const stmts = statements(cmd);
5297
+ for (let i = 0; i < stmts.length; i++) {
5298
+ const { seg, op } = stmts[i];
5299
+ const wrapped = seg.match(TIMEOUT_WRAP);
5300
+ const inner = unwrap(wrapped ? wrapped[1] : seg);
5301
+ if (!invokesCheck(inner)) continue;
5302
+ const kind = scriptKind3(inner);
5303
+ const canon = canonical(inner);
5304
+ const rn = runner ?? runnerOfSegment(canon);
5305
+ const args = canon.includes(" ") ? " " + canon.slice(canon.indexOf(" ") + 1) : " ";
5306
+ if (wrapped) out3.push({ key: "timeout", what: "a timeout wrapper", direction: "added" });
5307
+ const following = stmts[i + 1]?.seg ?? "";
5308
+ if (op === "||" || op === "|" || op === "&" || op === ";" && !invokesCheck(unwrap(following))) {
5309
+ out3.push({ key: `op:${op}`, what: `${op} ${following.split(/\s+/).slice(0, 2).join(" ")}`.trim(), direction: "added" });
5310
+ }
5311
+ for (const re of MASKING_FLAGS) {
5312
+ const f = spelled(re, args);
5313
+ if (f) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
5314
+ }
5315
+ for (const re of GATE_FLAGS) {
5316
+ const f = spelled(re, " " + seg);
5317
+ if (f) out3.push({ key: `gate:${f}`, what: f, direction: "removed" });
5318
+ }
5319
+ if (kind !== "test" || opts.sliceByDesign || opts.extended?.has(kind)) continue;
5320
+ const flags = [SUITE_NARROWING_FLAGS, ...NARROWING_BY_RUNNER["*"], ...rn ? NARROWING_BY_RUNNER[rn] : []];
5321
+ for (const re of flags) {
5322
+ const f = spelled(re, args);
5323
+ if (f && !opts.excuseFlag?.(f, canon)) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
5324
+ }
5325
+ if (rn === "cargo" && CARGO_SKIP.test(args)) out3.push({ key: "flag:-- --skip", what: "-- --skip", direction: "added" });
5326
+ const shared = runnerNarrowing(canon, args);
5327
+ if (shared && !opts.excuseFlag?.(shared, canon)) out3.push({ key: `flag:${shared}`, what: shared, direction: "added" });
5328
+ if (opts.fresh) continue;
5329
+ const cfg = args.match(CONFIG_FLAG);
5330
+ if (cfg && !opts.configReviewed?.(cfg[3])) out3.push({ key: `config:${cfg[3]}`, what: `${cfg[1]} ${cfg[3]}`, direction: "added" });
5331
+ const pos = positionalOf(args);
5332
+ if (pos) out3.push({ key: "positional", what: `a path positional (${pos})`, direction: "added" });
5333
+ }
5334
+ return out3;
5335
+ }
5336
+ function checkStatements(cmd) {
5337
+ const out3 = [];
5338
+ for (const seg of segments(cmd)) {
5339
+ const w = seg.match(TIMEOUT_WRAP);
5340
+ const inner = unwrap(w ? w[1] : seg);
5341
+ const kind = scriptKind3(inner);
5342
+ if (kind) out3.push({ kind, canon: canonical(inner) });
5343
+ }
5344
+ return out3;
5345
+ }
5346
+ function checkKinds(cmd) {
5347
+ return new Set(checkStatements(cmd).map((s) => s.kind));
5348
+ }
5349
+ function failsLoudly(cmd) {
5350
+ const last = segments(cmd).pop();
5351
+ return last !== void 0 && FAILS_LOUDLY.test(last.trim());
5352
+ }
5353
+ function invocationWeakening(before, after, runner, opts = {}) {
5354
+ const was = checkKinds(before);
5355
+ const now = checkKinds(after);
5356
+ if (now.size === 0 && failsLoudly(after)) return { state: "kept" };
5357
+ for (const k of was) {
5358
+ if (now.has(k) || opts.relocated?.(k)) continue;
5359
+ const instead = [...now].filter((x) => !was.has(x));
5360
+ return { state: "removed", kind: k, what: instead.length ? `runs a ${instead.join("/")} check instead` : now.size ? "the check is gone" : "no check is run" };
5361
+ }
5362
+ const survived = new Set(checkStatements(before).map((s) => s.canon));
5363
+ const extended = new Set(checkStatements(after).filter((s) => survived.has(s.canon)).map((s) => s.kind));
5364
+ const read = { ...opts, fresh: was.size === 0, extended };
5365
+ const beforeDs = descriptors(before, runner, read);
5366
+ const afterDs = descriptors(after, runner, read);
5367
+ const seenBefore = new Set(beforeDs.map((d) => d.key));
5368
+ const added = afterDs.find((d) => d.direction === "added" && !seenBefore.has(d.key));
5369
+ if (added) return { state: "neutralised", what: added.what, direction: "added" };
5370
+ if (coverageFloorLowered(before, after)) return { state: "neutralised", what: "a lowered --cov-fail-under", direction: "added" };
5371
+ const seenAfter = new Set(afterDs.map((d) => d.key));
5372
+ const dropped = beforeDs.find((d) => d.direction === "removed" && !seenAfter.has(d.key));
5373
+ if (dropped) return { state: "neutralised", what: dropped.what, direction: "removed" };
5374
+ return { state: "kept" };
5375
+ }
5376
+ 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;
5377
+ var init_invocation = __esm({
5378
+ "src/detectors/invocation.ts"() {
5379
+ "use strict";
5380
+ init_gh_expression();
5381
+ init_command();
5382
+ 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/;
5383
+ 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/;
5384
+ invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
5385
+ MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
5386
+ usesRef = (core) => core.replace(/@.*$/, "");
5387
+ SUITE_NARROWING_FLAGS = /--testPathIgnorePatterns\b|--testPathPattern\b|--testNamePattern\b|(?:^|\s)-t\s|--onlyChanged\b|--changed\b|--findRelatedTests\b|--exclude\b|--dir\b|--project\b/;
5388
+ NEUTRALISING_SUFFIX = new RegExp(
5389
+ `\\|\\||;|(?<!&)&(?!&)|\\|(?!\\|)|--passWithNoTests\\b|--coverage=false\\b|--coverageThreshold\\b|${SUITE_NARROWING_FLAGS.source}`
5390
+ );
5391
+ TIMEOUT_WRAP = /^timeout\s+(?:-{1,2}\S+\s+)*\d\S*\s+(.+)$/;
5392
+ RUNNER_NARROWINGS = [
5393
+ {
5394
+ // a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
5395
+ // `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
5396
+ runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
5397
+ flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
5398
+ },
5399
+ {
5400
+ runner: /^(?:jest|vitest)\b/,
5401
+ 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)/
5402
+ },
5403
+ { runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
5404
+ { runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
5405
+ { runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
5406
+ { runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
5407
+ {
5408
+ // `-p no:<plugin>` disables collection when the plugin is the collector
5409
+ // (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
5410
+ runner: /^pytest\b/,
5411
+ 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|$)/
5412
+ },
5413
+ { runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
5414
+ { runner: /^go test\b/, flags: /\s-run(?:=|\s)|\s-skip(?:=|\s)/ }
5415
+ ];
5416
+ COVERAGE_FLOOR = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
5417
+ 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+)*)*/;
5418
+ 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)/;
5419
+ PATH_SHAPED = /\/|\.(?:test|spec)\./;
5420
+ NARROWING_BY_RUNNER = {
5421
+ "*": [/--shard\b/],
5422
+ jest: [/--root\b/, /--rootDir\b/, /--testMatch\b/, /--testRegex\b/, /--modulePathIgnorePatterns\b/, /--selectProjects\b/, /--ignoreProjects\b/, /--filter\b/],
5423
+ vitest: [/--grep\b/],
5424
+ mocha: [/--grep\b/, /(?:^|\s)-g(?:\s|=)/, /--ignore\b/, /(?:^|\s)-f(?:\s|=)/, /--fgrep\b/],
5425
+ 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|$)/],
5426
+ go: [/(?:^|\s)-run(?:\s|=)/, /(?:^|\s)-skip(?:\s|=)/],
5427
+ cargo: [],
5428
+ node: [/--test-name-pattern\b/, /--test-skip-pattern\b/]
5429
+ };
5430
+ CARGO_SKIP = /\s--\s+(?:\S+\s+)*--skip\b/;
5431
+ MASKING_FLAGS = [/--passWithNoTests\b/, /--coverage=false\b/, /--coverageThreshold\b/, /--if-present\b/];
5432
+ GATE_FLAGS = [/--check-coverage\b/];
5433
+ CONFIG_FLAG = /(?:^|\s)(--config|-c)(?:=|\s+)(['"]?)([^\s'"]+)\2(?=\s|$)/;
5434
+ PRINTER = /^(?:echo|printf|true|false|:|cat|exit|return|sleep|test|\[|node|git|curl|wget)(?=\s|$)/;
5435
+ PYTHON_M = /^python\d?(?:\.\d+)?\s+-m\s+(?=pytest\b)/;
5436
+ NODE_BIN = /^node(?:\s+-\S+)*\s+\S*node_modules\/(?:\.bin\/)?(jest|vitest|mocha|ava)\b\S*/;
5437
+ BIN_OF = /\$\((?:yarn|npm|pnpm)\s+bin\s+(\w+)\)|\$\((?:yarn|npm|pnpm)\s+bin\)\/(\w+)/g;
5438
+ RUNNER_NAME = /^(jest|vitest|mocha|pytest|go|cargo|node)\b/;
5439
+ spelled = (re, s) => {
5440
+ const m = s.match(re);
5441
+ return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
5442
+ };
5443
+ FAILS_LOUDLY = /^(?:exit\s+(?:[1-9]\d*)|false)\s*$/;
5444
+ }
5445
+ });
5446
+
5447
+ // src/detectors/ci-tampering.ts
5448
+ function isActiveWorkflow(path) {
5449
+ return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
5450
+ }
5451
+ function commandCore(line) {
5452
+ return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
5453
+ }
5180
5454
  function expressionOf(raw) {
5181
5455
  const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
5182
5456
  const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
@@ -5425,7 +5699,7 @@ function ancestorOf(lines, i) {
5425
5699
  while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
5426
5700
  return h >= 0 ? h : null;
5427
5701
  }
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;
5702
+ 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
5703
  var init_ci_tampering = __esm({
5430
5704
  "src/detectors/ci-tampering.ts"() {
5431
5705
  "use strict";
@@ -5435,60 +5709,22 @@ var init_ci_tampering = __esm({
5435
5709
  init_files();
5436
5710
  init_hook_wiring();
5437
5711
  init_repo();
5712
+ init_gh_expression();
5713
+ init_invocation();
5714
+ init_invocation();
5438
5715
  RULE6 = "ci-tampering";
5439
5716
  USES = /^\s*-\s*uses:/;
5440
5717
  CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
5441
5718
  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)\./;
5719
+ invokesCheck2 = (line) => invokesCheck(line) || INVOKES_TOOL.test(commandCore(line));
5482
5720
  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
5721
  SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
5486
5722
  indentOf = (l) => l.length - l.trimStart().length;
5487
5723
  STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
5488
5724
  REPORTER_ACTION = /^\s*-?\s*uses:\s*\S*?(?:[\w-]*-reporter|[\w-]*-results?[\w-]*|upload-[\w-]*|codecov[\w-]*|coveralls[\w-]*)(?:@|\s|$)/i;
5489
5725
  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));
5726
+ stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l));
5727
+ jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
5492
5728
  NEUTRALISERS = [
5493
5729
  { re: /^\s*set\s+\+e\b/, what: "set +e \u2014 a failing check no longer fails the step" },
5494
5730
  { re: /^\s*exit\s+0\s*(?:$|[;#])/, what: "exit 0 \u2014 the step ends green whatever the check did" },
@@ -5574,7 +5810,7 @@ var init_ci_tampering = __esm({
5574
5810
  }
5575
5811
  if (afterLines) {
5576
5812
  for (const b of runBlocks(afterLines)) {
5577
- if (!b.body.some(invokesCheck)) continue;
5813
+ if (!b.body.some(invokesCheck2)) continue;
5578
5814
  const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
5579
5815
  for (let k = 0; k < b.body.length; k++) {
5580
5816
  const idx = b.start + 1 + k;
@@ -5621,7 +5857,7 @@ var init_ci_tampering = __esm({
5621
5857
  const d = defaultsOf(afterLines, i);
5622
5858
  if (d === null) continue;
5623
5859
  const job = jobOf(afterLines, d);
5624
- if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck(x) || usesCheck(x))) continue;
5860
+ if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
5625
5861
  }
5626
5862
  out3.push(
5627
5863
  makeFinding(RULE6, policy, {
@@ -5678,7 +5914,7 @@ var init_ci_tampering = __esm({
5678
5914
  for (const l of removedLines(c)) {
5679
5915
  if (/^\s*#/.test(l.content)) continue;
5680
5916
  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);
5917
+ 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
5918
  if (!isUsesLine && !isCheckCommand) continue;
5683
5919
  const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
5684
5920
  if (s.state === "kept") continue;
@@ -7324,33 +7560,43 @@ function excludesOtherRunner(flag, cmd) {
7324
7560
  const values = [...cmd.matchAll(/--exclude(?:=|\s+)(['"]?)(\S+?)\1(?=\s|$)/g)].map((m) => m[2]);
7325
7561
  return values.length > 0 && values.every((v) => OTHER_RUNNER_DIR.test(v.replace(/^\.\//, "").replace(/(?:\/\*+)*\/?$/, "") + "/"));
7326
7562
  }
7327
- function scriptNarrowings(before, after, path) {
7563
+ function scriptsOf(src) {
7564
+ try {
7565
+ const pkg = JSON.parse(src ?? "{}");
7566
+ const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
7567
+ return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
7568
+ } catch {
7569
+ return {};
7570
+ }
7571
+ }
7572
+ function scriptWeakenings(before, after, path, policy, addedPaths) {
7328
7573
  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
7574
  const was = scriptsOf(before);
7339
7575
  const now = scriptsOf(after);
7576
+ const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/") + 1) : "";
7577
+ const configReviewed = (file) => {
7578
+ const f = dir + file.replace(/^\.\//, "");
7579
+ return isProtected(f, policy, "config") && !addedPaths.has(f);
7580
+ };
7581
+ const relocated = (kind) => Object.keys(now).some((n) => !(n in was) && checkKinds(now[n]).has(kind));
7340
7582
  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;
7583
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])) {
7584
+ const whole = WHOLE_SUITE_SCRIPT.test(name);
7585
+ if (!whole && !SLICE_SCRIPT.test(name)) continue;
7586
+ const prev = was[name];
7587
+ const cmd = now[name];
7588
+ if (prev === cmd) continue;
7589
+ if (prev === void 0 && !whole) continue;
7590
+ if (cmd === void 0) {
7591
+ if (name !== "pretest" || prev === void 0) continue;
7592
+ const gone = [...checkKinds(prev)].filter((k) => !relocated(k));
7593
+ if (gone.length === 0) continue;
7594
+ out3.push({ script: name, weakening: { state: "removed", kind: gone[0], what: "the script is gone" }, before: prev, after: "" });
7595
+ continue;
7353
7596
  }
7597
+ const w = invocationWeakening(prev ?? "", cmd, void 0, { configReviewed, excuseFlag: excludesOtherRunner, sliceByDesign: !whole, relocated });
7598
+ if (w.state === "kept") continue;
7599
+ out3.push({ script: name, weakening: w, before: prev ?? "", after: cmd });
7354
7600
  }
7355
7601
  return out3;
7356
7602
  }
@@ -7412,7 +7658,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
7412
7658
  })
7413
7659
  );
7414
7660
  }
7415
- var RULE8, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SED_INPLACE, testDeletion;
7661
+ var RULE8, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
7416
7662
  var init_test_deletion = __esm({
7417
7663
  "src/detectors/test-deletion.ts"() {
7418
7664
  "use strict";
@@ -7423,7 +7669,7 @@ var init_test_deletion = __esm({
7423
7669
  init_files();
7424
7670
  init_repo();
7425
7671
  init_suite_config();
7426
- init_ci_tampering();
7672
+ init_invocation();
7427
7673
  init_narrow();
7428
7674
  RULE8 = "test-deletion";
7429
7675
  JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
@@ -7441,7 +7687,8 @@ var init_test_deletion = __esm({
7441
7687
  isEachOf = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
7442
7688
  isDescribeEach = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
7443
7689
  OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
7444
- WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
7690
+ WHOLE_SUITE_SCRIPT = /^(?:test|test:ci|check|pretest)$/;
7691
+ SLICE_SCRIPT = /^test:(?:unit|integration)$/;
7445
7692
  SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
7446
7693
  testDeletion = {
7447
7694
  id: RULE8,
@@ -7450,6 +7697,7 @@ var init_test_deletion = __esm({
7450
7697
  run(changes, policy, _view, ctx) {
7451
7698
  const out3 = [];
7452
7699
  const isSpec = (p) => isProtected(p, policy, "tests") && !isProtected(p, policy, "snapshots");
7700
+ const addedPaths = new Set(changes.filter((c) => c.kind === "file" && c.op === "add").map((c) => c.path));
7453
7701
  const addedTestLines = /* @__PURE__ */ new Set();
7454
7702
  let addedTestBlocks = 0;
7455
7703
  for (const c of changes) {
@@ -7529,13 +7777,20 @@ var init_test_deletion = __esm({
7529
7777
  })
7530
7778
  );
7531
7779
  }
7532
- for (const n of scriptNarrowings(c.before, c.after, c.path)) {
7780
+ for (const n of scriptWeakenings(c.before, c.after, c.path, policy, addedPaths)) {
7781
+ const w = n.weakening;
7782
+ const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
7533
7783
  out3.push(
7534
- makeFinding(RULE8, policy, {
7784
+ w.state === "removed" ? makeFinding(RULE8, policy, {
7785
+ file: c.path,
7786
+ message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
7787
+ evidence,
7788
+ 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."
7789
+ }) : makeFinding(RULE8, policy, {
7535
7790
  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."
7791
+ message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
7792
+ evidence,
7793
+ 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
7794
  })
7540
7795
  );
7541
7796
  }
@@ -8902,8 +9157,22 @@ var init_deny = __esm({
8902
9157
  });
8903
9158
 
8904
9159
  // src/session.ts
8905
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
9160
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
8906
9161
  import { dirname as dirname3, join as join10 } from "node:path";
9162
+ function writeBaseline(p, sha3) {
9163
+ mkdirSync2(dirname3(p), { recursive: true });
9164
+ const tmp = `${p}.${process.pid}.tmp`;
9165
+ try {
9166
+ writeFileSync2(tmp, sha3);
9167
+ renameSync(tmp, p);
9168
+ } catch (e) {
9169
+ try {
9170
+ rmSync2(tmp, { force: true });
9171
+ } catch {
9172
+ }
9173
+ throw e;
9174
+ }
9175
+ }
8907
9176
  function baselinePath(cwd, sessionId) {
8908
9177
  const gd = gitDir(cwd);
8909
9178
  if (!gd) return null;
@@ -8911,21 +9180,23 @@ function baselinePath(cwd, sessionId) {
8911
9180
  }
8912
9181
  function turnBaseline(cwd, sessionId) {
8913
9182
  if (!sessionId) return null;
9183
+ const p = baselinePath(cwd, sessionId);
9184
+ if (!p) return null;
8914
9185
  try {
8915
- const p = baselinePath(cwd, sessionId);
8916
- if (!p) return null;
8917
9186
  if (existsSync7(p)) {
8918
9187
  const v = readFileSync7(p, "utf8").trim();
8919
9188
  if (SHA.test(v)) return v;
8920
9189
  }
8921
- const head = headSha(cwd);
8922
- if (!head) return null;
8923
- mkdirSync2(dirname3(p), { recursive: true });
8924
- writeFileSync2(p, head);
8925
- return head;
8926
9190
  } catch {
8927
- return null;
8928
9191
  }
9192
+ const head = headSha(cwd);
9193
+ if (!head) return null;
9194
+ try {
9195
+ writeBaseline(p, head);
9196
+ } catch (e) {
9197
+ throw new BaselineWriteError(p, e);
9198
+ }
9199
+ return head;
8929
9200
  }
8930
9201
  function advanceTurnBaseline(cwd, sessionId) {
8931
9202
  if (!sessionId) return;
@@ -8933,24 +9204,30 @@ function advanceTurnBaseline(cwd, sessionId) {
8933
9204
  const p = baselinePath(cwd, sessionId);
8934
9205
  const head = headSha(cwd);
8935
9206
  if (!p || !head) return;
8936
- mkdirSync2(dirname3(p), { recursive: true });
8937
- writeFileSync2(p, head);
9207
+ writeBaseline(p, head);
8938
9208
  } catch {
8939
9209
  }
8940
9210
  }
8941
- var UNSAFE, SHA;
9211
+ var UNSAFE, SHA, BaselineWriteError;
8942
9212
  var init_session = __esm({
8943
9213
  "src/session.ts"() {
8944
9214
  "use strict";
8945
9215
  init_build();
8946
9216
  UNSAFE = /[^A-Za-z0-9_-]/g;
8947
- SHA = /^[0-9a-f]{7,40}$/;
9217
+ SHA = /^[0-9a-f]{40}$/;
9218
+ BaselineWriteError = class extends Error {
9219
+ constructor(path, cause) {
9220
+ const detail = cause instanceof Error ? cause.message : String(cause);
9221
+ super(`turn baseline could not be recorded at ${path}: ${detail}`);
9222
+ this.name = "BaselineWriteError";
9223
+ }
9224
+ };
8948
9225
  }
8949
9226
  });
8950
9227
 
8951
9228
  // src/effect.ts
8952
9229
  import { createHash as createHash3 } from "node:crypto";
8953
- import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
9230
+ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
8954
9231
  import { join as join11 } from "node:path";
8955
9232
  function ptreeFrom(value) {
8956
9233
  if (!isRecord(value)) return null;
@@ -9055,14 +9332,14 @@ function saveTree(p, tree) {
9055
9332
  mkdirSync3(join11(p, ".."), { recursive: true });
9056
9333
  const tmp = `${p}.${process.pid}.tmp`;
9057
9334
  writeFileSync3(tmp, JSON.stringify(tree));
9058
- renameSync(tmp, p);
9335
+ renameSync2(tmp, p);
9059
9336
  } catch {
9060
9337
  }
9061
9338
  }
9062
9339
  function dropTree(p) {
9063
9340
  if (!p) return;
9064
9341
  try {
9065
- rmSync2(p, { force: true });
9342
+ rmSync3(p, { force: true });
9066
9343
  } catch {
9067
9344
  }
9068
9345
  }
@@ -9361,7 +9638,7 @@ var init_watch = __esm({
9361
9638
  });
9362
9639
 
9363
9640
  // src/detectors/fs-events.ts
9364
- import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
9641
+ import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
9365
9642
  function fsEventFrom(line) {
9366
9643
  let value;
9367
9644
  try {
@@ -9381,7 +9658,7 @@ function fsEventFrom(line) {
9381
9658
  function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_EVENT_READ_IO) {
9382
9659
  let size = 0;
9383
9660
  try {
9384
- size = statSync(log).size;
9661
+ size = statSync2(log).size;
9385
9662
  } catch {
9386
9663
  return {
9387
9664
  events: [],
@@ -9972,8 +10249,12 @@ function recordObserverHealth(state, reason) {
9972
10249
  function stopVerdict(input, defaultCwd) {
9973
10250
  if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
9974
10251
  const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
9975
- if (!isGitRepo(sessionCwd)) return { exitCode: 0, stdout: "" };
9976
- const cwd = repoRoot(sessionCwd);
10252
+ const ctx = repoContext(sessionCwd);
10253
+ if (!ctx) {
10254
+ const why = outsideRepository(sessionCwd);
10255
+ return why ? failClosed("Stop", why) : { exitCode: 0, stdout: "" };
10256
+ }
10257
+ const cwd = ctx.root;
9977
10258
  let blocks2;
9978
10259
  let commitCursor = () => {
9979
10260
  };
@@ -10163,7 +10444,7 @@ var init_ptree_cache = __esm({
10163
10444
  });
10164
10445
 
10165
10446
  // src/cli/hook-service.ts
10166
- import { chmodSync, existsSync as existsSync10, lstatSync as lstatSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
10447
+ import { chmodSync, existsSync as existsSync10, lstatSync as lstatSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
10167
10448
  import { createConnection as createConnection2, createServer } from "node:net";
10168
10449
  import { realpathSync as realpathSync3 } from "node:fs";
10169
10450
  import { sep } from "node:path";
@@ -10246,7 +10527,7 @@ function withEnv(env, fn) {
10246
10527
  }
10247
10528
  function removeQuietly(p) {
10248
10529
  try {
10249
- rmSync3(p, { force: true });
10530
+ rmSync4(p, { force: true });
10250
10531
  } catch {
10251
10532
  }
10252
10533
  }
@@ -10588,7 +10869,7 @@ var init_verifier_limits = __esm({
10588
10869
  // src/cli/init.ts
10589
10870
  import { execFileSync as execFileSync6 } from "node:child_process";
10590
10871
  import { createHash as createHash6 } from "node:crypto";
10591
- import { existsSync as existsSync11, lstatSync as lstatSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync4, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync2 } from "node:fs";
10872
+ import { existsSync as existsSync11, lstatSync as lstatSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync4, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync3 } from "node:fs";
10592
10873
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join14, relative as relative3, resolve as resolve4 } from "node:path";
10593
10874
  function gitConfig(cwd, key2) {
10594
10875
  try {
@@ -10951,7 +11232,7 @@ function planPreCommit(cwd) {
10951
11232
  }
10952
11233
  }
10953
11234
  const rel = display(cwd, path);
10954
- if (existsSync11(path) && !statSync2(path).isFile()) {
11235
+ if (existsSync11(path) && !statSync3(path).isFile()) {
10955
11236
  return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
10956
11237
  }
10957
11238
  const existing = existsSync11(path) ? readFileSync12(path, "utf8") : null;
@@ -11393,7 +11674,7 @@ import {
11393
11674
  readFileSync as readFileSync13,
11394
11675
  readlinkSync as readlinkSync2,
11395
11676
  realpathSync as realpathSync5,
11396
- statSync as statSync3
11677
+ statSync as statSync4
11397
11678
  } from "node:fs";
11398
11679
  import {
11399
11680
  basename as basename3,
@@ -11435,7 +11716,7 @@ function fingerprintRoot(root, cwd, h) {
11435
11716
  root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
11436
11717
  );
11437
11718
  }
11438
- if (!statSync3(root.path).isDirectory()) {
11719
+ if (!statSync4(root.path).isDirectory()) {
11439
11720
  throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
11440
11721
  }
11441
11722
  h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
@@ -11445,7 +11726,7 @@ function fingerprintRoot(root, cwd, h) {
11445
11726
  const hashFile = (path, label) => {
11446
11727
  let st;
11447
11728
  try {
11448
- st = statSync3(path);
11729
+ st = statSync4(path);
11449
11730
  if (!st.isFile()) throw new Error("not a regular file");
11450
11731
  h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
11451
11732
  h.update(readFileSync13(path));
@@ -11511,7 +11792,7 @@ function fingerprintRoot(root, cwd, h) {
11511
11792
  } catch {
11512
11793
  continue;
11513
11794
  }
11514
- const followed = statSync3(path);
11795
+ const followed = statSync4(path);
11515
11796
  if (followed.isFile()) {
11516
11797
  hashFile(resolved, "@target:" + rel);
11517
11798
  continue;
@@ -11559,7 +11840,7 @@ function measuredSnapshot(roots, probes, cwd, metrics) {
11559
11840
  }
11560
11841
  function freezeRoot(kind, path) {
11561
11842
  try {
11562
- if (!statSync3(path).isDirectory()) {
11843
+ if (!statSync4(path).isDirectory()) {
11563
11844
  throw new Error("not a directory");
11564
11845
  }
11565
11846
  return { kind, path: resolve5(path), realPath: realpathSync5(path) };
@@ -11608,7 +11889,7 @@ function resolveExecutable(token, cwd, env) {
11608
11889
  for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
11609
11890
  const candidate = resolve5(entry, token);
11610
11891
  try {
11611
- if (existsSync12(candidate) && statSync3(candidate).isFile()) return realpathSync5(candidate);
11892
+ if (existsSync12(candidate) && statSync4(candidate).isFile()) return realpathSync5(candidate);
11612
11893
  } catch {
11613
11894
  }
11614
11895
  }
@@ -11795,7 +12076,7 @@ var init_dependency_env = __esm({
11795
12076
 
11796
12077
  // src/suite-diagnostics.ts
11797
12078
  import { spawnSync } from "node:child_process";
11798
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
12079
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
11799
12080
  import { tmpdir as tmpdir3 } from "node:os";
11800
12081
  import { join as join16 } from "node:path";
11801
12082
  function emptySuiteDiagnostics() {
@@ -11924,7 +12205,7 @@ function runCapturedProcessSync(executable, args, opts) {
11924
12205
  }
11925
12206
  return parsed;
11926
12207
  } finally {
11927
- rmSync4(stateDir, { recursive: true, force: true });
12208
+ rmSync5(stateDir, { recursive: true, force: true });
11928
12209
  }
11929
12210
  }
11930
12211
  var DIAGNOSTIC_TAIL_BYTES, EMPTY_STREAM, CAPTURE_SUPERVISOR;
@@ -12114,7 +12395,7 @@ if (child) {
12114
12395
  // src/verifier-backend.ts
12115
12396
  import { createHash as createHash9, randomUUID } from "node:crypto";
12116
12397
  import { spawnSync as spawnSync2 } from "node:child_process";
12117
- import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync6, statSync as statSync4 } from "node:fs";
12398
+ import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync6, statSync as statSync5 } from "node:fs";
12118
12399
  import { delimiter as delimiter2, dirname as dirname6, join as join17, resolve as resolve6 } from "node:path";
12119
12400
  function identity() {
12120
12401
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
@@ -12123,7 +12404,7 @@ function identity() {
12123
12404
  }
12124
12405
  function writableByCaller(path) {
12125
12406
  try {
12126
- const st = statSync4(path);
12407
+ const st = statSync5(path);
12127
12408
  const { uid, groups } = identity();
12128
12409
  if (uid === 0) return true;
12129
12410
  if ((st.mode & 2) !== 0) return true;
@@ -12151,7 +12432,7 @@ function findExecutable(name) {
12151
12432
  if (!raw) continue;
12152
12433
  const candidate = join17(raw, name);
12153
12434
  try {
12154
- accessSync(candidate, constants2.X_OK);
12435
+ accessSync2(candidate, constants3.X_OK);
12155
12436
  return realpathSync6(candidate);
12156
12437
  } catch {
12157
12438
  }
@@ -12593,7 +12874,7 @@ var init_verifier_backend = __esm({
12593
12874
  // src/cli/verify.ts
12594
12875
  import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:child_process";
12595
12876
  import { createHash as createHash10 } from "node:crypto";
12596
- import { chmodSync as chmodSync3, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync5, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "node:fs";
12877
+ import { chmodSync as chmodSync3, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync6, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "node:fs";
12597
12878
  import { tmpdir as tmpdir4 } from "node:os";
12598
12879
  import { dirname as dirname7, isAbsolute as isAbsolute10, join as join18, relative as relative5, resolve as resolve7, sep as sep3 } from "node:path";
12599
12880
  function oracleAssuranceReport() {
@@ -12641,7 +12922,7 @@ function safeSymlink(target, out3, root, label) {
12641
12922
  if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out3), target))) {
12642
12923
  throw linkEscape(label, target);
12643
12924
  }
12644
- rmSync5(out3, { force: true });
12925
+ rmSync6(out3, { force: true });
12645
12926
  symlinkSync(target, out3);
12646
12927
  }
12647
12928
  function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
@@ -12752,7 +13033,7 @@ function probeFilesystemCaseSensitivity(dir) {
12752
13033
  writeFileSync9(upper, "", { flag: "wx" });
12753
13034
  return !existsSync13(lower);
12754
13035
  } finally {
12755
- rmSync5(upper, { force: true });
13036
+ rmSync6(upper, { force: true });
12756
13037
  }
12757
13038
  }
12758
13039
  function foldCase(p) {
@@ -12770,7 +13051,7 @@ function caseFoldCollision(paths) {
12770
13051
  }
12771
13052
  function dropSymlink(p) {
12772
13053
  try {
12773
- if (lstatSync9(p).isSymbolicLink()) rmSync5(p, { force: true });
13054
+ if (lstatSync9(p).isSymbolicLink()) rmSync6(p, { force: true });
12774
13055
  } catch {
12775
13056
  }
12776
13057
  }
@@ -12846,7 +13127,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
12846
13127
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
12847
13128
  const out3 = join18(dest, e.path);
12848
13129
  mkdirSync7(dirname7(out3), { recursive: true });
12849
- rmSync5(out3, { force: true });
13130
+ rmSync6(out3, { force: true });
12850
13131
  if (e.mode === "120000") {
12851
13132
  const target = content.toString("utf8");
12852
13133
  safeSymlink(target, out3, dest, `the base's ${e.path}`);
@@ -12861,7 +13142,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
12861
13142
  const inCopy = workingTreePaths(cwd);
12862
13143
  for (const rel of inCopy) {
12863
13144
  if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
12864
- rmSync5(join18(dest, rel), { force: true });
13145
+ rmSync6(join18(dest, rel), { force: true });
12865
13146
  removed++;
12866
13147
  }
12867
13148
  for (const link of restoredLinks) {
@@ -12948,7 +13229,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
12948
13229
  }
12949
13230
  return { exit: r.exit, secs, diagnostics: r.diagnostics };
12950
13231
  } finally {
12951
- rmSync5(scratch, { recursive: true, force: true });
13232
+ rmSync6(scratch, { recursive: true, force: true });
12952
13233
  }
12953
13234
  }
12954
13235
  function diagnosticsJson(diagnostics, includeTail) {
@@ -13101,7 +13382,7 @@ function runVerify(opts) {
13101
13382
  const visDir = join18(visRoot, "t");
13102
13383
  const cleanup = (dirs) => {
13103
13384
  if (opts.keep) return;
13104
- for (const d of dirs) rmSync5(d, { recursive: true, force: true });
13385
+ for (const d of dirs) rmSync6(d, { recursive: true, force: true });
13105
13386
  };
13106
13387
  filesystemCaseSensitive = (opts.probeCaseSensitivity ?? probeFilesystemCaseSensitivity)(visRoot);
13107
13388
  if (!filesystemCaseSensitive) {
@@ -13468,7 +13749,7 @@ var init_verify = __esm({
13468
13749
  // src/cli/run.ts
13469
13750
  import { execFileSync as execFileSync9, spawn, spawnSync as spawnSync4 } from "node:child_process";
13470
13751
  import { randomBytes } from "node:crypto";
13471
- import { accessSync as accessSync2, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
13752
+ import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync7, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
13472
13753
  import { tmpdir as tmpdir5 } from "node:os";
13473
13754
  import { dirname as dirname8, join as join19, resolve as resolve8 } from "node:path";
13474
13755
  function authoritativeRunLifecyclePlatform(platform = process.platform) {
@@ -13503,7 +13784,7 @@ function writableByCaller2(path) {
13503
13784
  let cur = path;
13504
13785
  for (; ; ) {
13505
13786
  try {
13506
- accessSync2(cur, fsConstants.W_OK);
13787
+ accessSync3(cur, fsConstants.W_OK);
13507
13788
  return true;
13508
13789
  } catch {
13509
13790
  }
@@ -13524,7 +13805,7 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
13524
13805
  for (const candidate of candidates) {
13525
13806
  try {
13526
13807
  const real = realpathSync8(candidate);
13527
- const st = statSync5(real);
13808
+ const st = statSync6(real);
13528
13809
  if (!st.isFile() || writableByCaller2(real)) continue;
13529
13810
  const probe = spawnSync4(
13530
13811
  real,
@@ -13647,7 +13928,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
13647
13928
  ...state.failure ? { failure: state.failure } : {}
13648
13929
  };
13649
13930
  } finally {
13650
- rmSync6(stateDir, { recursive: true, force: true });
13931
+ rmSync7(stateDir, { recursive: true, force: true });
13651
13932
  }
13652
13933
  }
13653
13934
  function waitMs(ms) {
@@ -15020,7 +15301,7 @@ import {
15020
15301
  readFileSync as readFileSync19,
15021
15302
  readdirSync as readdirSync6,
15022
15303
  realpathSync as realpathSync9,
15023
- rmSync as rmSync7,
15304
+ rmSync as rmSync8,
15024
15305
  symlinkSync as symlinkSync2
15025
15306
  } from "node:fs";
15026
15307
  import { tmpdir as tmpdir6 } from "node:os";
@@ -15218,8 +15499,8 @@ function runOneTrace(cwd, base, command, budget) {
15218
15499
  accesses: rewriteTraceRoot(accesses, root)
15219
15500
  };
15220
15501
  } finally {
15221
- rmSync7(root, { recursive: true, force: true });
15222
- rmSync7(traceDir, { recursive: true, force: true });
15502
+ rmSync8(root, { recursive: true, force: true });
15503
+ rmSync8(traceDir, { recursive: true, force: true });
15223
15504
  }
15224
15505
  }
15225
15506
  function renderText2(report2) {
@@ -15404,7 +15685,7 @@ var init_trace_verify = __esm({
15404
15685
  // src/cli/onboard.ts
15405
15686
  import { execFileSync as execFileSync12 } from "node:child_process";
15406
15687
  import { randomBytes as randomBytes2 } from "node:crypto";
15407
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync10, renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
15688
+ import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync10, renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
15408
15689
  import { tmpdir as tmpdir7 } from "node:os";
15409
15690
  import { join as join22, relative as relative7, resolve as resolve11, sep as sep5 } from "node:path";
15410
15691
  import { createInterface } from "node:readline";
@@ -15477,9 +15758,9 @@ function atomicReplaceFile(path, content, mode) {
15477
15758
  const tmp = `${path}.${process.pid}.${randomBytes2(8).toString("hex")}.tmp`;
15478
15759
  try {
15479
15760
  writeFileSync11(tmp, content, { encoding: "utf8", flag: "wx", mode });
15480
- renameSync2(tmp, path);
15761
+ renameSync3(tmp, path);
15481
15762
  } finally {
15482
- rmSync8(tmp, { force: true });
15763
+ rmSync9(tmp, { force: true });
15483
15764
  }
15484
15765
  }
15485
15766
  function writeVerifyCommand(cwd, command) {
@@ -15506,7 +15787,7 @@ function writeVerifyCommand(cwd, command) {
15506
15787
  if (loaded2.verify?.command !== command) throw new Error("verify.command did not round-trip");
15507
15788
  return null;
15508
15789
  } catch (e) {
15509
- if (original === null) rmSync8(path, { force: true });
15790
+ if (original === null) rmSync9(path, { force: true });
15510
15791
  else atomicReplaceFile(path, original, mode);
15511
15792
  return `the updated ${POLICY_FILE} does not load (${errorMessage(e)}); restored the previous file`;
15512
15793
  }
@@ -15906,7 +16187,7 @@ function runDemo(cwd, head, runners, out3) {
15906
16187
  } finally {
15907
16188
  git5(cwd, ["worktree", "remove", "--force", wt]);
15908
16189
  git5(cwd, ["worktree", "prune"]);
15909
- rmSync8(wt, { recursive: true, force: true });
16190
+ rmSync9(wt, { recursive: true, force: true });
15910
16191
  }
15911
16192
  const after = treeFingerprint(cwd);
15912
16193
  out3(`working tree fingerprint after: ${after.slice(0, 16)}`);
@@ -16321,7 +16602,7 @@ var init_record = __esm({
16321
16602
 
16322
16603
  // src/research/run.ts
16323
16604
  import { execFileSync as execFileSync13 } from "node:child_process";
16324
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync22, renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
16605
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "node:fs";
16325
16606
  import { join as join23, resolve as resolve14 } from "node:path";
16326
16607
  function git6(args, cwd) {
16327
16608
  return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -16370,11 +16651,11 @@ function resumableRecord(path, expected) {
16370
16651
  function writeRecordAtomically(path, text) {
16371
16652
  const tmp = `${path}.${process.pid}.tmp`;
16372
16653
  writeFileSync12(tmp, text);
16373
- renameSync3(tmp, path);
16654
+ renameSync4(tmp, path);
16374
16655
  }
16375
16656
  function freshWorkspace(ledger, task, pair, arm, sourceBase) {
16376
16657
  const ws2 = join23(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
16377
- rmSync9(ws2, { recursive: true, force: true });
16658
+ rmSync10(ws2, { recursive: true, force: true });
16378
16659
  mkdirSync8(join23(ledger, "workspaces"), { recursive: true });
16379
16660
  try {
16380
16661
  execFileSync13("git", ["clone", "-q", "--no-hardlinks", task.repo, ws2], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.10",
3
+ "version": "2.23.12",
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",