tamperward 2.10.1 → 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 +292 -105
  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
  }
@@ -4673,7 +4704,7 @@ function huskyRuntimeWrite(path, content, ctx) {
4673
4704
  if (!ok) return false;
4674
4705
  if (!ctx?.cwd) return false;
4675
4706
  const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
4676
- const entry = inspectRel(join3(ctx.cwd, root), ".husky/pre-commit");
4707
+ const entry = inspectRel(join4(ctx.cwd, root), ".husky/pre-commit");
4677
4708
  if (entry.kind !== "file") return false;
4678
4709
  const gate = textOf(entry);
4679
4710
  if (gate == null) return false;
@@ -4953,6 +4984,8 @@ import ts3 from "typescript";
4953
4984
  var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
4954
4985
  var JEST_DEFAULT_IGNORE = ["/node_modules/"];
4955
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/**", "**/.*/**"];
4956
4989
  var VITEST_DEFAULT_EXCLUDE = [
4957
4990
  "**/node_modules/**",
4958
4991
  "**/dist/**",
@@ -4970,6 +5003,13 @@ var CANONICAL_SAMPLES = [
4970
5003
  "a.spec.js",
4971
5004
  "test/a.test.mjs"
4972
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
+ ];
4973
5013
  function keyName2(name) {
4974
5014
  if (ts3.isIdentifier(name) || ts3.isStringLiteral(name)) return name.text;
4975
5015
  return null;
@@ -5004,8 +5044,104 @@ function runnerOf(path, content) {
5004
5044
  if (/^jest\.config\./.test(base) || base === "package.json") return "jest";
5005
5045
  if (/^vitest\.(?:config|workspace)\./.test(base)) return "vitest";
5006
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
+ }
5007
5066
  return null;
5008
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
+ }
5009
5145
  var isWorkspaceFile = (path) => /^vitest\.workspace\./.test(path.split("/").pop() ?? path);
5010
5146
  var empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, opaque: false, present: false });
5011
5147
  var stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
@@ -5154,6 +5290,7 @@ function workspaceArray(sf) {
5154
5290
  }
5155
5291
  function parseSelection(src, runner, path = "") {
5156
5292
  let sel = empty();
5293
+ if (runner === "pytest") return pytestSelection(src);
5157
5294
  try {
5158
5295
  const sf = ts3.createSourceFile("cfg.ts", asExpression2(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
5159
5296
  if (runner === "vitest" && isWorkspaceFile(path)) {
@@ -5202,10 +5339,14 @@ function predicate(sel, runner) {
5202
5339
  why: (p) => ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
5203
5340
  };
5204
5341
  }
5205
- const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : VITEST_DEFAULT_INCLUDE);
5206
- 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"));
5207
5348
  const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
5208
- 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));
5209
5350
  const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
5210
5351
  const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
5211
5352
  const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
@@ -5220,7 +5361,10 @@ function predicate(sel, runner) {
5220
5361
  if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
5221
5362
  if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
5222
5363
  const hit = ignoredBy(r);
5223
- 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
+ }
5224
5368
  const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
5225
5369
  return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
5226
5370
  }
@@ -5370,12 +5514,14 @@ function significantLines(src, path) {
5370
5514
  return out2;
5371
5515
  }
5372
5516
  var OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
5373
- 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;
5374
5520
  const files = trackedFiles(ctx);
5375
- if (!files) return CANONICAL_SAMPLES;
5521
+ if (!files) return fallback;
5376
5522
  const root = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/") + 1) : "";
5377
- 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));
5378
- 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;
5379
5525
  }
5380
5526
  var WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
5381
5527
  function excludesOtherRunner(flag, cmd) {
@@ -5496,7 +5642,9 @@ var testDeletion = {
5496
5642
  );
5497
5643
  }
5498
5644
  } else if (c.op !== "delete" && c.after != null && !isTest && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
5499
- 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)) : []) {
5500
5648
  out2.push(
5501
5649
  makeFinding(RULE7, policy, {
5502
5650
  file: c.path,
@@ -5606,9 +5754,48 @@ var testDeletion = {
5606
5754
  }
5607
5755
  }
5608
5756
  }
5757
+ out2.push(...pytestPrecedenceFindings(changes, policy, ctx));
5609
5758
  return out2;
5610
5759
  }
5611
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
+ }
5612
5799
 
5613
5800
  // src/detectors/test-content-removal.ts
5614
5801
  import ts5 from "typescript";
@@ -5894,8 +6081,8 @@ function hasBlocking(findings) {
5894
6081
  }
5895
6082
 
5896
6083
  // src/policy-load.ts
5897
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
5898
- 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";
5899
6086
  import { parse as parse2 } from "yaml";
5900
6087
  var PolicyError = class extends Error {
5901
6088
  };
@@ -6019,9 +6206,9 @@ function parseOrThrow(src, where) {
6019
6206
  return parsePolicy(raw, where);
6020
6207
  }
6021
6208
  function loadPolicy(cwd = process.cwd()) {
6022
- const path = join4(cwd, POLICY_FILE);
6209
+ const path = join5(cwd, POLICY_FILE);
6023
6210
  if (!existsSync3(path)) return defaultPolicy();
6024
- return parseOrThrow(readFileSync3(path, "utf8"), POLICY_FILE);
6211
+ return parseOrThrow(readFileSync4(path, "utf8"), POLICY_FILE);
6025
6212
  }
6026
6213
  function loadPolicyAt(rev, cwd) {
6027
6214
  const src = fileAt(rev, POLICY_FILE, { cwd });
@@ -6031,8 +6218,8 @@ function loadPolicyAt(rev, cwd) {
6031
6218
 
6032
6219
  // src/signoff.ts
6033
6220
  import { createHash } from "node:crypto";
6034
- import { appendFileSync, mkdirSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
6035
- 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";
6036
6223
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
6037
6224
  function fingerprint(rule, file, evidence) {
6038
6225
  return createHash("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
@@ -6043,13 +6230,13 @@ function ledgerPath(cwd, policy) {
6043
6230
  if (!ledgerInsideRepo(rel)) {
6044
6231
  throw new PolicyError(`signoff.ledger must be a relative path inside the repository, got ${JSON.stringify(rel)}`);
6045
6232
  }
6046
- return join5(cwd, rel);
6233
+ return join6(cwd, rel);
6047
6234
  }
6048
6235
  function readLedger(cwd, policy) {
6049
6236
  const p = ledgerPath(cwd, policy);
6050
6237
  if (!existsSync4(p)) return [];
6051
6238
  const out2 = [];
6052
- for (const line of readFileSync4(p, "utf8").split("\n")) {
6239
+ for (const line of readFileSync5(p, "utf8").split("\n")) {
6053
6240
  if (!line.trim()) continue;
6054
6241
  try {
6055
6242
  const e = JSON.parse(line);
@@ -6419,14 +6606,14 @@ function runCheck(opts) {
6419
6606
  }
6420
6607
 
6421
6608
  // src/cli/hook.ts
6422
- 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";
6423
6610
  import { createHash as createHash4 } from "node:crypto";
6424
6611
 
6425
6612
  // src/adapters/claude/changes.ts
6426
6613
  import { execFileSync as execFileSync3 } from "node:child_process";
6427
6614
  import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
6428
6615
  import { tmpdir } from "node:os";
6429
- 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";
6430
6617
  function asStr(v) {
6431
6618
  return typeof v === "string" ? v : "";
6432
6619
  }
@@ -6451,11 +6638,11 @@ function synthFileChange(displayPath, before, after) {
6451
6638
  if (before === null) op = "add";
6452
6639
  else if (after === null) op = "delete";
6453
6640
  else op = "modify";
6454
- const dir = mkdtempSync(join6(tmpdir(), "hf-"));
6641
+ const dir = mkdtempSync(join7(tmpdir(), "hf-"));
6455
6642
  let raw = "";
6456
6643
  try {
6457
- const a = join6(dir, "a");
6458
- const b = join6(dir, "b");
6644
+ const a = join7(dir, "a");
6645
+ const b = join7(dir, "b");
6459
6646
  writeFileSync(a, before ?? "");
6460
6647
  writeFileSync(b, after ?? "");
6461
6648
  try {
@@ -6540,14 +6727,14 @@ function formatDenial(blocks) {
6540
6727
  }
6541
6728
 
6542
6729
  // src/session.ts
6543
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
6544
- 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";
6545
6732
  var UNSAFE = /[^A-Za-z0-9_-]/g;
6546
6733
  var SHA = /^[0-9a-f]{7,40}$/;
6547
6734
  function baselinePath(cwd, sessionId) {
6548
6735
  const gd = gitDir(cwd);
6549
6736
  if (!gd) return null;
6550
- return join7(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
6737
+ return join8(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
6551
6738
  }
6552
6739
  function turnBaseline(cwd, sessionId) {
6553
6740
  if (!sessionId) return null;
@@ -6555,7 +6742,7 @@ function turnBaseline(cwd, sessionId) {
6555
6742
  const p = baselinePath(cwd, sessionId);
6556
6743
  if (!p) return null;
6557
6744
  if (existsSync5(p)) {
6558
- const v = readFileSync5(p, "utf8").trim();
6745
+ const v = readFileSync6(p, "utf8").trim();
6559
6746
  if (SHA.test(v)) return v;
6560
6747
  }
6561
6748
  const head = headSha(cwd);
@@ -6581,8 +6768,8 @@ function advanceTurnBaseline(cwd, sessionId) {
6581
6768
 
6582
6769
  // src/effect.ts
6583
6770
  import { createHash as createHash2 } from "node:crypto";
6584
- import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
6585
- 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";
6586
6773
  import { execFileSync as execFileSync4 } from "node:child_process";
6587
6774
  var UNSAFE2 = /[^A-Za-z0-9._-]/g;
6588
6775
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
@@ -6594,7 +6781,7 @@ function gitDir2(cwd) {
6594
6781
  stdio: ["ignore", "pipe", "pipe"]
6595
6782
  }).trim();
6596
6783
  if (!d) return null;
6597
- return isAbsolute6(d) ? d : join8(cwd, d);
6784
+ return isAbsolute6(d) ? d : join9(cwd, d);
6598
6785
  } catch {
6599
6786
  return null;
6600
6787
  }
@@ -6603,8 +6790,8 @@ function statePath(cwd, sessionId, kind) {
6603
6790
  if (!sessionId) return null;
6604
6791
  const gd = gitDir2(cwd);
6605
6792
  if (!gd) return null;
6606
- const dir = join8(gd, "tamperward");
6607
- return join8(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
6793
+ const dir = join9(gd, "tamperward");
6794
+ return join9(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
6608
6795
  }
6609
6796
  function ptreePath(cwd, sessionId) {
6610
6797
  return statePath(cwd, sessionId, "ptree");
@@ -6633,7 +6820,7 @@ function snapshotProtected(cwd, policy, prev) {
6633
6820
  } catch {
6634
6821
  }
6635
6822
  const walk2 = (rel) => {
6636
- const abs2 = rel ? join8(cwd, rel) : cwd;
6823
+ const abs2 = rel ? join9(cwd, rel) : cwd;
6637
6824
  let names;
6638
6825
  try {
6639
6826
  names = readdirSync(abs2);
@@ -6646,7 +6833,7 @@ function snapshotProtected(cwd, policy, prev) {
6646
6833
  if (pruned.has(r)) continue;
6647
6834
  let st;
6648
6835
  try {
6649
- st = lstatSync2(join8(cwd, r));
6836
+ st = lstatSync2(join9(cwd, r));
6650
6837
  } catch {
6651
6838
  continue;
6652
6839
  }
@@ -6675,7 +6862,7 @@ function driftBetween(expected, current) {
6675
6862
  function loadTree(p) {
6676
6863
  if (!p || !existsSync6(p)) return null;
6677
6864
  try {
6678
- return JSON.parse(readFileSync6(p, "utf8"));
6865
+ return JSON.parse(readFileSync7(p, "utf8"));
6679
6866
  } catch {
6680
6867
  return null;
6681
6868
  }
@@ -6683,7 +6870,7 @@ function loadTree(p) {
6683
6870
  function saveTree(p, tree) {
6684
6871
  if (!p) return;
6685
6872
  try {
6686
- mkdirSync3(join8(p, ".."), { recursive: true });
6873
+ mkdirSync3(join9(p, ".."), { recursive: true });
6687
6874
  const tmp = `${p}.${process.pid}.tmp`;
6688
6875
  writeFileSync3(tmp, JSON.stringify(tree));
6689
6876
  renameSync(tmp, p);
@@ -6715,7 +6902,7 @@ function dropTurnTree(cwd, sessionId) {
6715
6902
 
6716
6903
  // src/cli/watch.ts
6717
6904
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
6718
- import { join as join9 } from "node:path";
6905
+ import { join as join10 } from "node:path";
6719
6906
  import { createHash as createHash3 } from "node:crypto";
6720
6907
  var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
6721
6908
  function snap(abs2) {
@@ -6727,7 +6914,7 @@ function snap(abs2) {
6727
6914
  }
6728
6915
  function defaultEventLog(cwd) {
6729
6916
  if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
6730
- return join9(gitDir(cwd) ?? join9(cwd, ".git"), "tamperward", "fsevents.jsonl");
6917
+ return join10(gitDir(cwd) ?? join10(cwd, ".git"), "tamperward", "fsevents.jsonl");
6731
6918
  }
6732
6919
  function watchTree(dir, cb) {
6733
6920
  if (process.env.TAMPERWARD_WATCH_NO_RECURSIVE !== "1") {
@@ -6744,12 +6931,12 @@ function watchTree(dir, cb) {
6744
6931
  if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
6745
6932
  let w;
6746
6933
  try {
6747
- w = watch(rel ? join9(dir, rel) : dir, (kind, fname) => {
6934
+ w = watch(rel ? join10(dir, rel) : dir, (kind, fname) => {
6748
6935
  if (!fname) return;
6749
6936
  const child = rel ? `${rel}/${String(fname)}` : String(fname);
6750
6937
  cb(kind, child);
6751
6938
  try {
6752
- if (statSync(join9(dir, child)).isDirectory()) addDir(child);
6939
+ if (statSync(join10(dir, child)).isDirectory()) addDir(child);
6753
6940
  } catch {
6754
6941
  }
6755
6942
  });
@@ -6762,7 +6949,7 @@ function watchTree(dir, cb) {
6762
6949
  addDir(rel);
6763
6950
  let names = [];
6764
6951
  try {
6765
- names = readdirSync2(rel ? join9(dir, rel) : dir);
6952
+ names = readdirSync2(rel ? join10(dir, rel) : dir);
6766
6953
  } catch {
6767
6954
  return;
6768
6955
  }
@@ -6770,7 +6957,7 @@ function watchTree(dir, cb) {
6770
6957
  const child = rel ? `${rel}/${name}` : name;
6771
6958
  if (SKIP2.test(child + "/")) continue;
6772
6959
  try {
6773
- if (statSync(join9(dir, child)).isDirectory()) walk2(child);
6960
+ if (statSync(join10(dir, child)).isDirectory()) walk2(child);
6774
6961
  } catch {
6775
6962
  }
6776
6963
  }
@@ -6782,13 +6969,13 @@ function watchTree(dir, cb) {
6782
6969
  }
6783
6970
  function startWatcher(dir, log, policy) {
6784
6971
  try {
6785
- mkdirSync4(join9(log, ".."), { recursive: true });
6972
+ mkdirSync4(join10(log, ".."), { recursive: true });
6786
6973
  } catch {
6787
6974
  }
6788
6975
  const last = /* @__PURE__ */ new Map();
6789
6976
  return watchTree(dir, (kind, rel) => {
6790
6977
  if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
6791
- const s = snap(join9(dir, rel));
6978
+ const s = snap(join10(dir, rel));
6792
6979
  const key2 = `${s.hash}:${s.mode}`;
6793
6980
  if (last.get(rel) === key2) return;
6794
6981
  last.set(rel, key2);
@@ -6815,7 +7002,7 @@ function runWatch(args) {
6815
7002
  }
6816
7003
 
6817
7004
  // src/detectors/fs-events.ts
6818
- import { readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
7005
+ import { readFileSync as readFileSync9, statSync as statSync2 } from "node:fs";
6819
7006
  var RULE11 = "transient-protected-mutation";
6820
7007
  function readEvents(log, offset) {
6821
7008
  let size = 0;
@@ -6825,7 +7012,7 @@ function readEvents(log, offset) {
6825
7012
  return { events: [], newOffset: offset };
6826
7013
  }
6827
7014
  if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
6828
- const raw = readFileSync8(log, "utf8").slice(offset);
7015
+ const raw = readFileSync9(log, "utf8").slice(offset);
6829
7016
  const events = [];
6830
7017
  for (const line of raw.split("\n")) {
6831
7018
  if (!line.trim()) continue;
@@ -6878,7 +7065,7 @@ var HookInputError = class extends Error {
6878
7065
  };
6879
7066
  function readStdin() {
6880
7067
  try {
6881
- return readFileSync9(0, "utf8");
7068
+ return readFileSync10(0, "utf8");
6882
7069
  } catch (e) {
6883
7070
  throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
6884
7071
  }
@@ -7134,7 +7321,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
7134
7321
  if (!existsSync7(log)) return none;
7135
7322
  let offset = 0;
7136
7323
  if (cp && existsSync7(cp)) {
7137
- const n = Number(readFileSync9(cp, "utf8"));
7324
+ const n = Number(readFileSync10(cp, "utf8"));
7138
7325
  offset = Number.isFinite(n) ? n : 0;
7139
7326
  }
7140
7327
  const { events, newOffset } = readEvents(log, offset);
@@ -7287,8 +7474,8 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
7287
7474
  // src/cli/init.ts
7288
7475
  import { execFileSync as execFileSync5 } from "node:child_process";
7289
7476
  import { createHash as createHash5 } from "node:crypto";
7290
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
7291
- 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";
7292
7479
  var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
7293
7480
  var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7294
7481
  function gitConfig(cwd, key2) {
@@ -7464,7 +7651,7 @@ function hooksShapeError(hooks) {
7464
7651
  return null;
7465
7652
  }
7466
7653
  function planPolicy(cwd) {
7467
- const path = join10(cwd, POLICY_FILE);
7654
+ const path = join11(cwd, POLICY_FILE);
7468
7655
  if (existsSync8(path)) {
7469
7656
  try {
7470
7657
  loadPolicy(cwd);
@@ -7483,12 +7670,12 @@ function planPolicy(cwd) {
7483
7670
  }
7484
7671
  function planClaudeHooks(cwd) {
7485
7672
  const rel = ".claude/settings.json";
7486
- const path = join10(cwd, rel);
7673
+ const path = join11(cwd, rel);
7487
7674
  let settings = {};
7488
7675
  if (existsSync8(path)) {
7489
7676
  let parsed;
7490
7677
  try {
7491
- parsed = JSON.parse(readFileSync10(path, "utf8"));
7678
+ parsed = JSON.parse(readFileSync11(path, "utf8"));
7492
7679
  } catch {
7493
7680
  return {
7494
7681
  item: "agent",
@@ -7584,7 +7771,7 @@ function hooksDir(cwd) {
7584
7771
  if (out2) return resolve3(cwd, out2);
7585
7772
  } catch {
7586
7773
  }
7587
- return existsSync8(join10(cwd, ".git")) ? join10(cwd, ".git", "hooks") : null;
7774
+ return existsSync8(join11(cwd, ".git")) ? join11(cwd, ".git", "hooks") : null;
7588
7775
  }
7589
7776
  function display(cwd, path) {
7590
7777
  const rel = relative3(cwd, path);
@@ -7611,30 +7798,30 @@ function planPreCommit(cwd) {
7611
7798
  const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
7612
7799
  let path;
7613
7800
  let note;
7614
- if (existsSync8(join10(cwd, ".husky"))) {
7615
- path = join10(cwd, ".husky", "pre-commit");
7801
+ if (existsSync8(join11(cwd, ".husky"))) {
7802
+ path = join11(cwd, ".husky", "pre-commit");
7616
7803
  note = "husky";
7617
7804
  } else {
7618
- 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))));
7619
7806
  if (managed) {
7620
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}`);
7621
7808
  }
7622
7809
  const dir = hooksDir(cwd);
7623
7810
  if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
7624
7811
  if (basename2(dir) === "_" && basename2(dirname4(dir)) === ".husky") {
7625
- path = join10(dirname4(dir), "pre-commit");
7812
+ path = join11(dirname4(dir), "pre-commit");
7626
7813
  note = "husky";
7627
7814
  } else {
7628
- path = join10(dir, "pre-commit");
7815
+ path = join11(dir, "pre-commit");
7629
7816
  const rel2 = display(cwd, dir);
7630
- 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)`;
7631
7818
  }
7632
7819
  }
7633
7820
  const rel = display(cwd, path);
7634
7821
  if (existsSync8(path) && !statSync3(path).isFile()) {
7635
7822
  return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
7636
7823
  }
7637
- const existing = existsSync8(path) ? readFileSync10(path, "utf8") : null;
7824
+ const existing = existsSync8(path) ? readFileSync11(path, "utf8") : null;
7638
7825
  if (existing === null) {
7639
7826
  return {
7640
7827
  item: "pre-commit",
@@ -7723,10 +7910,10 @@ ${block.join("\n")}
7723
7910
  function planCodeowners(cwd) {
7724
7911
  const rel = ".github/CODEOWNERS";
7725
7912
  const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
7726
- (r) => existsSync8(join10(cwd, r))
7913
+ (r) => existsSync8(join11(cwd, r))
7727
7914
  );
7728
- const path = join10(cwd, existingRel ?? rel);
7729
- const existing = existingRel ? readFileSync10(path, "utf8") : null;
7915
+ const path = join11(cwd, existingRel ?? rel);
7916
+ const existing = existingRel ? readFileSync11(path, "utf8") : null;
7730
7917
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
7731
7918
  if (missing.length === 0) {
7732
7919
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
@@ -7759,7 +7946,7 @@ ${CODEOWNERS_MARK}
7759
7946
  }
7760
7947
  function planWorkflow(cwd, force) {
7761
7948
  const rel = ".github/workflows/tamperward.yml";
7762
- const path = join10(cwd, rel);
7949
+ const path = join11(cwd, rel);
7763
7950
  const write = () => {
7764
7951
  mkdirSync5(dirname4(path), { recursive: true });
7765
7952
  writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
@@ -7775,7 +7962,7 @@ function planWorkflow(cwd, force) {
7775
7962
  }
7776
7963
  let src;
7777
7964
  try {
7778
- src = readFileSync10(path, "utf8");
7965
+ src = readFileSync11(path, "utf8");
7779
7966
  } catch {
7780
7967
  return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
7781
7968
  }
@@ -7874,15 +8061,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
7874
8061
  // src/cli/verify.ts
7875
8062
  import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
7876
8063
  import { createHash as createHash8 } from "node:crypto";
7877
- 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";
7878
8065
  import { tmpdir as tmpdir2 } from "node:os";
7879
- 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";
7880
8067
 
7881
8068
  // src/git/trusted.ts
7882
8069
  import { execFileSync as execFileSync6 } from "node:child_process";
7883
8070
  import { createHash as createHash6 } from "node:crypto";
7884
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
7885
- 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";
7886
8073
  function trustedGitEnv() {
7887
8074
  return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
7888
8075
  }
@@ -7895,7 +8082,7 @@ function gitDirOf(cwd) {
7895
8082
  env: trustedGitEnv()
7896
8083
  }).trim();
7897
8084
  if (!d) return null;
7898
- return isAbsolute8(d) ? d : join11(cwd, d);
8085
+ return isAbsolute8(d) ? d : join12(cwd, d);
7899
8086
  } catch {
7900
8087
  return null;
7901
8088
  }
@@ -7920,9 +8107,9 @@ function objectRewriteState(cwd) {
7920
8107
  parts.push(`${rel}:<no-git-dir>`);
7921
8108
  continue;
7922
8109
  }
7923
- const p = join11(dir, rel);
8110
+ const p = join12(dir, rel);
7924
8111
  try {
7925
- parts.push(`${rel}:` + (existsSync9(p) ? readFileSync11(p, "utf8").trim() : ""));
8112
+ parts.push(`${rel}:` + (existsSync9(p) ? readFileSync12(p, "utf8").trim() : ""));
7926
8113
  } catch {
7927
8114
  parts.push(`${rel}:<unreadable>`);
7928
8115
  }
@@ -7933,8 +8120,8 @@ function objectRewriteState(cwd) {
7933
8120
  // src/fingerprint.ts
7934
8121
  import { execFileSync as execFileSync7 } from "node:child_process";
7935
8122
  import { createHash as createHash7 } from "node:crypto";
7936
- import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
7937
- 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";
7938
8125
  function treeFingerprint(cwd) {
7939
8126
  const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
7940
8127
  cwd,
@@ -7952,7 +8139,7 @@ function treeFingerprint(cwd) {
7952
8139
  return h.digest("hex");
7953
8140
  }
7954
8141
  function depsFingerprint(cwd) {
7955
- const root = join12(cwd, "node_modules");
8142
+ const root = join13(cwd, "node_modules");
7956
8143
  const h = createHash7("sha256");
7957
8144
  const walk2 = (dir) => {
7958
8145
  let entries;
@@ -7962,12 +8149,12 @@ function depsFingerprint(cwd) {
7962
8149
  return;
7963
8150
  }
7964
8151
  for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
7965
- const p = join12(dir, e.name);
8152
+ const p = join13(dir, e.name);
7966
8153
  if (e.isDirectory()) walk2(p);
7967
8154
  else if (e.isFile()) {
7968
8155
  h.update(p);
7969
8156
  try {
7970
- h.update(readFileSync12(p));
8157
+ h.update(readFileSync13(p));
7971
8158
  h.update(String(statSync4(p).mode));
7972
8159
  } catch {
7973
8160
  h.update("<unreadable>");
@@ -8082,7 +8269,7 @@ function baseIsAncestorOfHead(base, cwd) {
8082
8269
  function materialize(cwd, dest) {
8083
8270
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8084
8271
  for (const rel of listed) {
8085
- const src = join13(cwd, rel);
8272
+ const src = join14(cwd, rel);
8086
8273
  let st;
8087
8274
  try {
8088
8275
  st = statSync5(src);
@@ -8090,18 +8277,18 @@ function materialize(cwd, dest) {
8090
8277
  continue;
8091
8278
  }
8092
8279
  if (!st.isFile()) continue;
8093
- const out2 = join13(dest, rel);
8280
+ const out2 = join14(dest, rel);
8094
8281
  mkdirSync6(dirname5(out2), { recursive: true });
8095
8282
  dropSymlink(out2);
8096
8283
  if (lstatSync3(src).isSymbolicLink()) {
8097
- writeFileSync6(out2, readFileSync13(src));
8284
+ writeFileSync6(out2, readFileSync14(src));
8098
8285
  } else {
8099
8286
  cpSync(src, out2, { dereference: true });
8100
8287
  }
8101
8288
  chmodSync2(out2, st.mode);
8102
8289
  }
8103
8290
  const nm = resolve4(cwd, "node_modules");
8104
- if (existsSync10(nm)) symlinkSync(nm, join13(dest, "node_modules"), "dir");
8291
+ if (existsSync10(nm)) symlinkSync(nm, join14(dest, "node_modules"), "dir");
8105
8292
  }
8106
8293
  function dropSymlink(p) {
8107
8294
  try {
@@ -8134,11 +8321,11 @@ function overlayDigest(dest, paths) {
8134
8321
  for (const rel of [...paths].sort()) {
8135
8322
  h.update(rel);
8136
8323
  h.update("\0");
8137
- const p = join13(dest, rel);
8324
+ const p = join14(dest, rel);
8138
8325
  try {
8139
8326
  const st = lstatSync3(p);
8140
8327
  h.update(String(st.mode));
8141
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync13(p));
8328
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
8142
8329
  } catch {
8143
8330
  h.update("<gone>");
8144
8331
  }
@@ -8161,7 +8348,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8161
8348
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
8162
8349
  }
8163
8350
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
8164
- const out2 = join13(dest, e.path);
8351
+ const out2 = join14(dest, e.path);
8165
8352
  mkdirSync6(dirname5(out2), { recursive: true });
8166
8353
  rmSync3(out2, { force: true });
8167
8354
  if (e.mode === "120000") {
@@ -8176,7 +8363,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8176
8363
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8177
8364
  for (const rel of inCopy) {
8178
8365
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
8179
- rmSync3(join13(dest, rel), { force: true });
8366
+ rmSync3(join14(dest, rel), { force: true });
8180
8367
  removed++;
8181
8368
  }
8182
8369
  return { restored, removed };
@@ -8218,8 +8405,8 @@ function suiteEnv(scratch) {
8218
8405
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
8219
8406
  env[k] = v;
8220
8407
  }
8221
- const userRc = join13(scratch, "npmrc-user");
8222
- const globalRc = join13(scratch, "npmrc-global");
8408
+ const userRc = join14(scratch, "npmrc-user");
8409
+ const globalRc = join14(scratch, "npmrc-global");
8223
8410
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
8224
8411
  env.npm_config_userconfig = userRc;
8225
8412
  env.npm_config_globalconfig = globalRc;
@@ -8229,8 +8416,8 @@ function suiteEnv(scratch) {
8229
8416
  }
8230
8417
  function runSuite(dir, cmd, budgetSecs) {
8231
8418
  const t0 = Date.now();
8232
- const outDir = mkdtempSync2(join13(tmpdir2(), "tw-verify-run-"));
8233
- const outFile = join13(outDir, "result.json");
8419
+ const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
8420
+ const outFile = join14(outDir, "result.json");
8234
8421
  try {
8235
8422
  spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
8236
8423
  cwd: dir,
@@ -8243,7 +8430,7 @@ function runSuite(dir, cmd, budgetSecs) {
8243
8430
  const secs = Math.round((Date.now() - t0) / 1e3);
8244
8431
  let r;
8245
8432
  try {
8246
- r = JSON.parse(readFileSync13(outFile, "utf8"));
8433
+ r = JSON.parse(readFileSync14(outFile, "utf8"));
8247
8434
  } catch {
8248
8435
  return { exit: null, secs };
8249
8436
  }
@@ -8294,8 +8481,8 @@ function runVerify(opts) {
8294
8481
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
8295
8482
  return 2;
8296
8483
  }
8297
- const visRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-vis-"));
8298
- const visDir = join13(visRoot, "t");
8484
+ const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
8485
+ const visDir = join14(visRoot, "t");
8299
8486
  const cleanup = (dirs) => {
8300
8487
  if (opts.keep) return;
8301
8488
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
@@ -8323,8 +8510,8 @@ function runVerify(opts) {
8323
8510
  out2("pristine run executes through it too. Failing closed, not open.");
8324
8511
  return 2;
8325
8512
  }
8326
- const priRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-pri-"));
8327
- const priDir = join13(priRoot, "t");
8513
+ const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
8514
+ const priDir = join14(priRoot, "t");
8328
8515
  let restored = [];
8329
8516
  let removedAdded = 0;
8330
8517
  try {
@@ -8436,7 +8623,7 @@ function parseVerify(args) {
8436
8623
 
8437
8624
  // src/cli/run.ts
8438
8625
  import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8439
- 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";
8440
8627
  import { resolve as resolve5 } from "node:path";
8441
8628
  var out = (s) => process.stdout.write(s + "\n");
8442
8629
  var err = (s) => process.stderr.write(s + "\n");
@@ -8445,7 +8632,7 @@ function git3(args, cwd) {
8445
8632
  }
8446
8633
  function nowTicks() {
8447
8634
  try {
8448
- return Math.floor(parseFloat(readFileSync14("/proc/uptime", "utf8").split(" ")[0]) * 100);
8635
+ return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
8449
8636
  } catch {
8450
8637
  return Number.POSITIVE_INFINITY;
8451
8638
  }
@@ -8475,7 +8662,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8475
8662
  }
8476
8663
  if (held !== real && !held.startsWith(real + "/")) continue;
8477
8664
  try {
8478
- const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
8665
+ const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
8479
8666
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
8480
8667
  if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
8481
8668
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.10.1",
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",