tamperward 2.10.0 → 2.10.2

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 +347 -104
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -153,6 +153,18 @@ function defaultPolicy(version = 1) {
153
153
  // as a strengthening). pyproject.toml carries [tool.coverage.report].
154
154
  "**/.coveragerc",
155
155
  "**/pyproject.toml",
156
+ // pytest's selection configuration. These decide WHICH TESTS RUN, so a
157
+ // narrowing in them removes a test as surely as deleting the file. The
158
+ // pristine boundary already refused to inherit them (verify's
159
+ // VERIFICATION_SURFACE, 1.14.2); this is the in-loop half, so the gate can
160
+ // say so at the tool call instead of only at adjudication. `tox.ini` and
161
+ // `setup.cfg` are shared files — the suite-config detector only treats them
162
+ // as pytest configs when they carry the section pytest reads, so ordinary
163
+ // packaging edits are untouched.
164
+ "**/pytest.ini",
165
+ "**/.pytest.ini",
166
+ "**/tox.ini",
167
+ "**/setup.cfg",
156
168
  "**/.nycrc",
157
169
  "**/.nycrc.*",
158
170
  "**/nyc.config.*",
@@ -1334,6 +1346,8 @@ import ts2 from "typescript";
1334
1346
 
1335
1347
  // src/detectors/repo.ts
1336
1348
  import { execFileSync as execFileSync2 } from "node:child_process";
1349
+ import { readFileSync } from "node:fs";
1350
+ import { join as join2 } from "node:path";
1337
1351
  var cache = /* @__PURE__ */ new Map();
1338
1352
  function trackedFiles(ctx) {
1339
1353
  if (!ctx) return null;
@@ -1355,6 +1369,23 @@ function trackedFiles(ctx) {
1355
1369
  cache.set(ctx.cwd, files);
1356
1370
  return files;
1357
1371
  }
1372
+ var contentCache = /* @__PURE__ */ new Map();
1373
+ function trackedContent(path, ctx) {
1374
+ if (!ctx) return null;
1375
+ if (ctx.trackedContents) return ctx.trackedContents[path] ?? null;
1376
+ if (!ctx.cwd) return null;
1377
+ const key2 = ctx.cwd + "\0" + path;
1378
+ const hit = contentCache.get(key2);
1379
+ if (hit !== void 0) return hit;
1380
+ let src = null;
1381
+ try {
1382
+ src = readFileSync(join2(ctx.cwd, path), "utf8");
1383
+ } catch {
1384
+ src = null;
1385
+ }
1386
+ contentCache.set(key2, src);
1387
+ return src;
1388
+ }
1358
1389
  var branchCache = /* @__PURE__ */ new Map();
1359
1390
  var refsCache = /* @__PURE__ */ new Map();
1360
1391
  function branchRefs(ctx) {
@@ -2660,20 +2691,20 @@ var ciTampering = {
2660
2691
  };
2661
2692
 
2662
2693
  // src/detectors/hook-tampering.ts
2663
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
2664
- import { isAbsolute as isAbsolute3, join as join3, relative } from "node:path";
2694
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
2695
+ import { isAbsolute as isAbsolute3, join as join4, relative } from "node:path";
2665
2696
 
2666
2697
  // src/wiring.ts
2667
- import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
2698
+ import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
2668
2699
  import { homedir } from "node:os";
2669
- import { basename, dirname, isAbsolute, join as join2, posix } from "node:path";
2700
+ import { basename, dirname, isAbsolute, join as join3, posix } from "node:path";
2670
2701
  import { fileURLToPath } from "node:url";
2671
2702
  function shippedVersion() {
2672
2703
  try {
2673
2704
  const here = dirname(fileURLToPath(import.meta.url));
2674
2705
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
2675
2706
  try {
2676
- const pkg = JSON.parse(readFileSync(join2(here, rel), "utf8"));
2707
+ const pkg = JSON.parse(readFileSync2(join3(here, rel), "utf8"));
2677
2708
  if (pkg.name === "tamperward" && pkg.version) return pkg.version;
2678
2709
  } catch {
2679
2710
  }
@@ -4551,8 +4582,8 @@ function lefthookBase(c, changes, ctx) {
4551
4582
  if (!ctx?.cwd) return null;
4552
4583
  for (const n of names) {
4553
4584
  try {
4554
- const p = join3(ctx.cwd, n);
4555
- if (existsSync2(p)) return parseDoc(readFileSync2(p, "utf8"));
4585
+ const p = join4(ctx.cwd, n);
4586
+ if (existsSync2(p)) return parseDoc(readFileSync3(p, "utf8"));
4556
4587
  } catch {
4557
4588
  }
4558
4589
  }
@@ -4626,6 +4657,61 @@ function repoRelative(path, ctx) {
4626
4657
  const rel = relative(ctx.cwd, path);
4627
4658
  return rel && !rel.startsWith("..") && !isAbsolute3(rel) ? rel : path;
4628
4659
  }
4660
+ var HUSKY_SHIM = '#!/usr/bin/env sh\n. "$(dirname "$0")/h"';
4661
+ var HUSKY_SHIM_NAMES = /* @__PURE__ */ new Set(["applypatch-msg", "commit-msg", "post-applypatch", "post-checkout", "post-commit", "post-merge", "post-rewrite", "pre-applypatch", "pre-auto-gc", "pre-commit", "pre-merge-commit", "pre-push", "pre-rebase", "prepare-commit-msg"]);
4662
+ var HUSKY_H_KNOWN = [
4663
+ // husky 9.1.x
4664
+ `#!/usr/bin/env sh
4665
+ [ "$HUSKY" = "2" ] && set -x
4666
+ n=$(basename "$0")
4667
+ s=$(dirname "$(dirname "$0")")/$n
4668
+
4669
+ [ ! -f "$s" ] && exit 0
4670
+
4671
+ if [ -f "$HOME/.huskyrc" ]; then
4672
+ echo "husky - '~/.huskyrc' is DEPRECATED, please move your code to ~/.config/husky/init.sh"
4673
+ fi
4674
+ i="\${XDG_CONFIG_HOME:-$HOME/.config}/husky/init.sh"
4675
+ [ -f "$i" ] && . "$i"
4676
+
4677
+ [ "\${HUSKY-}" = "0" ] && exit 0
4678
+
4679
+ export PATH="node_modules/.bin:$PATH"
4680
+ sh -e "$s" "$@"
4681
+ c=$?
4682
+
4683
+ [ $c != 0 ] && echo "husky - $n script failed (code $c)"
4684
+ [ $c = 127 ] && echo "husky - command not found in PATH=$PATH"
4685
+ exit $c
4686
+ `
4687
+ ];
4688
+ var HUSKY_DEPRECATION = `echo "husky - DEPRECATED
4689
+
4690
+ Please remove the following two lines from $0:
4691
+
4692
+ #!/usr/bin/env sh
4693
+ . \\"\\$(dirname -- \\"\\$0\\")/_/husky.sh\\"
4694
+
4695
+ They WILL FAIL in v10.0.0
4696
+ "`;
4697
+ var stripTrailingNl = (s) => s.endsWith("\n") ? s.slice(0, -1) : s;
4698
+ function huskyRuntimeWrite(path, content, ctx) {
4699
+ const m = path.match(/(?:^|\/)\.husky\/_\/([^/]+)$/);
4700
+ if (!m || content == null) return false;
4701
+ const name = m[1];
4702
+ const body = stripTrailingNl(content);
4703
+ const ok = name === ".gitignore" ? body === "*" : name === "h" ? HUSKY_H_KNOWN.some((t) => stripTrailingNl(t) === body) : name === "husky.sh" ? stripTrailingNl(HUSKY_DEPRECATION) === body : HUSKY_SHIM_NAMES.has(name) && body === HUSKY_SHIM;
4704
+ if (!ok) return false;
4705
+ if (!ctx?.cwd) return false;
4706
+ const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
4707
+ const entry = inspectRel(join4(ctx.cwd, root), ".husky/pre-commit");
4708
+ if (entry.kind !== "file") return false;
4709
+ const gate = textOf(entry);
4710
+ if (gate == null) return false;
4711
+ return invocations(gate.split("\n"), { errexit: true }).some(
4712
+ (i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
4713
+ );
4714
+ }
4629
4715
  var isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
4630
4716
  var isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
4631
4717
  var hookTampering = {
@@ -4700,6 +4786,7 @@ var hookTampering = {
4700
4786
  } else if ((isLefthook(c.path) || isPreCommitConfig(c.path)) && (semantic = configFindings(c, changes, policy, ctx)) !== null) {
4701
4787
  out2.push(...semantic);
4702
4788
  } else if (!c.path.endsWith(POLICY_FILE)) {
4789
+ if ((c.op === "add" || c.op === "modify") && huskyRuntimeWrite(c.path, c.after, ctx)) continue;
4703
4790
  const husky = isHuskyScript(c.path);
4704
4791
  const opts = husky ? { errexit: true } : {};
4705
4792
  const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
@@ -4897,6 +4984,8 @@ import ts3 from "typescript";
4897
4984
  var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
4898
4985
  var JEST_DEFAULT_IGNORE = ["/node_modules/"];
4899
4986
  var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
4987
+ var PYTEST_DEFAULT_INCLUDE = ["**/test_*.py", "**/*_test.py"];
4988
+ var PYTEST_DEFAULT_IGNORE = ["**/build/**", "**/dist/**", "**/node_modules/**", "**/venv/**", "**/.*/**"];
4900
4989
  var VITEST_DEFAULT_EXCLUDE = [
4901
4990
  "**/node_modules/**",
4902
4991
  "**/dist/**",
@@ -4914,6 +5003,13 @@ var CANONICAL_SAMPLES = [
4914
5003
  "a.spec.js",
4915
5004
  "test/a.test.mjs"
4916
5005
  ];
5006
+ var PYTEST_CANONICAL_SAMPLES = [
5007
+ "tests/test_a.py",
5008
+ "tests/a_test.py",
5009
+ "test_a.py",
5010
+ "src/tests/test_a.py",
5011
+ "tests/conftest.py"
5012
+ ];
4917
5013
  function keyName2(name) {
4918
5014
  if (ts3.isIdentifier(name) || ts3.isStringLiteral(name)) return name.text;
4919
5015
  return null;
@@ -4948,8 +5044,104 @@ function runnerOf(path, content) {
4948
5044
  if (/^jest\.config\./.test(base) || base === "package.json") return "jest";
4949
5045
  if (/^vitest\.(?:config|workspace)\./.test(base)) return "vitest";
4950
5046
  if (/^vite\.config\./.test(base) && content != null && /(?:^|[\s,{])["']?test["']?\s*:/m.test(content)) return "vitest";
5047
+ if (base === "pytest.ini" || base === ".pytest.ini") return "pytest";
5048
+ if (content == null) return null;
5049
+ if (base === "tox.ini" && /^\s*\[pytest\]/m.test(content)) return "pytest";
5050
+ if (base === "setup.cfg" && /^\s*\[tool:pytest\]/m.test(content)) return "pytest";
5051
+ if (base === "pyproject.toml" && /^\s*\[tool\.pytest\.ini_options\]/m.test(content)) return "pytest";
5052
+ return null;
5053
+ }
5054
+ var PYTEST_INI_ORDER = ["pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"];
5055
+ function claimsPytest(name, src) {
5056
+ if (name === "pytest.ini" || name === ".pytest.ini") return true;
5057
+ const section = name === "pyproject.toml" ? /^\s*\[tool\.pytest\.ini_options\]\s*$/m : name === "tox.ini" ? /^\s*\[pytest\]\s*$/m : name === "setup.cfg" ? /^\s*\[tool:pytest\]\s*$/m : null;
5058
+ return section !== null && section.test(src);
5059
+ }
5060
+ function effectivePytestFile(files) {
5061
+ for (const name of PYTEST_INI_ORDER) {
5062
+ const content = files.get(name);
5063
+ if (content == null) continue;
5064
+ if (claimsPytest(name, content)) return { path: name, content };
5065
+ }
4951
5066
  return null;
4952
5067
  }
5068
+ function effectivePytestConfig(path, ctx) {
5069
+ if (path.includes("/")) return false;
5070
+ const base = path;
5071
+ const ORDER = PYTEST_INI_ORDER;
5072
+ const rank = ORDER.indexOf(base);
5073
+ if (rank < 0) return false;
5074
+ const files = ctx?.trackedFiles;
5075
+ if (!files) return true;
5076
+ const rootNames = new Set(files.filter((f) => !f.includes("/")));
5077
+ return !ORDER.slice(0, rank).some((n) => rootNames.has(n));
5078
+ }
5079
+ function pytestBlock(src) {
5080
+ const lines = src.split(/\r?\n/);
5081
+ const head = /^\s*\[(pytest|tool:pytest|tool\.pytest\.ini_options)\]\s*$/;
5082
+ let i = lines.findIndex((l) => head.test(l));
5083
+ if (i < 0) return null;
5084
+ const out2 = {};
5085
+ let key2 = null;
5086
+ for (i += 1; i < lines.length; i++) {
5087
+ const line = lines[i];
5088
+ if (/^\s*\[/.test(line)) break;
5089
+ if (/^\s*[#;]/.test(line) || !line.trim()) {
5090
+ continue;
5091
+ }
5092
+ const m = line.match(/^(\w[\w.-]*)\s*[=:]\s*(.*)$/);
5093
+ if (m) {
5094
+ key2 = m[1];
5095
+ out2[key2] = m[2].trim();
5096
+ continue;
5097
+ }
5098
+ if (key2 && /^\s+\S/.test(line)) out2[key2] += " " + line.trim();
5099
+ }
5100
+ return out2;
5101
+ }
5102
+ function pytestTokens(raw) {
5103
+ const v = raw.trim();
5104
+ if (v.startsWith("[")) {
5105
+ return [...v.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
5106
+ }
5107
+ const unq2 = v.replace(/^"([\s\S]*)"$/, "$1").replace(/^'([\s\S]*)'$/, "$1");
5108
+ return [...unq2.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean);
5109
+ }
5110
+ var pathGlobs = (p) => {
5111
+ const clean = p.replace(/^\.\//, "").replace(/::.*$/, "").replace(/\/+$/, "");
5112
+ return clean ? [clean, clean + "/**"] : [];
5113
+ };
5114
+ function pytestSelection(src) {
5115
+ const sel = empty();
5116
+ const blk = pytestBlock(src);
5117
+ if (!blk) return sel;
5118
+ sel.present = true;
5119
+ const ignore = blk.norecursedirs !== void 0 ? pytestTokens(blk.norecursedirs).flatMap((d) => pathGlobs(d).map((pattern) => ({ pattern, key: "norecursedirs" }))) : PYTEST_DEFAULT_IGNORE.map((pattern) => ({ pattern, key: "norecursedirs" }));
5120
+ if (blk.testpaths !== void 0) sel.roots = pytestTokens(blk.testpaths).map((t) => t.replace(/^\.\//, ""));
5121
+ if (blk.python_files !== void 0) sel.include = pytestTokens(blk.python_files).map((g) => g.includes("/") ? g : "**/" + g);
5122
+ for (const key2 of ["python_classes", "python_functions"]) {
5123
+ if (blk[key2] !== void 0) ignore.push({ pattern: "\0" + key2, key: key2 });
5124
+ }
5125
+ const opts = blk.addopts !== void 0 ? pytestTokens(blk.addopts) : [];
5126
+ for (let j = 0; j < opts.length; j++) {
5127
+ const o = opts[j];
5128
+ const val = (inline) => o.includes("=") ? o.slice(o.indexOf("=") + 1) : opts[++j] ?? inline;
5129
+ if (o === "--ignore" || o.startsWith("--ignore=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--ignore" });
5130
+ else if (o === "--ignore-glob" || o.startsWith("--ignore-glob=")) {
5131
+ const g = val("");
5132
+ ignore.push({ pattern: g, key: "--ignore-glob" });
5133
+ if (!g.startsWith("**/") && !g.startsWith("/")) ignore.push({ pattern: "**/" + g, key: "--ignore-glob" });
5134
+ } else if (o === "--deselect" || o.startsWith("--deselect=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--deselect" });
5135
+ else if (o === "-k" || o.startsWith("-k=")) {
5136
+ for (const w of (val("").match(/\bnot\s+([A-Za-z_][\w]*)/g) ?? []).map((t) => t.replace(/^not\s+/, "")))
5137
+ ignore.push({ pattern: `**/*${w}*`, key: "-k" });
5138
+ } else if (o === "-m" || o.startsWith("-m=")) {
5139
+ ignore.push({ pattern: "\0-m " + val(""), key: "-m" });
5140
+ } else if (o === "--collect-only" || o === "--co") ignore.push({ pattern: "**", key: o });
5141
+ }
5142
+ sel.ignore = ignore;
5143
+ return sel;
5144
+ }
4953
5145
  var isWorkspaceFile = (path) => /^vitest\.workspace\./.test(path.split("/").pop() ?? path);
4954
5146
  var empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, opaque: false, present: false });
4955
5147
  var stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
@@ -5098,6 +5290,7 @@ function workspaceArray(sf) {
5098
5290
  }
5099
5291
  function parseSelection(src, runner, path = "") {
5100
5292
  let sel = empty();
5293
+ if (runner === "pytest") return pytestSelection(src);
5101
5294
  try {
5102
5295
  const sf = ts3.createSourceFile("cfg.ts", asExpression2(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
5103
5296
  if (runner === "vitest" && isWorkspaceFile(path)) {
@@ -5146,10 +5339,14 @@ function predicate(sel, runner) {
5146
5339
  why: (p) => ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
5147
5340
  };
5148
5341
  }
5149
- const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : VITEST_DEFAULT_INCLUDE);
5150
- const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({ pattern, key: runner === "jest" ? "testPathIgnorePatterns" : "test.exclude" }));
5342
+ const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : runner === "pytest" ? PYTEST_DEFAULT_INCLUDE : VITEST_DEFAULT_INCLUDE);
5343
+ const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
5344
+ pattern,
5345
+ key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
5346
+ }));
5347
+ const blockWide = ignoreList.filter((e) => e.pattern.startsWith("\0"));
5151
5348
  const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
5152
- const ignoredBy = (p) => ignoreList.find((e) => (runner === "jest" ? regexes([e.pattern]) : globs3([e.pattern]))(p));
5349
+ const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs3([e.pattern]))(p));
5153
5350
  const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
5154
5351
  const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
5155
5352
  const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
@@ -5164,7 +5361,10 @@ function predicate(sel, runner) {
5164
5361
  if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
5165
5362
  if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
5166
5363
  const hit = ignoredBy(r);
5167
- if (hit) return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
5364
+ if (hit) {
5365
+ const pat = hit.pattern.startsWith("\0") ? hit.pattern.slice(1) : hit.pattern;
5366
+ return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(pat)})`;
5367
+ }
5168
5368
  const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
5169
5369
  return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
5170
5370
  }
@@ -5314,12 +5514,14 @@ function significantLines(src, path) {
5314
5514
  return out2;
5315
5515
  }
5316
5516
  var OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
5317
- function runnerSamples(configPath, policy, ctx) {
5517
+ function runnerSamples(configPath, policy, ctx, runner) {
5518
+ const lang = runner === "pytest" ? "py" : "js";
5519
+ const fallback = runner === "pytest" ? PYTEST_CANONICAL_SAMPLES : CANONICAL_SAMPLES;
5318
5520
  const files = trackedFiles(ctx);
5319
- if (!files) return CANONICAL_SAMPLES;
5521
+ if (!files) return fallback;
5320
5522
  const root = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/") + 1) : "";
5321
- const own = files.filter((f) => f.startsWith(root) && langOf(f) === "js" && isProtected(f, policy, "tests") && !OTHER_RUNNER_DIR.test(f)).map((f) => f.slice(root.length));
5322
- return own.length ? own : CANONICAL_SAMPLES;
5523
+ const own = files.filter((f) => f.startsWith(root) && langOf(f) === lang && isProtected(f, policy, "tests") && (lang !== "js" || !OTHER_RUNNER_DIR.test(f))).map((f) => f.slice(root.length));
5524
+ return own.length ? own : fallback;
5323
5525
  }
5324
5526
  var WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
5325
5527
  function excludesOtherRunner(flag, cmd) {
@@ -5440,7 +5642,9 @@ var testDeletion = {
5440
5642
  );
5441
5643
  }
5442
5644
  } else if (c.op !== "delete" && c.after != null && !isTest && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
5443
- for (const n of suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx))) {
5645
+ const rn = runnerOf(c.path, c.after);
5646
+ const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
5647
+ for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
5444
5648
  out2.push(
5445
5649
  makeFinding(RULE7, policy, {
5446
5650
  file: c.path,
@@ -5550,9 +5754,48 @@ var testDeletion = {
5550
5754
  }
5551
5755
  }
5552
5756
  }
5757
+ out2.push(...pytestPrecedenceFindings(changes, policy, ctx));
5553
5758
  return out2;
5554
5759
  }
5555
5760
  };
5761
+ function pytestPrecedenceFindings(changes, policy, ctx) {
5762
+ const order = PYTEST_INI_ORDER;
5763
+ const touched = /* @__PURE__ */ new Map();
5764
+ for (const c of changes) {
5765
+ if (c.kind !== "file" || c.oldPath) continue;
5766
+ if (order.includes(c.path)) touched.set(c.path, c);
5767
+ }
5768
+ if (touched.size === 0) return [];
5769
+ const listing = trackedFiles(ctx);
5770
+ if (!listing) return [];
5771
+ const rootNames = new Set(listing.filter((f) => !f.includes("/")));
5772
+ const base = /* @__PURE__ */ new Map();
5773
+ const head = /* @__PURE__ */ new Map();
5774
+ for (const name of order) {
5775
+ const c = touched.get(name);
5776
+ if (c) {
5777
+ base.set(name, c.before);
5778
+ head.set(name, c.after);
5779
+ continue;
5780
+ }
5781
+ if (!rootNames.has(name)) continue;
5782
+ const src = trackedContent(name, ctx);
5783
+ base.set(name, src);
5784
+ head.set(name, src);
5785
+ }
5786
+ const was = effectivePytestFile(base);
5787
+ const now = effectivePytestFile(head);
5788
+ if (!was || !now || was.path === now.path) return [];
5789
+ const samples = runnerSamples(now.path, policy, ctx, "pytest");
5790
+ return suiteNarrowings(was.content, now.content, now.path, samples).map(
5791
+ (n) => makeFinding(RULE7, policy, {
5792
+ file: now.path,
5793
+ message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
5794
+ evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
5795
+ remediation: "pytest reads ONE config file. Adding a higher-precedence one, or deleting the one in force, changes which settings apply \u2014 keep the effective configuration selecting the whole suite."
5796
+ })
5797
+ );
5798
+ }
5556
5799
 
5557
5800
  // src/detectors/test-content-removal.ts
5558
5801
  import ts5 from "typescript";
@@ -5838,8 +6081,8 @@ function hasBlocking(findings) {
5838
6081
  }
5839
6082
 
5840
6083
  // src/policy-load.ts
5841
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
5842
- import { isAbsolute as isAbsolute4, join as join4, posix as posix2 } from "node:path";
6084
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
6085
+ import { isAbsolute as isAbsolute4, join as join5, posix as posix2 } from "node:path";
5843
6086
  import { parse as parse2 } from "yaml";
5844
6087
  var PolicyError = class extends Error {
5845
6088
  };
@@ -5963,9 +6206,9 @@ function parseOrThrow(src, where) {
5963
6206
  return parsePolicy(raw, where);
5964
6207
  }
5965
6208
  function loadPolicy(cwd = process.cwd()) {
5966
- const path = join4(cwd, POLICY_FILE);
6209
+ const path = join5(cwd, POLICY_FILE);
5967
6210
  if (!existsSync3(path)) return defaultPolicy();
5968
- return parseOrThrow(readFileSync3(path, "utf8"), POLICY_FILE);
6211
+ return parseOrThrow(readFileSync4(path, "utf8"), POLICY_FILE);
5969
6212
  }
5970
6213
  function loadPolicyAt(rev, cwd) {
5971
6214
  const src = fileAt(rev, POLICY_FILE, { cwd });
@@ -5975,8 +6218,8 @@ function loadPolicyAt(rev, cwd) {
5975
6218
 
5976
6219
  // src/signoff.ts
5977
6220
  import { createHash } from "node:crypto";
5978
- import { appendFileSync, mkdirSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
5979
- import { dirname as dirname2, join as join5 } from "node:path";
6221
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync5, existsSync as existsSync4 } from "node:fs";
6222
+ import { dirname as dirname2, join as join6 } from "node:path";
5980
6223
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
5981
6224
  function fingerprint(rule, file, evidence) {
5982
6225
  return createHash("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
@@ -5987,13 +6230,13 @@ function ledgerPath(cwd, policy) {
5987
6230
  if (!ledgerInsideRepo(rel)) {
5988
6231
  throw new PolicyError(`signoff.ledger must be a relative path inside the repository, got ${JSON.stringify(rel)}`);
5989
6232
  }
5990
- return join5(cwd, rel);
6233
+ return join6(cwd, rel);
5991
6234
  }
5992
6235
  function readLedger(cwd, policy) {
5993
6236
  const p = ledgerPath(cwd, policy);
5994
6237
  if (!existsSync4(p)) return [];
5995
6238
  const out2 = [];
5996
- for (const line of readFileSync4(p, "utf8").split("\n")) {
6239
+ for (const line of readFileSync5(p, "utf8").split("\n")) {
5997
6240
  if (!line.trim()) continue;
5998
6241
  try {
5999
6242
  const e = JSON.parse(line);
@@ -6363,14 +6606,14 @@ function runCheck(opts) {
6363
6606
  }
6364
6607
 
6365
6608
  // src/cli/hook.ts
6366
- import { readFileSync as readFileSync9, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
6609
+ import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
6367
6610
  import { createHash as createHash4 } from "node:crypto";
6368
6611
 
6369
6612
  // src/adapters/claude/changes.ts
6370
6613
  import { execFileSync as execFileSync3 } from "node:child_process";
6371
6614
  import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
6372
6615
  import { tmpdir } from "node:os";
6373
- import { isAbsolute as isAbsolute5, join as join6, relative as relative2, resolve as resolve2 } from "node:path";
6616
+ import { isAbsolute as isAbsolute5, join as join7, relative as relative2, resolve as resolve2 } from "node:path";
6374
6617
  function asStr(v) {
6375
6618
  return typeof v === "string" ? v : "";
6376
6619
  }
@@ -6395,11 +6638,11 @@ function synthFileChange(displayPath, before, after) {
6395
6638
  if (before === null) op = "add";
6396
6639
  else if (after === null) op = "delete";
6397
6640
  else op = "modify";
6398
- const dir = mkdtempSync(join6(tmpdir(), "hf-"));
6641
+ const dir = mkdtempSync(join7(tmpdir(), "hf-"));
6399
6642
  let raw = "";
6400
6643
  try {
6401
- const a = join6(dir, "a");
6402
- const b = join6(dir, "b");
6644
+ const a = join7(dir, "a");
6645
+ const b = join7(dir, "b");
6403
6646
  writeFileSync(a, before ?? "");
6404
6647
  writeFileSync(b, after ?? "");
6405
6648
  try {
@@ -6484,14 +6727,14 @@ function formatDenial(blocks) {
6484
6727
  }
6485
6728
 
6486
6729
  // src/session.ts
6487
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
6488
- import { dirname as dirname3, join as join7 } from "node:path";
6730
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
6731
+ import { dirname as dirname3, join as join8 } from "node:path";
6489
6732
  var UNSAFE = /[^A-Za-z0-9_-]/g;
6490
6733
  var SHA = /^[0-9a-f]{7,40}$/;
6491
6734
  function baselinePath(cwd, sessionId) {
6492
6735
  const gd = gitDir(cwd);
6493
6736
  if (!gd) return null;
6494
- return join7(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
6737
+ return join8(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
6495
6738
  }
6496
6739
  function turnBaseline(cwd, sessionId) {
6497
6740
  if (!sessionId) return null;
@@ -6499,7 +6742,7 @@ function turnBaseline(cwd, sessionId) {
6499
6742
  const p = baselinePath(cwd, sessionId);
6500
6743
  if (!p) return null;
6501
6744
  if (existsSync5(p)) {
6502
- const v = readFileSync5(p, "utf8").trim();
6745
+ const v = readFileSync6(p, "utf8").trim();
6503
6746
  if (SHA.test(v)) return v;
6504
6747
  }
6505
6748
  const head = headSha(cwd);
@@ -6525,8 +6768,8 @@ function advanceTurnBaseline(cwd, sessionId) {
6525
6768
 
6526
6769
  // src/effect.ts
6527
6770
  import { createHash as createHash2 } from "node:crypto";
6528
- import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
6529
- import { isAbsolute as isAbsolute6, join as join8 } from "node:path";
6771
+ import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
6772
+ import { isAbsolute as isAbsolute6, join as join9 } from "node:path";
6530
6773
  import { execFileSync as execFileSync4 } from "node:child_process";
6531
6774
  var UNSAFE2 = /[^A-Za-z0-9._-]/g;
6532
6775
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
@@ -6538,7 +6781,7 @@ function gitDir2(cwd) {
6538
6781
  stdio: ["ignore", "pipe", "pipe"]
6539
6782
  }).trim();
6540
6783
  if (!d) return null;
6541
- return isAbsolute6(d) ? d : join8(cwd, d);
6784
+ return isAbsolute6(d) ? d : join9(cwd, d);
6542
6785
  } catch {
6543
6786
  return null;
6544
6787
  }
@@ -6547,8 +6790,8 @@ function statePath(cwd, sessionId, kind) {
6547
6790
  if (!sessionId) return null;
6548
6791
  const gd = gitDir2(cwd);
6549
6792
  if (!gd) return null;
6550
- const dir = join8(gd, "tamperward");
6551
- return join8(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
6793
+ const dir = join9(gd, "tamperward");
6794
+ return join9(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
6552
6795
  }
6553
6796
  function ptreePath(cwd, sessionId) {
6554
6797
  return statePath(cwd, sessionId, "ptree");
@@ -6577,7 +6820,7 @@ function snapshotProtected(cwd, policy, prev) {
6577
6820
  } catch {
6578
6821
  }
6579
6822
  const walk2 = (rel) => {
6580
- const abs2 = rel ? join8(cwd, rel) : cwd;
6823
+ const abs2 = rel ? join9(cwd, rel) : cwd;
6581
6824
  let names;
6582
6825
  try {
6583
6826
  names = readdirSync(abs2);
@@ -6590,7 +6833,7 @@ function snapshotProtected(cwd, policy, prev) {
6590
6833
  if (pruned.has(r)) continue;
6591
6834
  let st;
6592
6835
  try {
6593
- st = lstatSync2(join8(cwd, r));
6836
+ st = lstatSync2(join9(cwd, r));
6594
6837
  } catch {
6595
6838
  continue;
6596
6839
  }
@@ -6619,7 +6862,7 @@ function driftBetween(expected, current) {
6619
6862
  function loadTree(p) {
6620
6863
  if (!p || !existsSync6(p)) return null;
6621
6864
  try {
6622
- return JSON.parse(readFileSync6(p, "utf8"));
6865
+ return JSON.parse(readFileSync7(p, "utf8"));
6623
6866
  } catch {
6624
6867
  return null;
6625
6868
  }
@@ -6627,7 +6870,7 @@ function loadTree(p) {
6627
6870
  function saveTree(p, tree) {
6628
6871
  if (!p) return;
6629
6872
  try {
6630
- mkdirSync3(join8(p, ".."), { recursive: true });
6873
+ mkdirSync3(join9(p, ".."), { recursive: true });
6631
6874
  const tmp = `${p}.${process.pid}.tmp`;
6632
6875
  writeFileSync3(tmp, JSON.stringify(tree));
6633
6876
  renameSync(tmp, p);
@@ -6659,7 +6902,7 @@ function dropTurnTree(cwd, sessionId) {
6659
6902
 
6660
6903
  // src/cli/watch.ts
6661
6904
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
6662
- import { join as join9 } from "node:path";
6905
+ import { join as join10 } from "node:path";
6663
6906
  import { createHash as createHash3 } from "node:crypto";
6664
6907
  var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
6665
6908
  function snap(abs2) {
@@ -6671,7 +6914,7 @@ function snap(abs2) {
6671
6914
  }
6672
6915
  function defaultEventLog(cwd) {
6673
6916
  if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
6674
- return join9(gitDir(cwd) ?? join9(cwd, ".git"), "tamperward", "fsevents.jsonl");
6917
+ return join10(gitDir(cwd) ?? join10(cwd, ".git"), "tamperward", "fsevents.jsonl");
6675
6918
  }
6676
6919
  function watchTree(dir, cb) {
6677
6920
  if (process.env.TAMPERWARD_WATCH_NO_RECURSIVE !== "1") {
@@ -6688,12 +6931,12 @@ function watchTree(dir, cb) {
6688
6931
  if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
6689
6932
  let w;
6690
6933
  try {
6691
- w = watch(rel ? join9(dir, rel) : dir, (kind, fname) => {
6934
+ w = watch(rel ? join10(dir, rel) : dir, (kind, fname) => {
6692
6935
  if (!fname) return;
6693
6936
  const child = rel ? `${rel}/${String(fname)}` : String(fname);
6694
6937
  cb(kind, child);
6695
6938
  try {
6696
- if (statSync(join9(dir, child)).isDirectory()) addDir(child);
6939
+ if (statSync(join10(dir, child)).isDirectory()) addDir(child);
6697
6940
  } catch {
6698
6941
  }
6699
6942
  });
@@ -6706,7 +6949,7 @@ function watchTree(dir, cb) {
6706
6949
  addDir(rel);
6707
6950
  let names = [];
6708
6951
  try {
6709
- names = readdirSync2(rel ? join9(dir, rel) : dir);
6952
+ names = readdirSync2(rel ? join10(dir, rel) : dir);
6710
6953
  } catch {
6711
6954
  return;
6712
6955
  }
@@ -6714,7 +6957,7 @@ function watchTree(dir, cb) {
6714
6957
  const child = rel ? `${rel}/${name}` : name;
6715
6958
  if (SKIP2.test(child + "/")) continue;
6716
6959
  try {
6717
- if (statSync(join9(dir, child)).isDirectory()) walk2(child);
6960
+ if (statSync(join10(dir, child)).isDirectory()) walk2(child);
6718
6961
  } catch {
6719
6962
  }
6720
6963
  }
@@ -6726,13 +6969,13 @@ function watchTree(dir, cb) {
6726
6969
  }
6727
6970
  function startWatcher(dir, log, policy) {
6728
6971
  try {
6729
- mkdirSync4(join9(log, ".."), { recursive: true });
6972
+ mkdirSync4(join10(log, ".."), { recursive: true });
6730
6973
  } catch {
6731
6974
  }
6732
6975
  const last = /* @__PURE__ */ new Map();
6733
6976
  return watchTree(dir, (kind, rel) => {
6734
6977
  if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
6735
- const s = snap(join9(dir, rel));
6978
+ const s = snap(join10(dir, rel));
6736
6979
  const key2 = `${s.hash}:${s.mode}`;
6737
6980
  if (last.get(rel) === key2) return;
6738
6981
  last.set(rel, key2);
@@ -6759,7 +7002,7 @@ function runWatch(args) {
6759
7002
  }
6760
7003
 
6761
7004
  // src/detectors/fs-events.ts
6762
- import { readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
7005
+ import { readFileSync as readFileSync9, statSync as statSync2 } from "node:fs";
6763
7006
  var RULE11 = "transient-protected-mutation";
6764
7007
  function readEvents(log, offset) {
6765
7008
  let size = 0;
@@ -6769,7 +7012,7 @@ function readEvents(log, offset) {
6769
7012
  return { events: [], newOffset: offset };
6770
7013
  }
6771
7014
  if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
6772
- const raw = readFileSync8(log, "utf8").slice(offset);
7015
+ const raw = readFileSync9(log, "utf8").slice(offset);
6773
7016
  const events = [];
6774
7017
  for (const line of raw.split("\n")) {
6775
7018
  if (!line.trim()) continue;
@@ -6822,7 +7065,7 @@ var HookInputError = class extends Error {
6822
7065
  };
6823
7066
  function readStdin() {
6824
7067
  try {
6825
- return readFileSync9(0, "utf8");
7068
+ return readFileSync10(0, "utf8");
6826
7069
  } catch (e) {
6827
7070
  throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
6828
7071
  }
@@ -7078,7 +7321,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
7078
7321
  if (!existsSync7(log)) return none;
7079
7322
  let offset = 0;
7080
7323
  if (cp && existsSync7(cp)) {
7081
- const n = Number(readFileSync9(cp, "utf8"));
7324
+ const n = Number(readFileSync10(cp, "utf8"));
7082
7325
  offset = Number.isFinite(n) ? n : 0;
7083
7326
  }
7084
7327
  const { events, newOffset } = readEvents(log, offset);
@@ -7231,8 +7474,8 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
7231
7474
  // src/cli/init.ts
7232
7475
  import { execFileSync as execFileSync5 } from "node:child_process";
7233
7476
  import { createHash as createHash5 } from "node:crypto";
7234
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
7235
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute7, join as join10, relative as relative3, resolve as resolve3 } from "node:path";
7477
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
7478
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute7, join as join11, relative as relative3, resolve as resolve3 } from "node:path";
7236
7479
  var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
7237
7480
  var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7238
7481
  function gitConfig(cwd, key2) {
@@ -7408,7 +7651,7 @@ function hooksShapeError(hooks) {
7408
7651
  return null;
7409
7652
  }
7410
7653
  function planPolicy(cwd) {
7411
- const path = join10(cwd, POLICY_FILE);
7654
+ const path = join11(cwd, POLICY_FILE);
7412
7655
  if (existsSync8(path)) {
7413
7656
  try {
7414
7657
  loadPolicy(cwd);
@@ -7427,12 +7670,12 @@ function planPolicy(cwd) {
7427
7670
  }
7428
7671
  function planClaudeHooks(cwd) {
7429
7672
  const rel = ".claude/settings.json";
7430
- const path = join10(cwd, rel);
7673
+ const path = join11(cwd, rel);
7431
7674
  let settings = {};
7432
7675
  if (existsSync8(path)) {
7433
7676
  let parsed;
7434
7677
  try {
7435
- parsed = JSON.parse(readFileSync10(path, "utf8"));
7678
+ parsed = JSON.parse(readFileSync11(path, "utf8"));
7436
7679
  } catch {
7437
7680
  return {
7438
7681
  item: "agent",
@@ -7528,7 +7771,7 @@ function hooksDir(cwd) {
7528
7771
  if (out2) return resolve3(cwd, out2);
7529
7772
  } catch {
7530
7773
  }
7531
- return existsSync8(join10(cwd, ".git")) ? join10(cwd, ".git", "hooks") : null;
7774
+ return existsSync8(join11(cwd, ".git")) ? join11(cwd, ".git", "hooks") : null;
7532
7775
  }
7533
7776
  function display(cwd, path) {
7534
7777
  const rel = relative3(cwd, path);
@@ -7555,30 +7798,30 @@ function planPreCommit(cwd) {
7555
7798
  const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
7556
7799
  let path;
7557
7800
  let note;
7558
- if (existsSync8(join10(cwd, ".husky"))) {
7559
- path = join10(cwd, ".husky", "pre-commit");
7801
+ if (existsSync8(join11(cwd, ".husky"))) {
7802
+ path = join11(cwd, ".husky", "pre-commit");
7560
7803
  note = "husky";
7561
7804
  } else {
7562
- const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(join10(cwd, c))));
7805
+ const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(join11(cwd, c))));
7563
7806
  if (managed) {
7564
7807
  return skip(`${managed.name} manages this repository's hooks and regenerates them on install, so a line in the hooks directory would not last \u2014 ${managed.hint}`);
7565
7808
  }
7566
7809
  const dir = hooksDir(cwd);
7567
7810
  if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
7568
7811
  if (basename2(dir) === "_" && basename2(dirname4(dir)) === ".husky") {
7569
- path = join10(dirname4(dir), "pre-commit");
7812
+ path = join11(dirname4(dir), "pre-commit");
7570
7813
  note = "husky";
7571
7814
  } else {
7572
- path = join10(dir, "pre-commit");
7815
+ path = join11(dir, "pre-commit");
7573
7816
  const rel2 = display(cwd, dir);
7574
- note = rel2 === join10(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
7817
+ note = rel2 === join11(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
7575
7818
  }
7576
7819
  }
7577
7820
  const rel = display(cwd, path);
7578
7821
  if (existsSync8(path) && !statSync3(path).isFile()) {
7579
7822
  return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
7580
7823
  }
7581
- const existing = existsSync8(path) ? readFileSync10(path, "utf8") : null;
7824
+ const existing = existsSync8(path) ? readFileSync11(path, "utf8") : null;
7582
7825
  if (existing === null) {
7583
7826
  return {
7584
7827
  item: "pre-commit",
@@ -7667,10 +7910,10 @@ ${block.join("\n")}
7667
7910
  function planCodeowners(cwd) {
7668
7911
  const rel = ".github/CODEOWNERS";
7669
7912
  const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
7670
- (r) => existsSync8(join10(cwd, r))
7913
+ (r) => existsSync8(join11(cwd, r))
7671
7914
  );
7672
- const path = join10(cwd, existingRel ?? rel);
7673
- const existing = existingRel ? readFileSync10(path, "utf8") : null;
7915
+ const path = join11(cwd, existingRel ?? rel);
7916
+ const existing = existingRel ? readFileSync11(path, "utf8") : null;
7674
7917
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
7675
7918
  if (missing.length === 0) {
7676
7919
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
@@ -7703,7 +7946,7 @@ ${CODEOWNERS_MARK}
7703
7946
  }
7704
7947
  function planWorkflow(cwd, force) {
7705
7948
  const rel = ".github/workflows/tamperward.yml";
7706
- const path = join10(cwd, rel);
7949
+ const path = join11(cwd, rel);
7707
7950
  const write = () => {
7708
7951
  mkdirSync5(dirname4(path), { recursive: true });
7709
7952
  writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
@@ -7719,7 +7962,7 @@ function planWorkflow(cwd, force) {
7719
7962
  }
7720
7963
  let src;
7721
7964
  try {
7722
- src = readFileSync10(path, "utf8");
7965
+ src = readFileSync11(path, "utf8");
7723
7966
  } catch {
7724
7967
  return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
7725
7968
  }
@@ -7818,15 +8061,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
7818
8061
  // src/cli/verify.ts
7819
8062
  import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
7820
8063
  import { createHash as createHash8 } from "node:crypto";
7821
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
8064
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
7822
8065
  import { tmpdir as tmpdir2 } from "node:os";
7823
- import { dirname as dirname5, join as join13, resolve as resolve4 } from "node:path";
8066
+ import { dirname as dirname5, join as join14, resolve as resolve4 } from "node:path";
7824
8067
 
7825
8068
  // src/git/trusted.ts
7826
8069
  import { execFileSync as execFileSync6 } from "node:child_process";
7827
8070
  import { createHash as createHash6 } from "node:crypto";
7828
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
7829
- import { isAbsolute as isAbsolute8, join as join11 } from "node:path";
8071
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "node:fs";
8072
+ import { isAbsolute as isAbsolute8, join as join12 } from "node:path";
7830
8073
  function trustedGitEnv() {
7831
8074
  return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
7832
8075
  }
@@ -7839,7 +8082,7 @@ function gitDirOf(cwd) {
7839
8082
  env: trustedGitEnv()
7840
8083
  }).trim();
7841
8084
  if (!d) return null;
7842
- return isAbsolute8(d) ? d : join11(cwd, d);
8085
+ return isAbsolute8(d) ? d : join12(cwd, d);
7843
8086
  } catch {
7844
8087
  return null;
7845
8088
  }
@@ -7864,9 +8107,9 @@ function objectRewriteState(cwd) {
7864
8107
  parts.push(`${rel}:<no-git-dir>`);
7865
8108
  continue;
7866
8109
  }
7867
- const p = join11(dir, rel);
8110
+ const p = join12(dir, rel);
7868
8111
  try {
7869
- parts.push(`${rel}:` + (existsSync9(p) ? readFileSync11(p, "utf8").trim() : ""));
8112
+ parts.push(`${rel}:` + (existsSync9(p) ? readFileSync12(p, "utf8").trim() : ""));
7870
8113
  } catch {
7871
8114
  parts.push(`${rel}:<unreadable>`);
7872
8115
  }
@@ -7877,8 +8120,8 @@ function objectRewriteState(cwd) {
7877
8120
  // src/fingerprint.ts
7878
8121
  import { execFileSync as execFileSync7 } from "node:child_process";
7879
8122
  import { createHash as createHash7 } from "node:crypto";
7880
- import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
7881
- import { join as join12 } from "node:path";
8123
+ import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
8124
+ import { join as join13 } from "node:path";
7882
8125
  function treeFingerprint(cwd) {
7883
8126
  const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
7884
8127
  cwd,
@@ -7896,7 +8139,7 @@ function treeFingerprint(cwd) {
7896
8139
  return h.digest("hex");
7897
8140
  }
7898
8141
  function depsFingerprint(cwd) {
7899
- const root = join12(cwd, "node_modules");
8142
+ const root = join13(cwd, "node_modules");
7900
8143
  const h = createHash7("sha256");
7901
8144
  const walk2 = (dir) => {
7902
8145
  let entries;
@@ -7906,12 +8149,12 @@ function depsFingerprint(cwd) {
7906
8149
  return;
7907
8150
  }
7908
8151
  for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
7909
- const p = join12(dir, e.name);
8152
+ const p = join13(dir, e.name);
7910
8153
  if (e.isDirectory()) walk2(p);
7911
8154
  else if (e.isFile()) {
7912
8155
  h.update(p);
7913
8156
  try {
7914
- h.update(readFileSync12(p));
8157
+ h.update(readFileSync13(p));
7915
8158
  h.update(String(statSync4(p).mode));
7916
8159
  } catch {
7917
8160
  h.update("<unreadable>");
@@ -8026,7 +8269,7 @@ function baseIsAncestorOfHead(base, cwd) {
8026
8269
  function materialize(cwd, dest) {
8027
8270
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8028
8271
  for (const rel of listed) {
8029
- const src = join13(cwd, rel);
8272
+ const src = join14(cwd, rel);
8030
8273
  let st;
8031
8274
  try {
8032
8275
  st = statSync5(src);
@@ -8034,18 +8277,18 @@ function materialize(cwd, dest) {
8034
8277
  continue;
8035
8278
  }
8036
8279
  if (!st.isFile()) continue;
8037
- const out2 = join13(dest, rel);
8280
+ const out2 = join14(dest, rel);
8038
8281
  mkdirSync6(dirname5(out2), { recursive: true });
8039
8282
  dropSymlink(out2);
8040
8283
  if (lstatSync3(src).isSymbolicLink()) {
8041
- writeFileSync6(out2, readFileSync13(src));
8284
+ writeFileSync6(out2, readFileSync14(src));
8042
8285
  } else {
8043
8286
  cpSync(src, out2, { dereference: true });
8044
8287
  }
8045
8288
  chmodSync2(out2, st.mode);
8046
8289
  }
8047
8290
  const nm = resolve4(cwd, "node_modules");
8048
- if (existsSync10(nm)) symlinkSync(nm, join13(dest, "node_modules"), "dir");
8291
+ if (existsSync10(nm)) symlinkSync(nm, join14(dest, "node_modules"), "dir");
8049
8292
  }
8050
8293
  function dropSymlink(p) {
8051
8294
  try {
@@ -8078,11 +8321,11 @@ function overlayDigest(dest, paths) {
8078
8321
  for (const rel of [...paths].sort()) {
8079
8322
  h.update(rel);
8080
8323
  h.update("\0");
8081
- const p = join13(dest, rel);
8324
+ const p = join14(dest, rel);
8082
8325
  try {
8083
8326
  const st = lstatSync3(p);
8084
8327
  h.update(String(st.mode));
8085
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync13(p));
8328
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
8086
8329
  } catch {
8087
8330
  h.update("<gone>");
8088
8331
  }
@@ -8105,7 +8348,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8105
8348
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
8106
8349
  }
8107
8350
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
8108
- const out2 = join13(dest, e.path);
8351
+ const out2 = join14(dest, e.path);
8109
8352
  mkdirSync6(dirname5(out2), { recursive: true });
8110
8353
  rmSync3(out2, { force: true });
8111
8354
  if (e.mode === "120000") {
@@ -8120,7 +8363,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8120
8363
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8121
8364
  for (const rel of inCopy) {
8122
8365
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
8123
- rmSync3(join13(dest, rel), { force: true });
8366
+ rmSync3(join14(dest, rel), { force: true });
8124
8367
  removed++;
8125
8368
  }
8126
8369
  return { restored, removed };
@@ -8162,8 +8405,8 @@ function suiteEnv(scratch) {
8162
8405
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
8163
8406
  env[k] = v;
8164
8407
  }
8165
- const userRc = join13(scratch, "npmrc-user");
8166
- const globalRc = join13(scratch, "npmrc-global");
8408
+ const userRc = join14(scratch, "npmrc-user");
8409
+ const globalRc = join14(scratch, "npmrc-global");
8167
8410
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
8168
8411
  env.npm_config_userconfig = userRc;
8169
8412
  env.npm_config_globalconfig = globalRc;
@@ -8173,8 +8416,8 @@ function suiteEnv(scratch) {
8173
8416
  }
8174
8417
  function runSuite(dir, cmd, budgetSecs) {
8175
8418
  const t0 = Date.now();
8176
- const outDir = mkdtempSync2(join13(tmpdir2(), "tw-verify-run-"));
8177
- const outFile = join13(outDir, "result.json");
8419
+ const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
8420
+ const outFile = join14(outDir, "result.json");
8178
8421
  try {
8179
8422
  spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
8180
8423
  cwd: dir,
@@ -8187,7 +8430,7 @@ function runSuite(dir, cmd, budgetSecs) {
8187
8430
  const secs = Math.round((Date.now() - t0) / 1e3);
8188
8431
  let r;
8189
8432
  try {
8190
- r = JSON.parse(readFileSync13(outFile, "utf8"));
8433
+ r = JSON.parse(readFileSync14(outFile, "utf8"));
8191
8434
  } catch {
8192
8435
  return { exit: null, secs };
8193
8436
  }
@@ -8238,8 +8481,8 @@ function runVerify(opts) {
8238
8481
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
8239
8482
  return 2;
8240
8483
  }
8241
- const visRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-vis-"));
8242
- const visDir = join13(visRoot, "t");
8484
+ const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
8485
+ const visDir = join14(visRoot, "t");
8243
8486
  const cleanup = (dirs) => {
8244
8487
  if (opts.keep) return;
8245
8488
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
@@ -8267,8 +8510,8 @@ function runVerify(opts) {
8267
8510
  out2("pristine run executes through it too. Failing closed, not open.");
8268
8511
  return 2;
8269
8512
  }
8270
- const priRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-pri-"));
8271
- const priDir = join13(priRoot, "t");
8513
+ const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
8514
+ const priDir = join14(priRoot, "t");
8272
8515
  let restored = [];
8273
8516
  let removedAdded = 0;
8274
8517
  try {
@@ -8380,7 +8623,7 @@ function parseVerify(args) {
8380
8623
 
8381
8624
  // src/cli/run.ts
8382
8625
  import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8383
- import { readdirSync as readdirSync4, readFileSync as readFileSync14, realpathSync as realpathSync3 } from "node:fs";
8626
+ import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync as realpathSync3 } from "node:fs";
8384
8627
  import { resolve as resolve5 } from "node:path";
8385
8628
  var out = (s) => process.stdout.write(s + "\n");
8386
8629
  var err = (s) => process.stderr.write(s + "\n");
@@ -8389,7 +8632,7 @@ function git3(args, cwd) {
8389
8632
  }
8390
8633
  function nowTicks() {
8391
8634
  try {
8392
- return Math.floor(parseFloat(readFileSync14("/proc/uptime", "utf8").split(" ")[0]) * 100);
8635
+ return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
8393
8636
  } catch {
8394
8637
  return Number.POSITIVE_INFINITY;
8395
8638
  }
@@ -8419,7 +8662,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8419
8662
  }
8420
8663
  if (held !== real && !held.startsWith(real + "/")) continue;
8421
8664
  try {
8422
- const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
8665
+ const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
8423
8666
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
8424
8667
  if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
8425
8668
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.10.0",
3
+ "version": "2.10.2",
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",