tamperward 2.10.8 → 2.10.9

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 +447 -90
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1651,8 +1651,8 @@ function numericOf(e) {
1651
1651
  }
1652
1652
  return void 0;
1653
1653
  }
1654
- function metricsOf(expr, resolve6) {
1655
- const obj = resolve6(expr);
1654
+ function metricsOf(expr, resolve7) {
1655
+ const obj = resolve7(expr);
1656
1656
  if (!ts2.isObjectLiteralExpression(obj)) return void 0;
1657
1657
  const out2 = { values: {}, opaque: false };
1658
1658
  let any = false;
@@ -1666,7 +1666,7 @@ function metricsOf(expr, resolve6) {
1666
1666
  const k = keyName(p.name);
1667
1667
  if (k === null || !isMetric(k)) continue;
1668
1668
  any = true;
1669
- const n = numericOf(resolve6(p.initializer));
1669
+ const n = numericOf(resolve7(p.initializer));
1670
1670
  if (n === void 0) out2.opaque = true;
1671
1671
  else out2.values[k] = n;
1672
1672
  }
@@ -1712,17 +1712,17 @@ function parseThresholds(src) {
1712
1712
  const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
1713
1713
  try {
1714
1714
  const sf = sourceOf(src);
1715
- const resolve6 = resolverFor(sf);
1715
+ const resolve7 = resolverFor(sf);
1716
1716
  const visit = (node) => {
1717
1717
  if (ts2.isPropertyAssignment(node)) {
1718
1718
  const key2 = keyName(node.name);
1719
1719
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
1720
1720
  res.present = true;
1721
- const init = resolve6(node.initializer);
1721
+ const init = resolve7(node.initializer);
1722
1722
  if (!ts2.isObjectLiteralExpression(init)) {
1723
1723
  res.opaque = true;
1724
1724
  } else {
1725
- const flat = metricsOf(init, resolve6);
1725
+ const flat = metricsOf(init, resolve7);
1726
1726
  if (flat) res.global = merge(res.global, flat);
1727
1727
  for (const p of init.properties) {
1728
1728
  if (ts2.isSpreadAssignment(p)) {
@@ -1736,7 +1736,7 @@ function parseThresholds(src) {
1736
1736
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
1737
1737
  continue;
1738
1738
  }
1739
- const m = metricsOf(p.initializer, resolve6);
1739
+ const m = metricsOf(p.initializer, resolve7);
1740
1740
  if (!m) continue;
1741
1741
  if (k === "global") res.global = merge(res.global, m);
1742
1742
  else res.paths.set(norm(k), m);
@@ -1769,18 +1769,18 @@ function parseLists(src) {
1769
1769
  const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
1770
1770
  try {
1771
1771
  const sf = sourceOf(src);
1772
- const resolve6 = resolverFor(sf);
1772
+ const resolve7 = resolverFor(sf);
1773
1773
  const visit = (node) => {
1774
1774
  if (ts2.isPropertyAssignment(node)) {
1775
1775
  const k = keyName(node.name);
1776
1776
  const vitestCoverage = ownerKey(node) === "coverage";
1777
1777
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
1778
- const { items, opaque } = literals(resolve6(node.initializer));
1778
+ const { items, opaque } = literals(resolve7(node.initializer));
1779
1779
  res.collectFrom = items;
1780
1780
  res.collectKey = k === "include" ? "coverage.include" : k;
1781
1781
  if (opaque) res.opaqueCollect = true;
1782
1782
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
1783
- const { items, opaque } = literals(resolve6(node.initializer));
1783
+ const { items, opaque } = literals(resolve7(node.initializer));
1784
1784
  res.ignore.push(...items);
1785
1785
  if (opaque) res.opaqueIgnore = true;
1786
1786
  }
@@ -8163,16 +8163,14 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
8163
8163
 
8164
8164
  // src/cli/verify.ts
8165
8165
  import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
8166
- import { createHash as createHash8 } from "node:crypto";
8167
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, realpathSync as realpathSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
8166
+ import { createHash as createHash9 } from "node:crypto";
8167
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
8168
8168
  import { tmpdir as tmpdir2 } from "node:os";
8169
- import { dirname as dirname5, isAbsolute as isAbsolute9, join as join14, relative as relative4, resolve as resolve4, sep } from "node:path";
8169
+ import { dirname as dirname6, isAbsolute as isAbsolute10, join as join14, relative as relative5, resolve as resolve5, sep as sep2 } from "node:path";
8170
8170
 
8171
8171
  // src/fingerprint.ts
8172
8172
  import { execFileSync as execFileSync7 } from "node:child_process";
8173
8173
  import { createHash as createHash7 } from "node:crypto";
8174
- import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
8175
- import { join as join13 } from "node:path";
8176
8174
  function treeFingerprint(cwd, keepIgnored) {
8177
8175
  const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
8178
8176
  cwd,
@@ -8195,45 +8193,359 @@ function treeFingerprint(cwd, keepIgnored) {
8195
8193
  }
8196
8194
  return h.digest("hex");
8197
8195
  }
8198
- function depsFingerprint(cwd) {
8199
- const root = join13(cwd, "node_modules");
8200
- const h = createHash7("sha256");
8201
- const walk2 = (dir) => {
8196
+
8197
+ // src/dependency-env.ts
8198
+ import { createHash as createHash8 } from "node:crypto";
8199
+ import {
8200
+ existsSync as existsSync10,
8201
+ lstatSync as lstatSync3,
8202
+ readdirSync as readdirSync3,
8203
+ readFileSync as readFileSync13,
8204
+ readlinkSync as readlinkSync2,
8205
+ realpathSync as realpathSync3,
8206
+ statSync as statSync4
8207
+ } from "node:fs";
8208
+ import {
8209
+ basename as basename3,
8210
+ delimiter,
8211
+ dirname as dirname5,
8212
+ isAbsolute as isAbsolute9,
8213
+ join as join13,
8214
+ relative as relative4,
8215
+ resolve as resolve4,
8216
+ sep
8217
+ } from "node:path";
8218
+ var Unattestable = class extends Error {
8219
+ };
8220
+ function inside(root, candidate) {
8221
+ const rel = relative4(resolve4(root), resolve4(candidate));
8222
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
8223
+ }
8224
+ function modeKind(path) {
8225
+ const st = lstatSync3(path);
8226
+ if (st.isFile()) return { kind: "file", mode: st.mode };
8227
+ if (st.isDirectory()) return { kind: "dir", mode: st.mode };
8228
+ if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
8229
+ return { kind: "special", mode: st.mode };
8230
+ }
8231
+ function fingerprintRoot(root, cwd, h) {
8232
+ let rootStat;
8233
+ let currentReal;
8234
+ try {
8235
+ rootStat = lstatSync3(root.path);
8236
+ currentReal = realpathSync3(root.path);
8237
+ } catch (e) {
8238
+ throw new Unattestable(
8239
+ root.kind + " root " + JSON.stringify(root.path) + " can no longer be resolved (" + (e instanceof Error ? e.message : String(e)) + ")"
8240
+ );
8241
+ }
8242
+ if (currentReal !== root.realPath) {
8243
+ throw new Unattestable(
8244
+ root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
8245
+ );
8246
+ }
8247
+ if (!statSync4(root.path).isDirectory()) {
8248
+ throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
8249
+ }
8250
+ h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
8251
+ if (rootStat.isSymbolicLink()) h.update(readlinkSync2(root.path) + "\0");
8252
+ const cwdReal = realpathSync3(cwd);
8253
+ const visited = /* @__PURE__ */ new Set();
8254
+ const hashFile = (path, label) => {
8255
+ let st;
8256
+ try {
8257
+ st = statSync4(path);
8258
+ if (!st.isFile()) throw new Error("not a regular file");
8259
+ h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
8260
+ h.update(readFileSync13(path));
8261
+ h.update("\0");
8262
+ } catch (e) {
8263
+ throw new Unattestable(
8264
+ "dependency link target " + JSON.stringify(path) + " is not attestable (" + (e instanceof Error ? e.message : String(e)) + ")"
8265
+ );
8266
+ }
8267
+ };
8268
+ const walk2 = (dir, namespace) => {
8269
+ const realDir = realpathSync3(dir);
8270
+ if (visited.has(realDir)) return;
8271
+ visited.add(realDir);
8202
8272
  let entries;
8203
8273
  try {
8204
8274
  entries = readdirSync3(dir, { withFileTypes: true });
8205
- } catch {
8206
- return;
8275
+ } catch (e) {
8276
+ throw new Unattestable(
8277
+ "dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8278
+ );
8207
8279
  }
8208
- for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8209
- const p = join13(dir, e.name);
8210
- if (e.isDirectory()) walk2(p);
8211
- else if (e.isFile()) {
8212
- h.update(p);
8280
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
8281
+ const path = join13(dir, entry.name);
8282
+ const rel = namespace ? namespace + "/" + entry.name : entry.name;
8283
+ let ident;
8284
+ try {
8285
+ ident = modeKind(path);
8286
+ } catch (e) {
8287
+ throw new Unattestable(
8288
+ "dependency entry " + JSON.stringify(path) + " disappeared while attesting (" + (e instanceof Error ? e.message : String(e)) + ")"
8289
+ );
8290
+ }
8291
+ h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
8292
+ if (ident.kind === "file") {
8213
8293
  try {
8214
- h.update(readFileSync13(p));
8215
- h.update(String(statSync4(p).mode));
8216
- } catch {
8217
- h.update("<unreadable>");
8294
+ h.update(readFileSync13(path));
8295
+ h.update("\0");
8296
+ } catch (e) {
8297
+ throw new Unattestable(
8298
+ "dependency file " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8299
+ );
8300
+ }
8301
+ continue;
8302
+ }
8303
+ if (ident.kind === "dir") {
8304
+ walk2(path, rel);
8305
+ continue;
8306
+ }
8307
+ if (ident.kind === "symlink") {
8308
+ let target;
8309
+ try {
8310
+ target = readlinkSync2(path);
8311
+ } catch (e) {
8312
+ throw new Unattestable(
8313
+ "dependency symlink " + JSON.stringify(path) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
8314
+ );
8218
8315
  }
8219
- } else if (e.isSymbolicLink()) {
8220
- h.update(p);
8316
+ h.update(target + "\0");
8317
+ let resolved;
8221
8318
  try {
8222
- h.update(readlinkSync2(p));
8319
+ resolved = realpathSync3(path);
8223
8320
  } catch {
8224
- h.update("<unreadable-link>");
8321
+ continue;
8322
+ }
8323
+ const followed = statSync4(path);
8324
+ if (followed.isFile()) {
8325
+ hashFile(resolved, "@target:" + rel);
8326
+ continue;
8327
+ }
8328
+ if (followed.isDirectory()) {
8329
+ if (inside(root.realPath, resolved)) {
8330
+ walk2(resolved, "@target:" + rel);
8331
+ continue;
8332
+ }
8333
+ if (inside(cwdReal, resolved)) {
8334
+ throw new Unattestable(
8335
+ "dependency workspace symlink " + JSON.stringify(path) + " points into the candidate worktree at " + JSON.stringify(resolved) + "; faithful per-copy remapping is not available"
8336
+ );
8337
+ }
8338
+ throw new Unattestable(
8339
+ "dependency directory symlink " + JSON.stringify(path) + " escapes the bounded root to " + JSON.stringify(resolved)
8340
+ );
8225
8341
  }
8342
+ throw new Unattestable("dependency symlink " + JSON.stringify(path) + " resolves to a special file");
8226
8343
  }
8344
+ throw new Unattestable("dependency entry " + JSON.stringify(path) + " is a special file");
8227
8345
  }
8228
8346
  };
8229
- try {
8230
- if (!statSync4(root).isDirectory()) return "none";
8231
- } catch {
8232
- return "none";
8347
+ walk2(root.realPath, "");
8348
+ }
8349
+ function snapshot(roots, probes, cwd) {
8350
+ const h = createHash8("sha256");
8351
+ for (const probe of probes) {
8352
+ h.update(
8353
+ "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync10(probe.path) ? "present" : "absent") + "\0"
8354
+ );
8233
8355
  }
8234
- walk2(root);
8356
+ for (const root of roots) fingerprintRoot(root, cwd, h);
8235
8357
  return h.digest("hex");
8236
8358
  }
8359
+ function freezeRoot(kind, path) {
8360
+ try {
8361
+ if (!statSync4(path).isDirectory()) {
8362
+ throw new Error("not a directory");
8363
+ }
8364
+ return { kind, path: resolve4(path), realPath: realpathSync3(path) };
8365
+ } catch (e) {
8366
+ throw new Unattestable(
8367
+ kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
8368
+ );
8369
+ }
8370
+ }
8371
+ function shellTokens(command) {
8372
+ const matches = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
8373
+ return matches.map((token) => {
8374
+ if (token.startsWith("'") && token.endsWith("'") || token.startsWith('"') && token.endsWith('"')) return token.slice(1, -1);
8375
+ return token;
8376
+ });
8377
+ }
8378
+ function firstExecutable(command) {
8379
+ const tokens2 = shellTokens(command);
8380
+ let i = 0;
8381
+ while (i < tokens2.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i])) i++;
8382
+ if (tokens2[i] === "env") {
8383
+ i++;
8384
+ while (i < tokens2.length && (tokens2[i].startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens2[i]))) i++;
8385
+ }
8386
+ if (tokens2[i] === "command") {
8387
+ i++;
8388
+ while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
8389
+ }
8390
+ if (tokens2[i] === "timeout") {
8391
+ i++;
8392
+ while (i < tokens2.length && tokens2[i].startsWith("-")) i++;
8393
+ if (i < tokens2.length) i++;
8394
+ }
8395
+ return tokens2[i] ?? null;
8396
+ }
8397
+ function resolveExecutable(token, cwd, env) {
8398
+ if (!token) return null;
8399
+ if (token.includes("/") || token.includes("\\")) {
8400
+ const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8401
+ try {
8402
+ return existsSync10(path) ? realpathSync3(path) : null;
8403
+ } catch {
8404
+ return null;
8405
+ }
8406
+ }
8407
+ for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
8408
+ const candidate = resolve4(entry, token);
8409
+ try {
8410
+ if (existsSync10(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
8411
+ } catch {
8412
+ }
8413
+ }
8414
+ return null;
8415
+ }
8416
+ function pythonName(name) {
8417
+ return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
8418
+ }
8419
+ function looksPython(command, executable, token) {
8420
+ const exe = executable ? basename3(executable) : "";
8421
+ const raw = token ? basename3(token) : "";
8422
+ if (pythonName(exe) || pythonName(raw)) return true;
8423
+ return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
8424
+ }
8425
+ function inferredVenvFromToken(token, cwd) {
8426
+ if (!token || !token.includes("/") && !token.includes("\\")) return null;
8427
+ const path = isAbsolute9(token) ? token : resolve4(cwd, token);
8428
+ if (!pythonName(basename3(path))) return null;
8429
+ const parent = dirname5(path);
8430
+ const parentName = basename3(parent).toLowerCase();
8431
+ if (parentName !== "bin" && parentName !== "scripts") return null;
8432
+ const root = dirname5(parent);
8433
+ return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8434
+ }
8435
+ function unsupportedEcosystem(command, token) {
8436
+ const name = token ? basename3(token).toLowerCase() : "";
8437
+ const raw = command.toLowerCase();
8438
+ if (["ruby", "bundle", "bundler", "rspec", "rake"].includes(name) || /(?:^|[\s;&|()])(?:bundle\s+exec|ruby|rspec|rake)(?=$|[\s;&|()])/.test(raw)) return "Ruby dependency environment discovery is not yet attestable";
8439
+ if (["mvn", "mvnw", "gradle", "gradlew", "java"].includes(name) || /(?:^|[\s;&|()])(?:mvn|mvnw|gradle|gradlew|java)(?=$|[\s;&|()])/.test(raw)) return "JVM dependency environment discovery is not yet attestable";
8440
+ if (name === "dotnet" || /(?:^|[\s;&|()])dotnet(?=$|[\s;&|()])/.test(raw)) return ".NET dependency environment discovery is not yet attestable";
8441
+ return null;
8442
+ }
8443
+ function inferredVenv(executable) {
8444
+ if (!executable) return null;
8445
+ const parent = dirname5(executable);
8446
+ const parentName = basename3(parent).toLowerCase();
8447
+ if (parentName !== "bin" && parentName !== "scripts") return null;
8448
+ const root = dirname5(parent);
8449
+ return existsSync10(join13(root, "pyvenv.cfg")) ? root : null;
8450
+ }
8451
+ function venvProblem(root) {
8452
+ const cfg = join13(root, "pyvenv.cfg");
8453
+ if (!existsSync10(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
8454
+ try {
8455
+ const body = readFileSync13(cfg, "utf8");
8456
+ if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
8457
+ return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
8458
+ }
8459
+ } catch (e) {
8460
+ return "cannot read " + JSON.stringify(cfg) + " (" + (e instanceof Error ? e.message : String(e)) + ")";
8461
+ }
8462
+ return null;
8463
+ }
8464
+ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
8465
+ const cwd = resolve4(cwdInput);
8466
+ const roots = [];
8467
+ const probes = [];
8468
+ const seen = /* @__PURE__ */ new Set();
8469
+ const addRoot = (kind, path) => {
8470
+ const root = freezeRoot(kind, path);
8471
+ const key2 = kind + ":" + root.path;
8472
+ if (!seen.has(key2)) {
8473
+ roots.push(root);
8474
+ seen.add(key2);
8475
+ }
8476
+ };
8477
+ try {
8478
+ const nodeModules = join13(cwd, "node_modules");
8479
+ if (existsSync10(nodeModules)) addRoot("node_modules", nodeModules);
8480
+ else probes.push({ kind: "node_modules", path: nodeModules });
8481
+ const token = firstExecutable(command);
8482
+ const executable = resolveExecutable(token, cwd, env);
8483
+ let venv = null;
8484
+ if (env.VIRTUAL_ENV?.trim()) {
8485
+ if (!isAbsolute9(env.VIRTUAL_ENV)) {
8486
+ throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
8487
+ }
8488
+ venv = resolve4(env.VIRTUAL_ENV);
8489
+ } else {
8490
+ venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
8491
+ if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync10(join13(cwd, ".venv", "pyvenv.cfg"))) {
8492
+ venv = join13(cwd, ".venv");
8493
+ }
8494
+ }
8495
+ if (venv) {
8496
+ const problem = venvProblem(venv);
8497
+ if (problem) throw new Unattestable(problem);
8498
+ addRoot("python-venv", venv);
8499
+ } else if (looksPython(command, executable, token)) {
8500
+ throw new Unattestable(
8501
+ "Python verifier selected without an identifiable virtual environment; global/user site-packages are not bounded"
8502
+ );
8503
+ }
8504
+ const unsupported = unsupportedEcosystem(command, token);
8505
+ if (unsupported) throw new Unattestable(unsupported);
8506
+ const status = roots.length ? "attested" : "none";
8507
+ const fingerprint2 = snapshot(roots, probes, cwd);
8508
+ return { status, command, roots, probes, fingerprint: fingerprint2 };
8509
+ } catch (e) {
8510
+ return {
8511
+ status: "unattestable",
8512
+ command,
8513
+ roots,
8514
+ probes,
8515
+ reason: e instanceof Error ? e.message : String(e)
8516
+ };
8517
+ }
8518
+ }
8519
+ function checkDependencyEnvironment(cwdInput, descriptor) {
8520
+ if (descriptor.status === "unattestable") {
8521
+ return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
8522
+ }
8523
+ try {
8524
+ const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve4(cwdInput));
8525
+ if (fingerprint2 !== descriptor.fingerprint) {
8526
+ return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
8527
+ }
8528
+ return { ok: true, fingerprint: fingerprint2 };
8529
+ } catch (e) {
8530
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
8531
+ }
8532
+ }
8533
+ function dependencyEnvironmentReport(descriptor) {
8534
+ return {
8535
+ status: descriptor.status,
8536
+ roots: descriptor.roots.map((root) => ({ kind: root.kind, path: root.path })),
8537
+ ...descriptor.fingerprint ? { fingerprint: descriptor.fingerprint } : {},
8538
+ ...descriptor.reason ? { reason: descriptor.reason } : {}
8539
+ };
8540
+ }
8541
+ function dependencyEnvironmentSummary(descriptor) {
8542
+ if (descriptor.status === "unattestable") {
8543
+ return "unattestable - " + (descriptor.reason ?? "unknown dependency environment");
8544
+ }
8545
+ if (descriptor.status === "none") return "none detected (candidate node_modules remains absent)";
8546
+ const kinds = [...new Set(descriptor.roots.map((root) => root.kind))].join(", ");
8547
+ return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
8548
+ }
8237
8549
 
8238
8550
  // src/cli/verify.ts
8239
8551
  var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
@@ -8323,25 +8635,25 @@ function baseIsAncestorOfHead(base, cwd) {
8323
8635
  const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
8324
8636
  return r.status === 0;
8325
8637
  }
8326
- function inside(root, path) {
8327
- const rel = relative4(resolve4(root), resolve4(path));
8328
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
8638
+ function inside2(root, path) {
8639
+ const rel = relative5(resolve5(root), resolve5(path));
8640
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
8329
8641
  }
8330
8642
  function linkParts(path) {
8331
- const parts = sep === "\\" ? path.split(/[\\/]+/) : path.split("/");
8643
+ const parts = sep2 === "\\" ? path.split(/[\\/]+/) : path.split("/");
8332
8644
  return parts.filter((part) => part !== "" && part !== ".");
8333
8645
  }
8334
8646
  function linkEscape(label, target) {
8335
8647
  return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
8336
8648
  }
8337
8649
  function driveRelativeLinkTarget(target) {
8338
- return sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
8650
+ return sep2 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
8339
8651
  }
8340
8652
  function rootedLinkTarget(target) {
8341
- return isAbsolute9(target) || driveRelativeLinkTarget(target);
8653
+ return isAbsolute10(target) || driveRelativeLinkTarget(target);
8342
8654
  }
8343
8655
  function safeSymlink(target, out2, root, label) {
8344
- if (rootedLinkTarget(target) || !inside(root, resolve4(dirname5(out2), target))) {
8656
+ if (rootedLinkTarget(target) || !inside2(root, resolve5(dirname6(out2), target))) {
8345
8657
  throw linkEscape(label, target);
8346
8658
  }
8347
8659
  rmSync3(out2, { force: true });
@@ -8349,7 +8661,7 @@ function safeSymlink(target, out2, root, label) {
8349
8661
  }
8350
8662
  function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8351
8663
  let domain = "tree";
8352
- let stack = linkParts(relative4(root, dirname5(out2)));
8664
+ let stack = linkParts(relative5(root, dirname6(out2)));
8353
8665
  const pending = linkParts(target);
8354
8666
  const seen = /* @__PURE__ */ new Set();
8355
8667
  let hops = 0;
@@ -8370,7 +8682,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8370
8682
  const current = join14(base, ...stack);
8371
8683
  let st;
8372
8684
  try {
8373
- st = lstatSync3(current);
8685
+ st = lstatSync4(current);
8374
8686
  } catch {
8375
8687
  continue;
8376
8688
  }
@@ -8378,7 +8690,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8378
8690
  if (++hops > 128) {
8379
8691
  throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8380
8692
  }
8381
- const key2 = `${domain}:${current}:${pending.join(sep)}`;
8693
+ const key2 = `${domain}:${current}:${pending.join(sep2)}`;
8382
8694
  if (seen.has(key2)) {
8383
8695
  throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8384
8696
  }
@@ -8388,12 +8700,12 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8388
8700
  if (driveRelativeLinkTarget(next)) {
8389
8701
  throw linkEscape(label, target);
8390
8702
  }
8391
- if (isAbsolute9(next)) {
8392
- if (domain === "tree" || !dependencyRoot || !inside(dependencyRoot, next)) {
8703
+ if (isAbsolute10(next)) {
8704
+ if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
8393
8705
  throw linkEscape(label, target);
8394
8706
  }
8395
8707
  stack = [];
8396
- pending.unshift(...linkParts(relative4(dependencyRoot, resolve4(next))));
8708
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve5(next))));
8397
8709
  } else {
8398
8710
  pending.unshift(...linkParts(next));
8399
8711
  }
@@ -8406,16 +8718,16 @@ function rejectLinkedParent(cwd, rel) {
8406
8718
  current = join14(current, part);
8407
8719
  let st;
8408
8720
  try {
8409
- st = lstatSync3(current);
8721
+ st = lstatSync4(current);
8410
8722
  } catch {
8411
8723
  continue;
8412
8724
  }
8413
8725
  if (st.isSymbolicLink()) {
8414
- throw new Error(`${rel} has a symlinked parent directory (${relative4(cwd, current)})`);
8726
+ throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
8415
8727
  }
8416
8728
  }
8417
8729
  }
8418
- function materialize(cwd, dest) {
8730
+ function materialize(cwd, dest, dependencyRoot) {
8419
8731
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8420
8732
  const links = [];
8421
8733
  for (const rel of listed) {
@@ -8423,12 +8735,12 @@ function materialize(cwd, dest) {
8423
8735
  rejectLinkedParent(cwd, rel);
8424
8736
  let st;
8425
8737
  try {
8426
- st = lstatSync3(src);
8738
+ st = lstatSync4(src);
8427
8739
  } catch {
8428
8740
  continue;
8429
8741
  }
8430
8742
  const out2 = join14(dest, rel);
8431
- mkdirSync6(dirname5(out2), { recursive: true });
8743
+ mkdirSync6(dirname6(out2), { recursive: true });
8432
8744
  dropSymlink(out2);
8433
8745
  if (st.isSymbolicLink()) {
8434
8746
  const target = readlinkSync3(src);
@@ -8441,17 +8753,14 @@ function materialize(cwd, dest) {
8441
8753
  throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
8442
8754
  }
8443
8755
  }
8444
- const nm = resolve4(cwd, "node_modules");
8445
- let dependencyRoot = null;
8446
- if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
8447
8756
  for (const link of links) {
8448
8757
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
8449
8758
  }
8450
- if (dependencyRoot) symlinkSync(nm, join14(dest, "node_modules"), "dir");
8759
+ if (dependencyRoot) symlinkSync(dependencyRoot, join14(dest, "node_modules"), "dir");
8451
8760
  }
8452
8761
  function dropSymlink(p) {
8453
8762
  try {
8454
- if (lstatSync3(p).isSymbolicLink()) rmSync3(p, { force: true });
8763
+ if (lstatSync4(p).isSymbolicLink()) rmSync3(p, { force: true });
8455
8764
  } catch {
8456
8765
  }
8457
8766
  }
@@ -8476,13 +8785,13 @@ function baseEntries(base, cwd) {
8476
8785
  });
8477
8786
  }
8478
8787
  function overlayDigest(dest, paths) {
8479
- const h = createHash8("sha256");
8788
+ const h = createHash9("sha256");
8480
8789
  for (const rel of [...paths].sort()) {
8481
8790
  h.update(rel);
8482
8791
  h.update("\0");
8483
8792
  const p = join14(dest, rel);
8484
8793
  try {
8485
- const st = lstatSync3(p);
8794
+ const st = lstatSync4(p);
8486
8795
  h.update(String(st.mode));
8487
8796
  h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
8488
8797
  } catch {
@@ -8492,7 +8801,7 @@ function overlayDigest(dest, paths) {
8492
8801
  }
8493
8802
  return h.digest("hex");
8494
8803
  }
8495
- function overlayPristine(cwd, base, dest, policy, cmd) {
8804
+ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
8496
8805
  const entries = baseEntries(base, cwd);
8497
8806
  const atBase = entries.map((e) => e.path);
8498
8807
  const verifierOwned = verifierInputs(cmd, atBase, policy);
@@ -8509,7 +8818,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8509
8818
  }
8510
8819
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
8511
8820
  const out2 = join14(dest, e.path);
8512
- mkdirSync6(dirname5(out2), { recursive: true });
8821
+ mkdirSync6(dirname6(out2), { recursive: true });
8513
8822
  rmSync3(out2, { force: true });
8514
8823
  if (e.mode === "120000") {
8515
8824
  const target = content.toString("utf8");
@@ -8528,8 +8837,6 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8528
8837
  rmSync3(join14(dest, rel), { force: true });
8529
8838
  removed++;
8530
8839
  }
8531
- const nm = resolve4(cwd, "node_modules");
8532
- const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
8533
8840
  for (const link of restoredLinks) {
8534
8841
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
8535
8842
  }
@@ -8641,6 +8948,22 @@ function runVerify(opts) {
8641
8948
  return 2;
8642
8949
  }
8643
8950
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
8951
+ const dependencyEnvironment = opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
8952
+ const dependencyReport = () => dependencyEnvironmentReport(dependencyEnvironment);
8953
+ if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
8954
+ if (opts.json) {
8955
+ out2(JSON.stringify({
8956
+ verdict: "CANNOT_VERIFY",
8957
+ reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
8958
+ dependency_environment: dependencyReport()
8959
+ }));
8960
+ } else {
8961
+ out2("verify: dependency environment is not attestable \u2014 failing closed");
8962
+ out2("verify: " + (dependencyEnvironment.reason ?? "unknown dependency environment"));
8963
+ }
8964
+ return 2;
8965
+ }
8966
+ const frozenNodeModules = dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null;
8644
8967
  let base;
8645
8968
  try {
8646
8969
  base = resolveBase(opts.base ?? "HEAD", cwd);
@@ -8656,10 +8979,16 @@ function runVerify(opts) {
8656
8979
  };
8657
8980
  const protectedIgnored = (rel) => isProtected(rel, policy);
8658
8981
  const treeBefore = treeFingerprint(cwd, protectedIgnored);
8659
- const depsBefore = depsFingerprint(cwd);
8982
+ const dependencyAtEntry = checkDependencyEnvironment(cwd, dependencyEnvironment);
8983
+ if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
8984
+ cleanup([visRoot]);
8985
+ out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
8986
+ out2((dependencyAtEntry.reason ?? "dependency identity changed") + ". Failing closed, not open.");
8987
+ return 2;
8988
+ }
8660
8989
  try {
8661
8990
  mkdirSync6(visDir);
8662
- materialize(cwd, visDir);
8991
+ materialize(cwd, visDir, frozenNodeModules);
8663
8992
  } catch (e) {
8664
8993
  cleanup([visRoot]);
8665
8994
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
@@ -8672,10 +9001,11 @@ function runVerify(opts) {
8672
9001
  out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
8673
9002
  return 2;
8674
9003
  }
8675
- if (depsFingerprint(cwd) !== depsBefore) {
9004
+ const dependencyAfterVisible = checkDependencyEnvironment(cwd, dependencyEnvironment);
9005
+ if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
8676
9006
  cleanup([visRoot]);
8677
- out2("verify: the installed dependency tree changed while the visible suite was running \u2014 the");
8678
- out2("pristine run executes through it too. Failing closed, not open.");
9007
+ out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
9008
+ out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
8679
9009
  return 2;
8680
9010
  }
8681
9011
  const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
@@ -8684,8 +9014,15 @@ function runVerify(opts) {
8684
9014
  let removedAdded = 0;
8685
9015
  try {
8686
9016
  mkdirSync6(priDir);
8687
- materialize(cwd, priDir);
8688
- ({ restored, removed: removedAdded } = overlayPristine(cwd, base, priDir, policy, cmd));
9017
+ materialize(cwd, priDir, frozenNodeModules);
9018
+ ({ restored, removed: removedAdded } = overlayPristine(
9019
+ cwd,
9020
+ base,
9021
+ priDir,
9022
+ policy,
9023
+ cmd,
9024
+ frozenNodeModules
9025
+ ));
8689
9026
  } catch (e) {
8690
9027
  cleanup([visRoot, priRoot]);
8691
9028
  out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
@@ -8695,11 +9032,12 @@ function runVerify(opts) {
8695
9032
  const pristine = runSuite(priDir, cmd, budget);
8696
9033
  const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
8697
9034
  const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
8698
- const depsMoved = depsFingerprint(cwd) !== depsBefore;
9035
+ const dependencyAfterPristine = checkDependencyEnvironment(cwd, dependencyEnvironment);
9036
+ const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
8699
9037
  cleanup([visRoot, priRoot]);
8700
9038
  if (overlayMoved || treeMoved || depsMoved) {
8701
9039
  out2(
8702
- "verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the installed dependency tree changed while the pristine suite was running") + " \u2014"
9040
+ "verify: " + (overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : "the frozen dependency environment changed while the pristine suite was running") + " \u2014"
8703
9041
  );
8704
9042
  out2("the verdict would describe something other than what ran. Failing closed, not open.");
8705
9043
  out2("(A suite that rewrites its own snapshots or test files in place will trip this; run it in");
@@ -8734,6 +9072,7 @@ function runVerify(opts) {
8734
9072
  pristine: { exit: pristine.exit, secs: pristine.secs },
8735
9073
  protected_restored: restored.length,
8736
9074
  added_protected_removed: removedAdded,
9075
+ dependency_environment: dependencyReport(),
8737
9076
  ...signedOff ? { oob_signoff: signedOff } : {},
8738
9077
  ...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
8739
9078
  })
@@ -8746,6 +9085,7 @@ function runVerify(opts) {
8746
9085
  BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
8747
9086
  };
8748
9087
  out2(`tamperward verify \u2014 ${lines[verdict2]}`);
9088
+ out2(`dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : ""));
8749
9089
  if (signedOff)
8750
9090
  out2(
8751
9091
  `masked failure cleared by out-of-band approval (tamperward:allow:${signedOff}): a reviewer accepted that the original suite no longer applies to this change. Exit 0.`
@@ -8791,8 +9131,8 @@ function parseVerify(args) {
8791
9131
 
8792
9132
  // src/cli/run.ts
8793
9133
  import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8794
- import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync4 } from "node:fs";
8795
- import { resolve as resolve5 } from "node:path";
9134
+ import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync5 } from "node:fs";
9135
+ import { resolve as resolve6 } from "node:path";
8796
9136
  var out = (s) => process.stdout.write(s + "\n");
8797
9137
  var err = (s) => process.stderr.write(s + "\n");
8798
9138
  function git3(args, cwd) {
@@ -8809,7 +9149,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8809
9149
  const out2 = [];
8810
9150
  let real;
8811
9151
  try {
8812
- real = realpathSync4(cwd);
9152
+ real = realpathSync5(cwd);
8813
9153
  } catch {
8814
9154
  return out2;
8815
9155
  }
@@ -8838,7 +9178,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8838
9178
  for (const link of procLinks) {
8839
9179
  try {
8840
9180
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
8841
- const held = raw.startsWith("/") ? resolve5(raw) : raw;
9181
+ const held = raw.startsWith("/") ? resolve6(raw) : raw;
8842
9182
  if (held === real || held.startsWith(real + "/")) {
8843
9183
  holds = true;
8844
9184
  break;
@@ -8851,7 +9191,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8851
9191
  return out2;
8852
9192
  }
8853
9193
  function runEnvelope(opts) {
8854
- const cwd = resolve5(opts.cwd ?? process.cwd());
9194
+ const cwd = resolve6(opts.cwd ?? process.cwd());
8855
9195
  if (!opts.argv.length) {
8856
9196
  err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
8857
9197
  return 2;
@@ -8887,8 +9227,15 @@ function runEnvelope(opts) {
8887
9227
  return 2;
8888
9228
  }
8889
9229
  }
9230
+ const dependencyEnvironment = discoverDependencyEnvironment(cwd, frozenCmd);
8890
9231
  out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
8891
- const depsBefore = depsFingerprint(cwd);
9232
+ out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
9233
+ if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
9234
+ err("tamperward run: the verifier dependency environment cannot be attested \u2014");
9235
+ err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
9236
+ err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
9237
+ return 2;
9238
+ }
8892
9239
  const rewritesBefore = objectRewriteState(cwd);
8893
9240
  const spawnTicks = nowTicks();
8894
9241
  const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
@@ -8901,9 +9248,10 @@ function runEnvelope(opts) {
8901
9248
  tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
8902
9249
  return 1;
8903
9250
  }
8904
- if (depsFingerprint(cwd) !== depsBefore) {
8905
- err("tamperward run: the installed dependency tree changed before adjudication began \u2014 the program");
8906
- err("that runs the suite is not the one we started with, and both verification runs execute it.");
9251
+ const dependencyBeforeAdjudication = checkDependencyEnvironment(cwd, dependencyEnvironment);
9252
+ if (!dependencyBeforeAdjudication.ok) {
9253
+ err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
9254
+ err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
8907
9255
  if (!opts.allowDepDrift) {
8908
9256
  out(`
8909
9257
  tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
@@ -8931,7 +9279,15 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
8931
9279
  out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
8932
9280
  const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
8933
9281
  out("\n[envelope] pristine verification against the trusted base:");
8934
- const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget, policyOverride: frozenPolicy });
9282
+ const verifyCode = runVerify({
9283
+ cwd,
9284
+ base,
9285
+ cmd: frozenCmd,
9286
+ budget: frozenBudget,
9287
+ policyOverride: frozenPolicy,
9288
+ dependencyEnvironment,
9289
+ allowDepDrift: opts.allowDepDrift
9290
+ });
8935
9291
  if (opts.settle && opts.settle > 0) {
8936
9292
  try {
8937
9293
  execFileSync9("sleep", [String(opts.settle)]);
@@ -8943,10 +9299,11 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
8943
9299
  err("tamperward run: the working tree changed while it was being adjudicated \u2014");
8944
9300
  err("the verdict below would describe a tree that no longer exists.");
8945
9301
  }
8946
- const depsDrifted = !opts.allowDepDrift && depsFingerprint(cwd) !== depsBefore;
9302
+ const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
9303
+ const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
8947
9304
  if (depsDrifted) {
8948
- err("tamperward run: the installed dependency tree changed during this run \u2014 the program that");
8949
- err("runs the suite is not the one we started with. Both verification runs execute it.");
9305
+ err("tamperward run: the frozen dependency environment changed during this run \u2014");
9306
+ err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
8950
9307
  }
8951
9308
  const rewrote = objectRewriteState(cwd) !== rewritesBefore;
8952
9309
  if (rewrote) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.10.8",
3
+ "version": "2.10.9",
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",