border-customs 0.3.2 → 0.4.1

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.
package/dist/index.js CHANGED
@@ -13036,9 +13036,9 @@ import { parseArgs } from "node:util";
13036
13036
 
13037
13037
  // src/config.ts
13038
13038
  var import_yaml = __toESM(require_dist(), 1);
13039
- import { spawnSync as spawnSync10 } from "node:child_process";
13039
+ import { spawnSync as spawnSync11 } from "node:child_process";
13040
13040
  import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
13041
- import { dirname as dirname8, join as join19, resolve as resolve9 } from "node:path";
13041
+ import { dirname as dirname8, join as join20, resolve as resolve9 } from "node:path";
13042
13042
 
13043
13043
  // node_modules/zod/v4/classic/external.js
13044
13044
  var external_exports = {};
@@ -32047,8 +32047,8 @@ var gitChannel = {
32047
32047
  import { spawn as spawn2 } from "node:child_process";
32048
32048
 
32049
32049
  // src/artifacts/npm.ts
32050
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
32051
- import { join as join8, relative as relative2 } from "node:path";
32050
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
32051
+ import { join as join10, relative as relative2 } from "node:path";
32052
32052
 
32053
32053
  // src/check/lock.ts
32054
32054
  import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
@@ -38386,90 +38386,580 @@ function sweepResidueNpmArtifact(o) {
38386
38386
  return hits;
38387
38387
  }
38388
38388
 
38389
- // src/artifacts/npm.ts
38390
- function nativeFinding(rule, severity, message, path4, identity, sanitizer) {
38391
- const digest2 = redact(identity);
38389
+ // src/rules/releaseCoherence.ts
38390
+ import { spawnSync as spawnSync3 } from "node:child_process";
38391
+ import { basename, join as join9 } from "node:path";
38392
+
38393
+ // src/ledger/records.ts
38394
+ import { appendFileSync, readFileSync as readFileSync5 } from "node:fs";
38395
+ import { join as join8 } from "node:path";
38396
+ var LEDGER_FILE = "ledger.jsonl";
38397
+ var RUNS_SUBDIR = "runs";
38398
+ var HEX64 = /^[0-9a-f]{64}$/;
38399
+ var HEX40 = /^[0-9a-f]{40}$/;
38400
+ var HEX8 = /^[0-9a-f]{8}$/;
38401
+ var PUSH_CONFIRMED_VIA = ["ls-remote", "npm-view", "pypi-json", "crates-json", "rubygems-json"];
38402
+ function ledgerPath(repoDir) {
38403
+ return join8(repoDir, BORDER_STATE_DIR, LEDGER_FILE);
38404
+ }
38405
+ function runsDir(repoDir) {
38406
+ return join8(repoDir, BORDER_STATE_DIR, RUNS_SUBDIR);
38407
+ }
38408
+ function formatSkipLine(record2) {
38409
+ return `SKIP ${record2.key8} \u2014 PASS ${record2.ts} report ${record2.reportPath}`;
38410
+ }
38411
+ function buildPushRecord(i) {
38392
38412
  return {
38393
- rule,
38394
- severity,
38395
- target: NPM_TARGET_LABEL,
38396
- path: path4,
38397
- engine: "native",
38398
- message: sanitizeOut(sanitizer, message),
38399
- valueDigest: digest2.valueDigest,
38400
- snippet: digest2.snippet
38413
+ t: "push",
38414
+ key: i.key,
38415
+ target: i.target,
38416
+ remoteName: i.remoteName,
38417
+ url: sanitizeUrl(i.url),
38418
+ localSha: i.localSha,
38419
+ ...i.remoteSha !== void 0 ? { remoteSha: i.remoteSha } : {},
38420
+ ...i.version !== void 0 ? { version: i.version } : {},
38421
+ confirmedVia: i.confirmedVia,
38422
+ ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
38401
38423
  };
38402
38424
  }
38403
- function attributeToTarball(findings, artifact) {
38404
- if (artifact === null) return findings;
38405
- const name = artifact.file.slice(artifact.file.lastIndexOf("/") + 1);
38406
- return findings.map((f) => {
38407
- const p = f.path ?? "";
38408
- const rel = p === "package.json" ? p : p.startsWith("package/") ? p.slice("package/".length) : null;
38409
- return rel === null || rel === "" ? f : { ...f, path: `${name}!${rel}` };
38425
+ function buildRoundtripRecord(i) {
38426
+ if (!HEX64.test(i.artifactSha256)) bad("artifactSha256 is not hex-shaped");
38427
+ if (!HEX64.test(i.rulesHash)) bad("rulesHash is not hex-shaped");
38428
+ if (i.verdict !== "clean" && i.verdict !== "residue") bad(`verdict must be clean or residue, got '${i.verdict}'`);
38429
+ if (!Number.isInteger(i.rows) || i.rows < 0) bad("rows must be a non-negative integer");
38430
+ return {
38431
+ t: "roundtrip",
38432
+ artifactSha256: i.artifactSha256,
38433
+ verdict: i.verdict,
38434
+ rulesHash: i.rulesHash,
38435
+ rows: i.rows,
38436
+ ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
38437
+ };
38438
+ }
38439
+ function bad(msg) {
38440
+ throw new Error(msg);
38441
+ }
38442
+ function needString(o, field) {
38443
+ const v = o[field];
38444
+ if (typeof v !== "string" || v === "") bad(`${field} must be a non-empty string`);
38445
+ return v;
38446
+ }
38447
+ function needHex(o, field, re) {
38448
+ const v = needString(o, field);
38449
+ if (!re.test(v)) bad(`${field} is not hex-shaped`);
38450
+ return v;
38451
+ }
38452
+ function needStringArray(o, field) {
38453
+ const v = o[field];
38454
+ if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) bad(`${field} must be an array of strings`);
38455
+ return v;
38456
+ }
38457
+ function parseCounts(value) {
38458
+ if (typeof value !== "object" || value === null) bad("counts must be an object");
38459
+ const o = value;
38460
+ for (const field of ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL", "total", "blocking", "warnings"]) {
38461
+ if (typeof o[field] !== "number" || !Number.isInteger(o[field])) bad(`counts.${field} must be an integer`);
38462
+ }
38463
+ return o;
38464
+ }
38465
+ function parseArtifacts(o) {
38466
+ if (o.artifacts === null) return null;
38467
+ const v = o.artifacts;
38468
+ if (!Array.isArray(v)) bad("artifacts must be null or an array");
38469
+ return v.map((item, i) => {
38470
+ if (typeof item !== "object" || item === null) bad(`artifacts[${String(i)}] must be an object`);
38471
+ const a = item;
38472
+ return { file: needString(a, "file"), sha256: needHex(a, "sha256", HEX64) };
38410
38473
  });
38411
38474
  }
38412
- function scopeFindingPath(f, extractDir) {
38413
- if (f.path === void 0) return f;
38414
- const bang = f.path.indexOf("!");
38415
- if (bang > 0) {
38416
- const rel2 = relative2(extractDir, f.path.slice(0, bang));
38417
- if (!rel2.startsWith("..")) return { ...f, path: rel2 === "" ? f.path.slice(bang + 1) : `${rel2}${f.path.slice(bang)}` };
38475
+ function parseLedgerRecord(value) {
38476
+ if (typeof value !== "object" || value === null) bad("record must be an object");
38477
+ const o = value;
38478
+ const t = o.t;
38479
+ if (t === "check") {
38480
+ const key = needHex(o, "key", HEX64);
38481
+ const key8 = needHex(o, "key8", HEX8);
38482
+ if (key.slice(0, 8) !== key8) bad("key8 must be the first 8 hex chars of key");
38483
+ const verdict = needString(o, "verdict");
38484
+ if (verdict !== "PASS" && verdict !== "FAIL") bad(`verdict must be PASS or FAIL, got '${verdict}'`);
38485
+ if (o.degraded !== false) bad("degraded must be false \u2014 degraded runs are never ledgered");
38486
+ if (typeof o.llm !== "boolean") bad("llm must be a boolean");
38487
+ return {
38488
+ t: "check",
38489
+ key,
38490
+ key8,
38491
+ head: needHex(o, "head", HEX40),
38492
+ dirtyDigest: needHex(o, "dirtyDigest", HEX64),
38493
+ refSetHash: needHex(o, "refSetHash", HEX64),
38494
+ exposureSet: needStringArray(o, "exposureSet"),
38495
+ effectiveTargets: needStringArray(o, "effectiveTargets"),
38496
+ rulesHash: needHex(o, "rulesHash", HEX64),
38497
+ artifacts: parseArtifacts(o),
38498
+ llm: o.llm,
38499
+ verdict,
38500
+ counts: parseCounts(o.counts),
38501
+ reportPath: needString(o, "reportPath"),
38502
+ degraded: false,
38503
+ ts: needString(o, "ts")
38504
+ };
38418
38505
  }
38419
- const rel = relative2(extractDir, f.path);
38420
- return rel === "" || rel.startsWith("..") ? f : { ...f, path: rel };
38506
+ if (t === "push") {
38507
+ const confirmedVia = needString(o, "confirmedVia");
38508
+ if (!PUSH_CONFIRMED_VIA.includes(confirmedVia)) {
38509
+ bad(`confirmedVia must be one of ${PUSH_CONFIRMED_VIA.join("|")}`);
38510
+ }
38511
+ const remoteSha = o.remoteSha;
38512
+ const version2 = o.version;
38513
+ if (remoteSha === void 0 && version2 === void 0) bad("push record needs remoteSha or version");
38514
+ if (remoteSha !== void 0 && typeof remoteSha !== "string") bad("remoteSha must be a string");
38515
+ if (version2 !== void 0 && typeof version2 !== "string") bad("version must be a string");
38516
+ return {
38517
+ t: "push",
38518
+ key: needHex(o, "key", HEX64),
38519
+ target: needString(o, "target"),
38520
+ remoteName: needString(o, "remoteName"),
38521
+ url: needString(o, "url"),
38522
+ localSha: needString(o, "localSha"),
38523
+ ...remoteSha !== void 0 ? { remoteSha } : {},
38524
+ ...version2 !== void 0 ? { version: version2 } : {},
38525
+ confirmedVia,
38526
+ ts: needString(o, "ts")
38527
+ };
38528
+ }
38529
+ if (t === "roundtrip") {
38530
+ const verdict = needString(o, "verdict");
38531
+ if (verdict !== "clean" && verdict !== "residue") bad(`verdict must be clean or residue, got '${verdict}'`);
38532
+ const rows = o.rows;
38533
+ if (typeof rows !== "number" || !Number.isInteger(rows) || rows < 0) bad("rows must be a non-negative integer");
38534
+ return {
38535
+ t: "roundtrip",
38536
+ artifactSha256: needHex(o, "artifactSha256", HEX64),
38537
+ verdict,
38538
+ rulesHash: needHex(o, "rulesHash", HEX64),
38539
+ rows,
38540
+ ts: needString(o, "ts")
38541
+ };
38542
+ }
38543
+ return bad(`unknown record type ${JSON.stringify(t)}`);
38421
38544
  }
38422
- function listFiles(dir) {
38423
- const out = [];
38424
- const stack = [dir];
38425
- while (stack.length > 0) {
38426
- const cur = stack.pop();
38427
- for (const ent of readdirSync2(cur, { withFileTypes: true })) {
38428
- const p = join8(cur, ent.name);
38429
- if (ent.isDirectory()) stack.push(p);
38430
- else if (ent.isFile()) out.push(p);
38545
+ function codeOf2(err) {
38546
+ return typeof err === "object" && err !== null ? err.code : void 0;
38547
+ }
38548
+ function readLedger(repoDir) {
38549
+ let text;
38550
+ try {
38551
+ text = readFileSync5(ledgerPath(repoDir), "utf8");
38552
+ } catch (err) {
38553
+ if (codeOf2(err) === "ENOENT") return { records: [], warnings: [] };
38554
+ throw err;
38555
+ }
38556
+ const records = [];
38557
+ const warnings = [];
38558
+ text.split("\n").forEach((line, i) => {
38559
+ if (line.trim() === "") return;
38560
+ try {
38561
+ records.push(parseLedgerRecord(JSON.parse(line)));
38562
+ } catch (err) {
38563
+ const reason = err instanceof Error ? err.message : String(err);
38564
+ warnings.push(`ledger line ${String(i + 1)} unreadable (${reason.slice(0, 160)}) \u2014 line skipped`);
38431
38565
  }
38566
+ });
38567
+ return { records, warnings };
38568
+ }
38569
+ function appendRecord(repoDir, record2) {
38570
+ ensureStateDir(repoDir);
38571
+ appendFileSync(ledgerPath(repoDir), `${JSON.stringify(record2)}
38572
+ `, "utf8");
38573
+ }
38574
+ function newestRecordOf(records, key) {
38575
+ for (let i = records.length - 1; i >= 0; i -= 1) {
38576
+ const r = records[i];
38577
+ if (r.t === "check" && r.key === key) return r;
38432
38578
  }
38433
- return out;
38579
+ return null;
38434
38580
  }
38435
- async function runNpmArtifactStage(o) {
38436
- if (o.cfg.targets.npm === void 0) {
38437
- throw new EngineRunError("runNpmArtifactStage invoked without targets.npm configured \u2014 pipeline bug", null);
38581
+ function isSkippable(r) {
38582
+ return r !== null && r.verdict === "PASS" && r.degraded === false;
38583
+ }
38584
+ function lookupSkipRecord(records, key, llm) {
38585
+ const newest = newestRecordOf(records, key);
38586
+ return isSkippable(newest) && newest.llm === llm ? newest : null;
38587
+ }
38588
+ function latestPassCoveringTargets(records, key, targets) {
38589
+ const newest = newestRecordOf(records, key);
38590
+ return isSkippable(newest) && targets.every((t) => newest.effectiveTargets.includes(t)) ? newest : null;
38591
+ }
38592
+ function pushRecords(records, key) {
38593
+ return records.filter((r) => r.t === "push" && (key === void 0 || r.key === key));
38594
+ }
38595
+ function latestRoundtripForSha(records, artifactSha256) {
38596
+ for (let i = records.length - 1; i >= 0; i -= 1) {
38597
+ const r = records[i];
38598
+ if (r !== void 0 && r.t === "roundtrip" && r.artifactSha256 === artifactSha256) return r;
38438
38599
  }
38439
- const { repoDir } = o;
38440
- const manifestPath = join8(repoDir, "package.json");
38441
- if (!existsSync6(manifestPath)) {
38442
- throw new EngineRunError("targets.npm is configured but the repo has no package.json to pack", null);
38600
+ return null;
38601
+ }
38602
+
38603
+ // src/rules/releaseCoherence.ts
38604
+ var RELEASE_COHERENCE_DRIFT_RULE = "release-coherence-version-drift";
38605
+ var RELEASE_COHERENCE_TWIN_RULE = "release-coherence-twin-drift";
38606
+ var RELEASE_COHERENCE_UNVERIFIABLE_RULE = "release-coherence-unverifiable-source";
38607
+ var RELEASE_COHERENCE_SEVERITIES = {
38608
+ [RELEASE_COHERENCE_DRIFT_RULE]: "CRITICAL",
38609
+ [RELEASE_COHERENCE_TWIN_RULE]: "CRITICAL",
38610
+ [RELEASE_COHERENCE_UNVERIFIABLE_RULE]: "MEDIUM"
38611
+ };
38612
+ var TARGET = "artifact";
38613
+ var ENGINE = "native";
38614
+ var FOUNDING = "version truth must AGREE across every source packed in the artifact (aihr: a wheel named 0.2.2 shipped __version__ 0.2.1)";
38615
+ function probeDunderVersion(text) {
38616
+ for (const line of text.split(/\r?\n/)) {
38617
+ const assign = /^\s*__version__\s*=\s*(.+?)\s*$/.exec(line);
38618
+ if (assign?.[1] !== void 0) {
38619
+ const lit = /^(['"])((?:(?!\1).)+)\1(?:\s*#.*)?$/.exec(assign[1]);
38620
+ return lit?.[2] !== void 0 ? { status: "literal", value: lit[2] } : { status: "dynamic", reason: `__version__ assigned a non-literal (${assign[1].slice(0, 60)})` };
38621
+ }
38622
+ if (/^\s*(?:from\s+\S+\s+)?import\b.*\b__version__\b/.test(line)) {
38623
+ return { status: "dynamic", reason: "__version__ imported from another module" };
38624
+ }
38443
38625
  }
38444
- const manifest = parseNpmManifest(readFileSync5(manifestPath, "utf8"));
38445
- const identity = `${manifest.name ?? "?"}@${manifest.version ?? "?"}`;
38446
- if (manifest.isPrivate) {
38626
+ return null;
38627
+ }
38628
+ function probeHeaderVersion(text) {
38629
+ const m = /^Version:[ \t]*(.*?)[ \t]*$/m.exec(text);
38630
+ if (m === null) return null;
38631
+ return m[1] !== void 0 && m[1] !== "" ? { status: "literal", value: m[1] } : { status: "dynamic", reason: "Version: header present but empty" };
38632
+ }
38633
+ function probePyprojectVersion(text) {
38634
+ const lines = text.split(/\r?\n/);
38635
+ let inProject = false;
38636
+ const slice = [];
38637
+ for (const line of lines) {
38638
+ if (/^\s*\[/.test(line)) {
38639
+ if (inProject) break;
38640
+ inProject = /^\s*\[project\]\s*$/.test(line);
38641
+ continue;
38642
+ }
38643
+ if (inProject) slice.push(line);
38644
+ }
38645
+ if (!inProject && slice.length === 0 && !text.includes("[project]")) return null;
38646
+ const body = slice.join("\n");
38647
+ const lit = /^\s*version\s*=\s*(['"])((?:(?!\1).)+)\1\s*(?:#.*)?$/m.exec(body);
38648
+ if (lit?.[2] !== void 0) return { status: "literal", value: lit[2] };
38649
+ const dyn = /dynamic\s*=\s*\[([\s\S]*?)\]/.exec(`${body}
38650
+ ]`);
38651
+ if (dyn?.[1] !== void 0 && /["']version["']/.test(dyn[1])) return { status: "dynamic", reason: 'project.version listed in dynamic = ["version"]' };
38652
+ return { status: "dynamic", reason: "[project] declares no statically-readable version" };
38653
+ }
38654
+ function probeSetupCfgVersion(text) {
38655
+ const lines = text.split(/\r?\n/);
38656
+ let inSection = false;
38657
+ const slice = [];
38658
+ for (const line of lines) {
38659
+ if (/^\s*\[/.test(line)) {
38660
+ if (inSection) break;
38661
+ inSection = /^\s*\[metadata\]\s*$/.test(line);
38662
+ continue;
38663
+ }
38664
+ if (inSection) slice.push(line);
38665
+ }
38666
+ const v = /^[ \t]*version[ \t]*=[ \t]*(.+?)[ \t]*$/m.exec(slice.join("\n"));
38667
+ if (v?.[1] === void 0) return null;
38668
+ return /^[\w.+-]+$/.test(v[1]) ? { status: "literal", value: v[1] } : { status: "dynamic", reason: `setup.cfg version = '${v[1].slice(0, 40)}' is not a bare literal` };
38669
+ }
38670
+ function probeSetupPyVersion(text) {
38671
+ const lit = /(?:^|[,(]\s*)version\s*=\s*(['"])((?:(?!\1).)+)\1/.exec(text);
38672
+ if (lit?.[2] !== void 0) return { status: "literal", value: lit[2] };
38673
+ return /(?:^|[,(]\s*)version\s*=/.test(text) ? { status: "dynamic", reason: "setup.py version= is not a string literal" } : null;
38674
+ }
38675
+ function parseWheelFilenameVersion(fileName) {
38676
+ const base = basename(fileName).replace(/\.whl$/i, "");
38677
+ const parts = base.split("-");
38678
+ if (parts.length < 5) return null;
38679
+ const name = parts[0];
38680
+ const version2 = parts[1];
38681
+ return name !== "" && version2 !== "" ? { name, version: version2 } : null;
38682
+ }
38683
+ function parseDistInfoDirVersion(relPath) {
38684
+ const dir = /(?:^|\/)([^/]+\.dist-info)\//.exec(relPath)?.[1];
38685
+ if (dir === void 0) return null;
38686
+ const stem = dir.slice(0, -".dist-info".length);
38687
+ const idx = stem.lastIndexOf("-");
38688
+ return idx >= 0 ? stem.slice(idx + 1) : null;
38689
+ }
38690
+ var normName = (s) => s.toLowerCase().replace(/[-_.]+/g, "_");
38691
+ function toFindings2(hits, identity, sanitizer) {
38692
+ return hits.map((hit) => {
38693
+ const { valueDigest, snippet } = redact(`${identity}:release-coherence:${hit.rule}:${hit.disc}`);
38447
38694
  return {
38448
- findings: [nativeFinding(
38449
- NPM_PRIVATE_RULE,
38450
- "CRITICAL",
38451
- `package.json is private:true but border.yaml configures targets.npm \u2014 nothing can ever publish from this repo; drop the private flag or the npm target`,
38452
- "package.json",
38453
- `${identity}:private`,
38454
- o.sanitizer
38455
- )],
38456
- artifact: null
38695
+ rule: hit.rule,
38696
+ severity: RELEASE_COHERENCE_SEVERITIES[hit.rule],
38697
+ target: TARGET,
38698
+ path: hit.path,
38699
+ engine: ENGINE,
38700
+ message: sanitizer?.sanitize(hit.message) ?? hit.message,
38701
+ valueDigest,
38702
+ snippet
38457
38703
  };
38458
- }
38459
- ensureStateDir(repoDir);
38460
- const stateDir = join8(repoDir, BORDER_STATE_DIR);
38461
- const distDir = join8(stateDir, "dist");
38462
- mkdirSync3(distDir, { recursive: true });
38463
- const { filename, sha256: sha2563, bytes, tarballAbs } = packOnce({
38464
- repoDir,
38465
- distDir,
38466
- ...o.env !== void 0 ? { env: o.env } : {},
38467
- ...o.npmBinPath !== void 0 ? { npmBinPath: o.npmBinPath } : {},
38468
- ...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
38469
38704
  });
38470
- const findings = [];
38471
- const extractDir = join8(stateDir, "tmp", sha2563.slice(0, 8));
38472
- rmSync3(extractDir, { recursive: true, force: true });
38705
+ }
38706
+ function analyze(sources, pathPrefix, hits) {
38707
+ const primary = sources.find((s) => s.probe?.status === "literal");
38708
+ for (const s of sources) {
38709
+ if (s.probe === null) continue;
38710
+ if (s.probe.status === "dynamic") {
38711
+ hits.push({
38712
+ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE,
38713
+ message: `release coherence: ${s.label} (${s.path}) carries a version that cannot be verified statically \u2014 ${s.probe.reason}; surfaced, never silent-clean`,
38714
+ path: `${pathPrefix}${s.path}`,
38715
+ disc: `${s.path}:unverifiable`
38716
+ });
38717
+ continue;
38718
+ }
38719
+ if (primary === void 0 || s === primary) continue;
38720
+ if (s.probe.value !== primary.probe.value) {
38721
+ hits.push({
38722
+ rule: RELEASE_COHERENCE_DRIFT_RULE,
38723
+ message: `release coherence: ${primary.label} '${primary.probe.value}' disagrees with ${s.label} '${s.probe.value}' \u2014 ${FOUNDING}`,
38724
+ path: `${pathPrefix}${s.path}`,
38725
+ disc: `${s.path}:vs:${primary.label}`
38726
+ });
38727
+ }
38728
+ }
38729
+ }
38730
+ function npmPackCoherenceFindings(o) {
38731
+ if (o.lockText === null || o.packedVersion === null) return [];
38732
+ let lock;
38733
+ try {
38734
+ lock = JSON.parse(o.lockText);
38735
+ } catch {
38736
+ return toFindings2([{ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: `release coherence: ${o.lockRelPath} is packed but not parseable JSON \u2014 cannot verify, never silent-clean`, path: o.lockRelPath, disc: "malformed" }], o.identity, o.sanitizer);
38737
+ }
38738
+ const hits = [];
38739
+ const rootVer = typeof lock["version"] === "string" ? lock["version"] : null;
38740
+ const pkgs = lock["packages"];
38741
+ const rootPkg = typeof pkgs === "object" && pkgs !== null ? pkgs[""] : void 0;
38742
+ const pkgEmptyVer = typeof rootPkg === "object" && rootPkg !== null ? rootPkg["version"] : null;
38743
+ const cmp = (label, value, disc) => {
38744
+ if (value !== null && value !== o.packedVersion) {
38745
+ hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: packed package.json version '${String(o.packedVersion)}' disagrees with ${label} '${value}' \u2014 ${FOUNDING}`, path: o.lockRelPath, disc });
38746
+ }
38747
+ };
38748
+ cmp("package-lock.json root version", rootVer, "root");
38749
+ cmp('package-lock.json packages[""] version', typeof pkgEmptyVer === "string" ? pkgEmptyVer : null, "packages-empty");
38750
+ return toFindings2(hits, o.identity, o.sanitizer);
38751
+ }
38752
+ function pypiArtifactCoherenceFindings(o) {
38753
+ const sources = [];
38754
+ const inTree = (rel) => o.files.includes(rel);
38755
+ const textOf = (rel) => inTree(rel) ? o.read(rel) : null;
38756
+ if (o.kind === "wheel") {
38757
+ const metaRel = o.files.find((f) => /\.dist-info\/METADATA$/.test(f));
38758
+ if (metaRel !== void 0) {
38759
+ const body = o.read(metaRel);
38760
+ sources.push({ label: "wheel METADATA Version:", path: metaRel, probe: body === null ? null : probeHeaderVersion(body) ?? { status: "dynamic", reason: "METADATA has no Version: header" } });
38761
+ const dirVer = parseDistInfoDirVersion(metaRel);
38762
+ if (dirVer !== null) sources.push({ label: "wheel .dist-info directory name", path: metaRel, probe: { status: "literal", value: dirVer } });
38763
+ }
38764
+ const fn = parseWheelFilenameVersion(o.archiveName);
38765
+ if (fn !== null) sources.push({ label: "wheel filename", path: metaRel ?? o.archiveName, probe: { status: "literal", value: fn.version } });
38766
+ } else {
38767
+ const pyproject = textOf("pyproject.toml");
38768
+ if (pyproject !== null) sources.push({ label: "pyproject [project].version", path: "pyproject.toml", probe: probePyprojectVersion(pyproject) });
38769
+ const cfg = textOf("setup.cfg");
38770
+ if (cfg !== null) sources.push({ label: "setup.cfg [metadata] version", path: "setup.cfg", probe: probeSetupCfgVersion(cfg) });
38771
+ const py = textOf("setup.py");
38772
+ if (py !== null) sources.push({ label: "setup.py version literal", path: "setup.py", probe: probeSetupPyVersion(py) });
38773
+ const pkgInfoRel = o.files.find((f) => f === "PKG-INFO" || f.endsWith(".egg-info/PKG-INFO"));
38774
+ if (pkgInfoRel !== void 0) {
38775
+ const body = o.read(pkgInfoRel);
38776
+ if (body !== null) sources.push({ label: "PKG-INFO Version:", path: pkgInfoRel, probe: probeHeaderVersion(body) ?? { status: "dynamic", reason: "PKG-INFO has no Version: header" } });
38777
+ }
38778
+ }
38779
+ for (const initRel of o.files.filter((f) => /(?:^|\/)__init__\.py$/.test(f)).sort()) {
38780
+ const body = o.read(initRel);
38781
+ if (body === null) continue;
38782
+ const probe = probeDunderVersion(body);
38783
+ if (probe !== null) sources.push({ label: `__version__ literal in ${initRel}`, path: initRel, probe });
38784
+ }
38785
+ const hits = [];
38786
+ analyze(sources, `${o.archiveName}!`, hits);
38787
+ return toFindings2(hits, o.identity, o.sanitizer);
38788
+ }
38789
+ function cratesCoherenceFindings(o) {
38790
+ if (o.lockText === null) return [];
38791
+ const entries = o.lockText.split(/^\[\[package\]\][ \t]*$/m).map((block) => ({ name: /^\s*name\s*=\s*"([^"]*)"/m.exec(block)?.[1], version: /^\s*version\s*=\s*"([^"]*)"/m.exec(block)?.[1] })).filter((e) => e.name !== void 0 && e.version !== void 0).filter((e) => e.name === o.name);
38792
+ if (entries.length === 0) {
38793
+ return toFindings2([{ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: `release coherence: ${o.lockRelPath} is packed but names no [[package]] '${o.name}' \u2014 cannot verify, never silent-clean`, path: o.lockRelPath, disc: "no-entry" }], o.identity, o.sanitizer);
38794
+ }
38795
+ const hits = [];
38796
+ for (const v of [...new Set(entries.map((e) => e.version))].sort()) {
38797
+ if (v !== o.version) {
38798
+ hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: Cargo.toml [package].version '${o.version}' disagrees with Cargo.lock entry '${o.name}' version '${v}' \u2014 ${FOUNDING}`, path: o.lockRelPath, disc: v });
38799
+ }
38800
+ }
38801
+ return toFindings2(hits, o.identity, o.sanitizer);
38802
+ }
38803
+ function gemCoherenceFindings(o) {
38804
+ if (o.metadataText === null) return [];
38805
+ const name = /^name:[ \t]*(\S+)[ \t]*$/m.exec(o.metadataText)?.[1];
38806
+ const version2 = /^version:[ \t]*!ruby\/object:Gem::Version[ \t]*\n[ \t]+version:[ \t]*(\S+)[ \t]*$/m.exec(o.metadataText)?.[1];
38807
+ if (name === void 0 || version2 === void 0) {
38808
+ return toFindings2([{ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: "release coherence: metadata.gz is present but its Gem::Specification name/version could not be read statically \u2014 cannot verify, never silent-clean", path: "metadata.gz", disc: "unparseable" }], o.identity, o.sanitizer);
38809
+ }
38810
+ const hits = [];
38811
+ if (name !== o.specName) hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: built gem '${o.identity}' names '${o.specName}' but metadata.gz Gem::Specification name is '${name}' \u2014 ${FOUNDING}`, path: "metadata.gz", disc: "name" });
38812
+ if (version2 !== o.specVersion) hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: built gem '${o.identity}' names version '${o.specVersion}' but metadata.gz Gem::Version is '${version2}' \u2014 ${FOUNDING}`, path: "metadata.gz", disc: "version" });
38813
+ return toFindings2(hits, o.identity, o.sanitizer);
38814
+ }
38815
+ function packedNpmIdentity(archiveAbs, env2) {
38816
+ const opts = { encoding: "utf8", timeout: 3e4, ...env2 !== void 0 ? { env: env2 } : {} };
38817
+ const listing = spawnSync3("tar", ["-tzf", archiveAbs], opts);
38818
+ if (listing.status !== 0) return null;
38819
+ const members2 = (listing.stdout ?? "").split("\n").filter((l) => /^[^/]+\/package\.json$/.test(l));
38820
+ const member = members2.includes("package/package.json") ? "package/package.json" : members2.length === 1 ? members2[0] : void 0;
38821
+ if (member === void 0) return null;
38822
+ const body = spawnSync3("tar", ["-xzOf", archiveAbs, member], opts);
38823
+ if (body.status !== 0) return null;
38824
+ try {
38825
+ const pkg = JSON.parse(body.stdout);
38826
+ return typeof pkg["name"] === "string" && typeof pkg["version"] === "string" ? { name: pkg["name"], version: pkg["version"] } : null;
38827
+ } catch {
38828
+ return null;
38829
+ }
38830
+ }
38831
+ async function twinCoherenceFindings(o) {
38832
+ const wheels = o.artifacts.filter((a) => a.file.endsWith(".whl")).sort((a, b) => a.file.localeCompare(b.file));
38833
+ const tgzs = o.artifacts.filter((a) => a.file.endsWith(".tgz")).sort((a, b) => a.file.localeCompare(b.file));
38834
+ const hits = [];
38835
+ for (const pair of o.pairs) {
38836
+ const wh = wheels.find((a) => {
38837
+ const parsed = parseWheelFilenameVersion(basename(a.file));
38838
+ return parsed !== null && normName(parsed.name) === normName(pair.pypi);
38839
+ });
38840
+ if (wh === void 0) {
38841
+ hits.push({ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: `release coherence: release.twin pair '${pair.pypi}'/'${pair.npm}' \u2014 no staged wheel for '${pair.pypi}' in this run, so the declared cross-manager equality cannot be verified`, path: `${pair.pypi}(missing).whl`, disc: `twin:${pair.pypi}:no-wheel` });
38842
+ continue;
38843
+ }
38844
+ const wheelVersion = parseWheelFilenameVersion(basename(wh.file)).version;
38845
+ let npmSide = null;
38846
+ let npmUnreadable = false;
38847
+ for (const t of tgzs) {
38848
+ const id = packedNpmIdentity(join9(o.repoDir, t.file), o.env);
38849
+ if (id === null) {
38850
+ npmUnreadable = true;
38851
+ continue;
38852
+ }
38853
+ if (id.name === pair.npm) {
38854
+ npmSide = { file: t.file, version: id.version };
38855
+ break;
38856
+ }
38857
+ }
38858
+ if (npmSide === null) {
38859
+ hits.push({
38860
+ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE,
38861
+ message: `release coherence: release.twin pair '${pair.pypi}'/'${pair.npm}' \u2014 ${npmUnreadable ? `npm twin '${pair.npm}' is staged but its package.json could not be read from the tarball bytes` : `no staged npm artifact for twin '${pair.npm}' in this run`}, so the declared cross-manager equality cannot be verified`,
38862
+ path: basename(wh.file),
38863
+ disc: `twin:${pair.pypi}:no-npm`
38864
+ });
38865
+ continue;
38866
+ }
38867
+ if (wheelVersion !== npmSide.version) {
38868
+ hits.push({
38869
+ rule: RELEASE_COHERENCE_TWIN_RULE,
38870
+ message: `release coherence: release.twin pair '${pair.pypi}'/'${pair.npm}' \u2014 wheel '${basename(wh.file)}' says version '${wheelVersion}' but npm twin '${basename(npmSide.file)}' packed package.json says '${npmSide.version}' \u2014 ${FOUNDING}`,
38871
+ path: basename(wh.file),
38872
+ disc: `twin:${pair.pypi}:${basename(npmSide.file)}`
38873
+ });
38874
+ }
38875
+ }
38876
+ return toFindings2(hits, "release.twin", o.sanitizer);
38877
+ }
38878
+
38879
+ // src/artifacts/npm.ts
38880
+ function nativeFinding(rule, severity, message, path4, identity, sanitizer) {
38881
+ const digest2 = redact(identity);
38882
+ return {
38883
+ rule,
38884
+ severity,
38885
+ target: NPM_TARGET_LABEL,
38886
+ path: path4,
38887
+ engine: "native",
38888
+ message: sanitizeOut(sanitizer, message),
38889
+ valueDigest: digest2.valueDigest,
38890
+ snippet: digest2.snippet
38891
+ };
38892
+ }
38893
+ function attributeToTarball(findings, artifact) {
38894
+ if (artifact === null) return findings;
38895
+ const name = artifact.file.slice(artifact.file.lastIndexOf("/") + 1);
38896
+ return findings.map((f) => {
38897
+ const p = f.path ?? "";
38898
+ const rel = p === "package.json" ? p : p.startsWith("package/") ? p.slice("package/".length) : null;
38899
+ return rel === null || rel === "" ? f : { ...f, path: `${name}!${rel}` };
38900
+ });
38901
+ }
38902
+ function scopeFindingPath(f, extractDir) {
38903
+ if (f.path === void 0) return f;
38904
+ const bang = f.path.indexOf("!");
38905
+ if (bang > 0) {
38906
+ const rel2 = relative2(extractDir, f.path.slice(0, bang));
38907
+ if (!rel2.startsWith("..")) return { ...f, path: rel2 === "" ? f.path.slice(bang + 1) : `${rel2}${f.path.slice(bang)}` };
38908
+ }
38909
+ const rel = relative2(extractDir, f.path);
38910
+ return rel === "" || rel.startsWith("..") ? f : { ...f, path: rel };
38911
+ }
38912
+ function listFiles(dir) {
38913
+ const out = [];
38914
+ const stack = [dir];
38915
+ while (stack.length > 0) {
38916
+ const cur = stack.pop();
38917
+ for (const ent of readdirSync2(cur, { withFileTypes: true })) {
38918
+ const p = join10(cur, ent.name);
38919
+ if (ent.isDirectory()) stack.push(p);
38920
+ else if (ent.isFile()) out.push(p);
38921
+ }
38922
+ }
38923
+ return out;
38924
+ }
38925
+ async function runNpmArtifactStage(o) {
38926
+ if (o.cfg.targets.npm === void 0) {
38927
+ throw new EngineRunError("runNpmArtifactStage invoked without targets.npm configured \u2014 pipeline bug", null);
38928
+ }
38929
+ const { repoDir } = o;
38930
+ const manifestPath = join10(repoDir, "package.json");
38931
+ if (!existsSync6(manifestPath)) {
38932
+ throw new EngineRunError("targets.npm is configured but the repo has no package.json to pack", null);
38933
+ }
38934
+ const manifest = parseNpmManifest(readFileSync6(manifestPath, "utf8"));
38935
+ const identity = `${manifest.name ?? "?"}@${manifest.version ?? "?"}`;
38936
+ if (manifest.isPrivate) {
38937
+ return {
38938
+ findings: [nativeFinding(
38939
+ NPM_PRIVATE_RULE,
38940
+ "CRITICAL",
38941
+ `package.json is private:true but border.yaml configures targets.npm \u2014 nothing can ever publish from this repo; drop the private flag or the npm target`,
38942
+ "package.json",
38943
+ `${identity}:private`,
38944
+ o.sanitizer
38945
+ )],
38946
+ artifact: null
38947
+ };
38948
+ }
38949
+ ensureStateDir(repoDir);
38950
+ const stateDir = join10(repoDir, BORDER_STATE_DIR);
38951
+ const distDir = join10(stateDir, "dist");
38952
+ mkdirSync3(distDir, { recursive: true });
38953
+ const { filename, sha256: sha2563, bytes, tarballAbs } = packOnce({
38954
+ repoDir,
38955
+ distDir,
38956
+ ...o.env !== void 0 ? { env: o.env } : {},
38957
+ ...o.npmBinPath !== void 0 ? { npmBinPath: o.npmBinPath } : {},
38958
+ ...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
38959
+ });
38960
+ const findings = [];
38961
+ const extractDir = join10(stateDir, "tmp", sha2563.slice(0, 8));
38962
+ rmSync3(extractDir, { recursive: true, force: true });
38473
38963
  mkdirSync3(extractDir, { recursive: true });
38474
38964
  try {
38475
38965
  extractArchive(tarballAbs, extractDir, {
@@ -38484,8 +38974,16 @@ async function runNpmArtifactStage(o) {
38484
38974
  }
38485
38975
  const packedManifestRel = `${root}/package.json`;
38486
38976
  if (!relPaths.includes(packedManifestRel)) throw new EngineRunError("extracted artifact has no package/package.json", null);
38487
- const packedManifest = parseNpmManifest(readFileSync5(join8(extractDir, packedManifestRel), "utf8"));
38488
- const pkgDir = join8(extractDir, root);
38977
+ const packedManifest = parseNpmManifest(readFileSync6(join10(extractDir, packedManifestRel), "utf8"));
38978
+ const lockRel = `${root}/package-lock.json`;
38979
+ findings.push(...npmPackCoherenceFindings({
38980
+ packedVersion: packedManifest.version,
38981
+ lockText: relPaths.includes(lockRel) ? readFileSync6(join10(extractDir, lockRel), "utf8") : null,
38982
+ lockRelPath: lockRel,
38983
+ identity,
38984
+ ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
38985
+ }));
38986
+ const pkgDir = join10(extractDir, root);
38489
38987
  const residueCtx = { root, identity, ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {} };
38490
38988
  for (const key of LIFECYCLE_SCRIPT_KEYS) {
38491
38989
  const cmd = packedManifest.scripts[key];
@@ -38570,7 +39068,7 @@ async function runNpmArtifactStage(o) {
38570
39068
  }
38571
39069
 
38572
39070
  // src/check/context.ts
38573
- import { spawnSync as spawnSync3 } from "node:child_process";
39071
+ import { spawnSync as spawnSync4 } from "node:child_process";
38574
39072
  import { createHash as createHash4 } from "node:crypto";
38575
39073
  import { dirname as dirname4, resolve as resolve4 } from "node:path";
38576
39074
 
@@ -38597,7 +39095,7 @@ function isRecord(value) {
38597
39095
  // src/check/context.ts
38598
39096
  function gitSpawn(repoDir, args, o = {}) {
38599
39097
  const abs = resolve4(repoDir);
38600
- const r = spawnSync3("git", ["-C", abs, ...args], {
39098
+ const r = spawnSync4("git", ["-C", abs, ...args], {
38601
39099
  encoding: "utf8",
38602
39100
  env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname4(abs) },
38603
39101
  timeout: 6e4
@@ -38656,7 +39154,7 @@ function computeEffectiveTargets(cfg, requested) {
38656
39154
  }
38657
39155
 
38658
39156
  // src/rules/identity.ts
38659
- import { spawnSync as spawnSync4 } from "node:child_process";
39157
+ import { spawnSync as spawnSync5 } from "node:child_process";
38660
39158
  import { dirname as dirname5, resolve as resolve5 } from "node:path";
38661
39159
 
38662
39160
  // src/rules/allowlist.ts
@@ -38721,7 +39219,7 @@ function addRole(entries, key, entry, obj, role) {
38721
39219
  found.objects.set(obj, prev === void 0 ? role : `${prev}+${role}`);
38722
39220
  }
38723
39221
  function runGit2(repoDir, args) {
38724
- const r = spawnSync4("git", args, {
39222
+ const r = spawnSync5("git", args, {
38725
39223
  cwd: repoDir,
38726
39224
  encoding: "utf8",
38727
39225
  env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname5(resolve5(repoDir)) }
@@ -38767,7 +39265,7 @@ function collectCommits(s, revs) {
38767
39265
  }
38768
39266
  }
38769
39267
  function revExists(repoDir, rev) {
38770
- const r = spawnSync4("git", ["rev-parse", "--verify", "--quiet", rev], {
39268
+ const r = spawnSync5("git", ["rev-parse", "--verify", "--quiet", rev], {
38771
39269
  cwd: repoDir,
38772
39270
  encoding: "utf8",
38773
39271
  env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname5(resolve5(repoDir)) }
@@ -38871,12 +39369,12 @@ function scanIdentity(o) {
38871
39369
  // src/ledger.ts
38872
39370
  import { createHash as createHash8 } from "node:crypto";
38873
39371
  import { writeFileSync as writeFileSync3 } from "node:fs";
38874
- import { join as join14 } from "node:path";
39372
+ import { join as join15 } from "node:path";
38875
39373
 
38876
39374
  // src/check/rulesHash.ts
38877
39375
  import { createHash as createHash5 } from "node:crypto";
38878
39376
  import { existsSync as existsSync7 } from "node:fs";
38879
- import { dirname as dirname6, join as join9 } from "node:path";
39377
+ import { dirname as dirname6, join as join11 } from "node:path";
38880
39378
  import { fileURLToPath as fileURLToPath4 } from "node:url";
38881
39379
  var RESIDUE_FINGERPRINT_SOURCES = [
38882
39380
  { dir: "rules", base: "residueMatchers.ts" },
@@ -38894,12 +39392,26 @@ function resolveResidueFingerprintFiles(env2 = process.env) {
38894
39392
  const here = dirname6(fileURLToPath4(import.meta.url));
38895
39393
  const seam = env2.BORDER_RESIDUE_SRC_DIR;
38896
39394
  return RESIDUE_FINGERPRINT_SOURCES.map(({ dir, base }) => {
38897
- if (seam !== void 0) return join9(seam, base);
38898
- const live = join9(here, "..", dir, base);
39395
+ if (seam !== void 0) return join11(seam, base);
39396
+ const live = join11(here, "..", dir, base);
38899
39397
  if (existsSync7(live)) return live;
38900
39398
  return resolveAsset(import.meta.url, ["residue-src", base]);
38901
39399
  });
38902
39400
  }
39401
+ var RELEASE_FINGERPRINT_SOURCES = [
39402
+ { dir: "rules", base: "releaseCoherence.ts" }
39403
+ ];
39404
+ var RELEASE_FINGERPRINT_BASENAMES = RELEASE_FINGERPRINT_SOURCES.map((s) => s.base);
39405
+ function resolveReleaseFingerprintFiles(env2 = process.env) {
39406
+ const here = dirname6(fileURLToPath4(import.meta.url));
39407
+ const seam = env2.BORDER_RELEASE_SRC_DIR;
39408
+ return RELEASE_FINGERPRINT_SOURCES.map(({ dir, base }) => {
39409
+ if (seam !== void 0) return join11(seam, base);
39410
+ const live = join11(here, "..", dir, base);
39411
+ if (existsSync7(live)) return live;
39412
+ return resolveAsset(import.meta.url, ["release-src", base]);
39413
+ });
39414
+ }
38903
39415
  function resolvePromptTemplatePath(env2 = process.env) {
38904
39416
  return env2.BORDER_PROMPT_TEMPLATE_PATH ?? resolveAsset(import.meta.url, ["prompts", "llm-review.md"]);
38905
39417
  }
@@ -38918,7 +39430,11 @@ function computeConfigDigest(load) {
38918
39430
  async function computeCheckRulesHash(input2) {
38919
39431
  const template = resolvePromptTemplatePath(input2.env ?? process.env);
38920
39432
  return computeRulesHash({
38921
- bundledRulePaths: [GITLEAKS_VENDORED_CONFIG, ...resolveResidueFingerprintFiles(input2.env ?? process.env)],
39433
+ bundledRulePaths: [
39434
+ GITLEAKS_VENDORED_CONFIG,
39435
+ ...resolveResidueFingerprintFiles(input2.env ?? process.env),
39436
+ ...resolveReleaseFingerprintFiles(input2.env ?? process.env)
39437
+ ],
38922
39438
  configDigest: input2.configDigest,
38923
39439
  engineVersions: input2.engineVersions,
38924
39440
  promptTemplatePaths: existsSync7(template) ? [template] : []
@@ -39187,23 +39703,23 @@ function renderReportMd(report) {
39187
39703
  }
39188
39704
 
39189
39705
  // src/ledger/freshness.ts
39190
- import { spawnSync as spawnSync6 } from "node:child_process";
39706
+ import { spawnSync as spawnSync7 } from "node:child_process";
39191
39707
  import { createHash as createHash7 } from "node:crypto";
39192
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
39193
- import { join as join11 } from "node:path";
39708
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
39709
+ import { join as join13 } from "node:path";
39194
39710
 
39195
39711
  // src/artifacts/rubygems.ts
39196
39712
  import { createHash as createHash6 } from "node:crypto";
39197
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
39198
- import { join as join10, relative as relative3 } from "node:path";
39199
- import { spawnSync as spawnSync5 } from "node:child_process";
39713
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
39714
+ import { join as join12, relative as relative3 } from "node:path";
39715
+ import { spawnSync as spawnSync6 } from "node:child_process";
39200
39716
  import { gunzipSync } from "node:zlib";
39201
39717
 
39202
39718
  // src/artifacts/residueGem.ts
39203
- import { basename } from "node:path";
39719
+ import { basename as basename2 } from "node:path";
39204
39720
  var GEM_WORDING = { what: "gem install-time", ledger: "gem" };
39205
39721
  function gemPersistenceHit(rel, text) {
39206
- if (GEM_PERSISTENCE_BASENAMES.includes(basename(rel))) {
39722
+ if (GEM_PERSISTENCE_BASENAMES.includes(basename2(rel))) {
39207
39723
  return {
39208
39724
  rule: RESIDUE_T4_RULE,
39209
39725
  severity: "CRITICAL",
@@ -39449,7 +39965,7 @@ function residueGemHits(pkgDir, rels, metadata) {
39449
39965
  const rel = normalizeRel(raw);
39450
39966
  const text = readText(pkgDir, rel);
39451
39967
  if (text === null) continue;
39452
- const base = basename(rel);
39968
+ const base = basename2(rel);
39453
39969
  const isExtconf = base === GEM_EXTCONF_BASENAME;
39454
39970
  const persistence = gemPersistenceHit(rel, text);
39455
39971
  if (persistence !== null) hits.push(persistence);
@@ -39548,7 +40064,7 @@ function listFiles2(dir) {
39548
40064
  while (stack.length > 0) {
39549
40065
  const cur = stack.pop();
39550
40066
  for (const ent of readdirSync3(cur, { withFileTypes: true })) {
39551
- const p = join10(cur, ent.name);
40067
+ const p = join12(cur, ent.name);
39552
40068
  if (ent.isDirectory()) stack.push(p);
39553
40069
  else if (ent.isFile()) out.push(p);
39554
40070
  }
@@ -39561,11 +40077,11 @@ function treeGemspecCandidates(repoDir) {
39561
40077
  while (stack.length > 0) {
39562
40078
  const cur = stack.pop();
39563
40079
  for (const ent of readdirSync3(cur, { withFileTypes: true })) {
39564
- const p = join10(cur, ent.name);
40080
+ const p = join12(cur, ent.name);
39565
40081
  if (ent.isDirectory()) {
39566
40082
  if (!SKIP_DIRECTORIES2.has(ent.name)) stack.push(p);
39567
40083
  } else if (ent.isFile() && ent.name.toLowerCase().endsWith(".gemspec")) {
39568
- out.push({ path: relative3(repoDir, p), body: readFileSync6(p, "utf8") });
40084
+ out.push({ path: relative3(repoDir, p), body: readFileSync7(p, "utf8") });
39569
40085
  }
39570
40086
  }
39571
40087
  }
@@ -39597,7 +40113,7 @@ async function runRubygemsArtifactStage(o) {
39597
40113
  throw e;
39598
40114
  }
39599
40115
  const gemName = `${identity.name}-${identity.version}.gem`;
39600
- const versionCheck = spawnSync5(bin, ["--version"], {
40116
+ const versionCheck = spawnSync6(bin, ["--version"], {
39601
40117
  encoding: "utf8",
39602
40118
  stdio: "ignore",
39603
40119
  ...o.env === void 0 ? {} : { env: { ...o.env } }
@@ -39608,11 +40124,11 @@ async function runRubygemsArtifactStage(o) {
39608
40124
  );
39609
40125
  }
39610
40126
  ensureStateDir(repoDir);
39611
- const stateDir = join10(repoDir, BORDER_STATE_DIR);
39612
- const distDir = join10(stateDir, "dist");
40127
+ const stateDir = join12(repoDir, BORDER_STATE_DIR);
40128
+ const distDir = join12(stateDir, "dist");
39613
40129
  mkdirSync4(distDir, { recursive: true });
39614
- const dist = join10(distDir, gemName);
39615
- const result = spawnSync5(bin, ["build", candidates[0].path, "-o", dist], {
40130
+ const dist = join12(distDir, gemName);
40131
+ const result = spawnSync6(bin, ["build", candidates[0].path, "-o", dist], {
39616
40132
  cwd: repoDir,
39617
40133
  encoding: "utf8",
39618
40134
  timeout: o.gemTimeoutMs ?? 12e4,
@@ -39627,10 +40143,10 @@ async function runRubygemsArtifactStage(o) {
39627
40143
  if (!existsSync8(dist)) {
39628
40144
  throw new EngineRunError(`gem build produced no ${gemName} in ${relative3(repoDir, distDir)} \u2014 failing closed (was the gemspec's literal name/version re-read?)`, null);
39629
40145
  }
39630
- const bytes = readFileSync6(dist);
40146
+ const bytes = readFileSync7(dist);
39631
40147
  const sha2563 = createHash6("sha256").update(bytes).digest("hex");
39632
40148
  const findings = [];
39633
- const extractDir = join10(stateDir, "tmp", sha2563.slice(0, 8));
40149
+ const extractDir = join12(stateDir, "tmp", sha2563.slice(0, 8));
39634
40150
  rmSync4(extractDir, { recursive: true, force: true });
39635
40151
  mkdirSync4(extractDir, { recursive: true });
39636
40152
  const dataDir = `${extractDir}-data`;
@@ -39669,13 +40185,13 @@ async function runRubygemsArtifactStage(o) {
39669
40185
  if (members2.has("metadata.gz") && members2.has("data.tar.gz")) {
39670
40186
  let metadata;
39671
40187
  try {
39672
- metadata = gunzipSync(readFileSync6(join10(extractDir, "metadata.gz"))).toString("utf8");
40188
+ metadata = gunzipSync(readFileSync7(join12(extractDir, "metadata.gz"))).toString("utf8");
39673
40189
  } catch {
39674
40190
  throw new EngineRunError(`${gemName}: metadata.gz is not gunzip-readable \u2014 the extensions declaration cannot be verified, failing closed`, null);
39675
40191
  }
39676
40192
  rmSync4(dataDir, { recursive: true, force: true });
39677
40193
  mkdirSync4(dataDir, { recursive: true });
39678
- extractArchive(join10(extractDir, "data.tar.gz"), dataDir, {
40194
+ extractArchive(join12(extractDir, "data.tar.gz"), dataDir, {
39679
40195
  ...o.env !== void 0 ? { env: o.env } : {},
39680
40196
  ...o.timeoutMs !== void 0 ? { timeoutMs: o.timeoutMs } : {}
39681
40197
  });
@@ -39694,6 +40210,13 @@ async function runRubygemsArtifactStage(o) {
39694
40210
  })) {
39695
40211
  findings.push(f);
39696
40212
  }
40213
+ findings.push(...gemCoherenceFindings({
40214
+ specName: identity.name,
40215
+ specVersion: identity.version,
40216
+ metadataText: metadata,
40217
+ identity: gemName,
40218
+ ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
40219
+ }));
39697
40220
  }
39698
40221
  } finally {
39699
40222
  removeSandbox(extractDir);
@@ -39710,15 +40233,15 @@ function artifactTargets() {
39710
40233
  return publishChannels().map((c) => c.id);
39711
40234
  }
39712
40235
  function sha256File(path4) {
39713
- return createHash7("sha256").update(readFileSync7(path4)).digest("hex");
40236
+ return createHash7("sha256").update(readFileSync8(path4)).digest("hex");
39714
40237
  }
39715
40238
  var packSeq = 0;
39716
40239
  function packNpmArtifacts(repoDir, o = {}) {
39717
40240
  packSeq += 1;
39718
- const dest = join11(repoDir, BORDER_STATE_DIR, "tmp", `pack-${String(process.pid)}-${String(packSeq)}`);
40241
+ const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `pack-${String(process.pid)}-${String(packSeq)}`);
39719
40242
  mkdirSync5(dest, { recursive: true });
39720
40243
  try {
39721
- const r = spawnSync6("npm", ["pack", "--ignore-scripts", "--silent", `--pack-destination=${dest}`], {
40244
+ const r = spawnSync7("npm", ["pack", "--ignore-scripts", "--silent", `--pack-destination=${dest}`], {
39722
40245
  cwd: repoDir,
39723
40246
  encoding: "utf8",
39724
40247
  env: { ...o.env ?? process.env },
@@ -39727,27 +40250,27 @@ function packNpmArtifacts(repoDir, o = {}) {
39727
40250
  if (r.error !== void 0 || r.status !== 0) return null;
39728
40251
  const files = readdirSync4(dest).filter((f) => f.endsWith(".tgz")).sort();
39729
40252
  if (files.length === 0) return null;
39730
- return files.map((f) => ({ file: f, sha256: sha256File(join11(dest, f)) }));
40253
+ return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
39731
40254
  } finally {
39732
40255
  rmSync5(dest, { recursive: true, force: true });
39733
40256
  }
39734
40257
  }
39735
40258
  function packCrateArtifacts(repoDir, o = {}) {
39736
40259
  packSeq += 1;
39737
- const dest = join11(repoDir, BORDER_STATE_DIR, "tmp", `cargo-pack-${String(process.pid)}-${String(packSeq)}`);
40260
+ const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `cargo-pack-${String(process.pid)}-${String(packSeq)}`);
39738
40261
  mkdirSync5(dest, { recursive: true });
39739
40262
  try {
39740
- const r = spawnSync6("cargo", ["package", "--allow-dirty", "--no-verify"], {
40263
+ const r = spawnSync7("cargo", ["package", "--allow-dirty", "--no-verify"], {
39741
40264
  cwd: repoDir,
39742
40265
  encoding: "utf8",
39743
40266
  env: { ...o.env ?? process.env, CARGO_TARGET_DIR: dest },
39744
40267
  timeout: 12e4
39745
40268
  });
39746
40269
  if (r.error !== void 0 || r.status !== 0) return null;
39747
- const dir = join11(dest, "package");
40270
+ const dir = join13(dest, "package");
39748
40271
  const files = readdirSync4(dir).filter((f) => f.endsWith(".crate")).sort();
39749
40272
  if (files.length === 0) return null;
39750
- return files.map((f) => ({ file: f, sha256: sha256File(join11(dir, f)) }));
40273
+ return files.map((f) => ({ file: f, sha256: sha256File(join13(dir, f)) }));
39751
40274
  } finally {
39752
40275
  rmSync5(dest, { recursive: true, force: true });
39753
40276
  }
@@ -39756,11 +40279,11 @@ function packRubygemsArtifacts(repoDir, o = {}) {
39756
40279
  packSeq += 1;
39757
40280
  const ident = singleTreeGemspec(repoDir);
39758
40281
  if (ident === null) return null;
39759
- const dest = join11(repoDir, BORDER_STATE_DIR, "tmp", `gem-pack-${String(process.pid)}-${String(packSeq)}`);
40282
+ const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `gem-pack-${String(process.pid)}-${String(packSeq)}`);
39760
40283
  mkdirSync5(dest, { recursive: true });
39761
40284
  try {
39762
40285
  const gemName = `${ident.name}-${ident.version}.gem`;
39763
- const r = spawnSync6("gem", ["build", ident.path, "-o", join11(dest, gemName)], {
40286
+ const r = spawnSync7("gem", ["build", ident.path, "-o", join13(dest, gemName)], {
39764
40287
  cwd: repoDir,
39765
40288
  encoding: "utf8",
39766
40289
  env: { ...o.env ?? process.env },
@@ -39769,7 +40292,7 @@ function packRubygemsArtifacts(repoDir, o = {}) {
39769
40292
  if (r.error !== void 0 || r.status !== 0) return null;
39770
40293
  const files = readdirSync4(dest).filter((f) => f.endsWith(".gem")).sort();
39771
40294
  if (files.length === 0) return null;
39772
- return files.map((f) => ({ file: f, sha256: sha256File(join11(dest, f)) }));
40295
+ return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
39773
40296
  } finally {
39774
40297
  rmSync5(dest, { recursive: true, force: true });
39775
40298
  }
@@ -39812,231 +40335,56 @@ function verifyArtifactFreshness(record2, ctx, repoDir, o = {}) {
39812
40335
  );
39813
40336
  }
39814
40337
 
39815
- // src/ledger/records.ts
39816
- import { appendFileSync, readFileSync as readFileSync8 } from "node:fs";
39817
- import { join as join12 } from "node:path";
39818
- var LEDGER_FILE = "ledger.jsonl";
39819
- var RUNS_SUBDIR = "runs";
39820
- var HEX64 = /^[0-9a-f]{64}$/;
39821
- var HEX40 = /^[0-9a-f]{40}$/;
39822
- var HEX8 = /^[0-9a-f]{8}$/;
39823
- var PUSH_CONFIRMED_VIA = ["ls-remote", "npm-view", "pypi-json", "crates-json", "rubygems-json"];
39824
- function ledgerPath(repoDir) {
39825
- return join12(repoDir, BORDER_STATE_DIR, LEDGER_FILE);
39826
- }
39827
- function runsDir(repoDir) {
39828
- return join12(repoDir, BORDER_STATE_DIR, RUNS_SUBDIR);
39829
- }
39830
- function formatSkipLine(record2) {
39831
- return `SKIP ${record2.key8} \u2014 PASS ${record2.ts} report ${record2.reportPath}`;
39832
- }
39833
- function buildPushRecord(i) {
39834
- return {
39835
- t: "push",
39836
- key: i.key,
39837
- target: i.target,
39838
- remoteName: i.remoteName,
39839
- url: sanitizeUrl(i.url),
39840
- localSha: i.localSha,
39841
- ...i.remoteSha !== void 0 ? { remoteSha: i.remoteSha } : {},
39842
- ...i.version !== void 0 ? { version: i.version } : {},
39843
- confirmedVia: i.confirmedVia,
39844
- ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
39845
- };
40338
+ // src/ledger/retention.ts
40339
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
40340
+ import { join as join14 } from "node:path";
40341
+ var KEEP_RUN_DIRS_PER_KEY8 = 20;
40342
+ function codeOf3(err) {
40343
+ return typeof err === "object" && err !== null ? err.code : void 0;
39846
40344
  }
39847
- function bad(msg) {
39848
- throw new Error(msg);
40345
+ function runDirName(key8, ts) {
40346
+ return `${key8}-${ts.replace(/[:.]/g, "-")}`;
39849
40347
  }
39850
- function needString(o, field) {
39851
- const v = o[field];
39852
- if (typeof v !== "string" || v === "") bad(`${field} must be a non-empty string`);
39853
- return v;
40348
+ function uniqueRunDirName(repoDir, key8, ts) {
40349
+ const base = runDirName(key8, ts);
40350
+ let name = base;
40351
+ for (let n = 2; existsSync9(join14(runsDir(repoDir), name)) && n < 1e3; n += 1) {
40352
+ name = `${base}-r${String(n)}`;
40353
+ }
40354
+ return name;
39854
40355
  }
39855
- function needHex(o, field, re) {
39856
- const v = needString(o, field);
39857
- if (!re.test(v)) bad(`${field} is not hex-shaped`);
39858
- return v;
40356
+ function ensureRunDir(repoDir, name) {
40357
+ const dir = join14(runsDir(repoDir), name);
40358
+ mkdirSync6(dir, { recursive: true });
40359
+ return dir;
39859
40360
  }
39860
- function needStringArray(o, field) {
39861
- const v = o[field];
39862
- if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) bad(`${field} must be an array of strings`);
39863
- return v;
40361
+ function reportRelPath(name) {
40362
+ return `${RUNS_SUBDIR}/${name}/report.json`;
39864
40363
  }
39865
- function parseCounts(value) {
39866
- if (typeof value !== "object" || value === null) bad("counts must be an object");
39867
- const o = value;
39868
- for (const field of ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL", "total", "blocking", "warnings"]) {
39869
- if (typeof o[field] !== "number" || !Number.isInteger(o[field])) bad(`counts.${field} must be an integer`);
40364
+ function pruneRunDirs(repoDir, key8, keep = KEEP_RUN_DIRS_PER_KEY8) {
40365
+ let entries;
40366
+ try {
40367
+ entries = readdirSync5(runsDir(repoDir));
40368
+ } catch (err) {
40369
+ if (codeOf3(err) === "ENOENT") return [];
40370
+ throw err;
39870
40371
  }
39871
- return o;
40372
+ const prefix = `${key8}-`;
40373
+ const mine = entries.filter((e) => e.startsWith(prefix)).sort();
40374
+ const doomed = mine.slice(0, Math.max(0, mine.length - keep));
40375
+ for (const dir of doomed) {
40376
+ rmSync6(join14(runsDir(repoDir), dir), { recursive: true, force: true });
40377
+ }
40378
+ return doomed;
39872
40379
  }
39873
- function parseArtifacts(o) {
39874
- if (o.artifacts === null) return null;
39875
- const v = o.artifacts;
39876
- if (!Array.isArray(v)) bad("artifacts must be null or an array");
39877
- return v.map((item, i) => {
39878
- if (typeof item !== "object" || item === null) bad(`artifacts[${String(i)}] must be an object`);
39879
- const a = item;
39880
- return { file: needString(a, "file"), sha256: needHex(a, "sha256", HEX64) };
39881
- });
39882
- }
39883
- function parseLedgerRecord(value) {
39884
- if (typeof value !== "object" || value === null) bad("record must be an object");
39885
- const o = value;
39886
- const t = o.t;
39887
- if (t === "check") {
39888
- const key = needHex(o, "key", HEX64);
39889
- const key8 = needHex(o, "key8", HEX8);
39890
- if (key.slice(0, 8) !== key8) bad("key8 must be the first 8 hex chars of key");
39891
- const verdict = needString(o, "verdict");
39892
- if (verdict !== "PASS" && verdict !== "FAIL") bad(`verdict must be PASS or FAIL, got '${verdict}'`);
39893
- if (o.degraded !== false) bad("degraded must be false \u2014 degraded runs are never ledgered");
39894
- if (typeof o.llm !== "boolean") bad("llm must be a boolean");
39895
- return {
39896
- t: "check",
39897
- key,
39898
- key8,
39899
- head: needHex(o, "head", HEX40),
39900
- dirtyDigest: needHex(o, "dirtyDigest", HEX64),
39901
- refSetHash: needHex(o, "refSetHash", HEX64),
39902
- exposureSet: needStringArray(o, "exposureSet"),
39903
- effectiveTargets: needStringArray(o, "effectiveTargets"),
39904
- rulesHash: needHex(o, "rulesHash", HEX64),
39905
- artifacts: parseArtifacts(o),
39906
- llm: o.llm,
39907
- verdict,
39908
- counts: parseCounts(o.counts),
39909
- reportPath: needString(o, "reportPath"),
39910
- degraded: false,
39911
- ts: needString(o, "ts")
39912
- };
39913
- }
39914
- if (t === "push") {
39915
- const confirmedVia = needString(o, "confirmedVia");
39916
- if (!PUSH_CONFIRMED_VIA.includes(confirmedVia)) {
39917
- bad(`confirmedVia must be one of ${PUSH_CONFIRMED_VIA.join("|")}`);
39918
- }
39919
- const remoteSha = o.remoteSha;
39920
- const version2 = o.version;
39921
- if (remoteSha === void 0 && version2 === void 0) bad("push record needs remoteSha or version");
39922
- if (remoteSha !== void 0 && typeof remoteSha !== "string") bad("remoteSha must be a string");
39923
- if (version2 !== void 0 && typeof version2 !== "string") bad("version must be a string");
39924
- return {
39925
- t: "push",
39926
- key: needHex(o, "key", HEX64),
39927
- target: needString(o, "target"),
39928
- remoteName: needString(o, "remoteName"),
39929
- url: needString(o, "url"),
39930
- localSha: needString(o, "localSha"),
39931
- ...remoteSha !== void 0 ? { remoteSha } : {},
39932
- ...version2 !== void 0 ? { version: version2 } : {},
39933
- confirmedVia,
39934
- ts: needString(o, "ts")
39935
- };
39936
- }
39937
- return bad(`unknown record type ${JSON.stringify(t)}`);
39938
- }
39939
- function codeOf2(err) {
39940
- return typeof err === "object" && err !== null ? err.code : void 0;
39941
- }
39942
- function readLedger(repoDir) {
39943
- let text;
39944
- try {
39945
- text = readFileSync8(ledgerPath(repoDir), "utf8");
39946
- } catch (err) {
39947
- if (codeOf2(err) === "ENOENT") return { records: [], warnings: [] };
39948
- throw err;
39949
- }
39950
- const records = [];
39951
- const warnings = [];
39952
- text.split("\n").forEach((line, i) => {
39953
- if (line.trim() === "") return;
39954
- try {
39955
- records.push(parseLedgerRecord(JSON.parse(line)));
39956
- } catch (err) {
39957
- const reason = err instanceof Error ? err.message : String(err);
39958
- warnings.push(`ledger line ${String(i + 1)} unreadable (${reason.slice(0, 160)}) \u2014 line skipped`);
39959
- }
39960
- });
39961
- return { records, warnings };
39962
- }
39963
- function appendRecord(repoDir, record2) {
39964
- ensureStateDir(repoDir);
39965
- appendFileSync(ledgerPath(repoDir), `${JSON.stringify(record2)}
39966
- `, "utf8");
39967
- }
39968
- function newestRecordOf(records, key) {
39969
- for (let i = records.length - 1; i >= 0; i -= 1) {
39970
- const r = records[i];
39971
- if (r.t === "check" && r.key === key) return r;
39972
- }
39973
- return null;
39974
- }
39975
- function isSkippable(r) {
39976
- return r !== null && r.verdict === "PASS" && r.degraded === false;
39977
- }
39978
- function lookupSkipRecord(records, key, llm) {
39979
- const newest = newestRecordOf(records, key);
39980
- return isSkippable(newest) && newest.llm === llm ? newest : null;
39981
- }
39982
- function latestPassCoveringTargets(records, key, targets) {
39983
- const newest = newestRecordOf(records, key);
39984
- return isSkippable(newest) && targets.every((t) => newest.effectiveTargets.includes(t)) ? newest : null;
39985
- }
39986
- function pushRecords(records, key) {
39987
- return records.filter((r) => r.t === "push" && (key === void 0 || r.key === key));
39988
- }
39989
-
39990
- // src/ledger/retention.ts
39991
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
39992
- import { join as join13 } from "node:path";
39993
- var KEEP_RUN_DIRS_PER_KEY8 = 20;
39994
- function codeOf3(err) {
39995
- return typeof err === "object" && err !== null ? err.code : void 0;
39996
- }
39997
- function runDirName(key8, ts) {
39998
- return `${key8}-${ts.replace(/[:.]/g, "-")}`;
39999
- }
40000
- function uniqueRunDirName(repoDir, key8, ts) {
40001
- const base = runDirName(key8, ts);
40002
- let name = base;
40003
- for (let n = 2; existsSync9(join13(runsDir(repoDir), name)) && n < 1e3; n += 1) {
40004
- name = `${base}-r${String(n)}`;
40005
- }
40006
- return name;
40007
- }
40008
- function ensureRunDir(repoDir, name) {
40009
- const dir = join13(runsDir(repoDir), name);
40010
- mkdirSync6(dir, { recursive: true });
40011
- return dir;
40012
- }
40013
- function reportRelPath(name) {
40014
- return `${RUNS_SUBDIR}/${name}/report.json`;
40015
- }
40016
- function pruneRunDirs(repoDir, key8, keep = KEEP_RUN_DIRS_PER_KEY8) {
40017
- let entries;
40018
- try {
40019
- entries = readdirSync5(runsDir(repoDir));
40020
- } catch (err) {
40021
- if (codeOf3(err) === "ENOENT") return [];
40022
- throw err;
40023
- }
40024
- const prefix = `${key8}-`;
40025
- const mine = entries.filter((e) => e.startsWith(prefix)).sort();
40026
- const doomed = mine.slice(0, Math.max(0, mine.length - keep));
40027
- for (const dir of doomed) {
40028
- rmSync6(join13(runsDir(repoDir), dir), { recursive: true, force: true });
40029
- }
40030
- return doomed;
40031
- }
40032
-
40033
- // src/ledger.ts
40034
- async function computeFingerprint(repoDir, cfg, configDigest, effectiveTargets, o = {}) {
40035
- const envOpt = o.env !== void 0 ? { env: o.env } : {};
40036
- const ctx = await gatherContext(repoDir, envOpt);
40037
- const probe = await probeEngines(cfg, {
40038
- ...envOpt,
40039
- ...o.requireOverride !== void 0 ? { requireOverride: o.requireOverride } : {}
40380
+
40381
+ // src/ledger.ts
40382
+ async function computeFingerprint(repoDir, cfg, configDigest, effectiveTargets, o = {}) {
40383
+ const envOpt = o.env !== void 0 ? { env: o.env } : {};
40384
+ const ctx = await gatherContext(repoDir, envOpt);
40385
+ const probe = await probeEngines(cfg, {
40386
+ ...envOpt,
40387
+ ...o.requireOverride !== void 0 ? { requireOverride: o.requireOverride } : {}
40040
40388
  });
40041
40389
  const rulesHash = await computeCheckRulesHash({
40042
40390
  engineVersions: probe.engineVersions,
@@ -40091,9 +40439,9 @@ function recordCheckRun(i) {
40091
40439
  const envOpt = i.env !== void 0 ? { env: i.env } : {};
40092
40440
  const artifacts = i.artifacts ?? (i.report.verdict === "PASS" && !i.ctx.dirty && i.effectiveTargets.includes("npm") ? packNpmArtifacts(i.repoDir, envOpt) : null);
40093
40441
  const persisted = artifacts !== null ? { ...i.report, artifacts } : i.report;
40094
- writeFileSync3(join14(i.repoDir, BORDER_STATE_DIR, relReport), `${JSON.stringify(persisted, null, 2)}
40442
+ writeFileSync3(join15(i.repoDir, BORDER_STATE_DIR, relReport), `${JSON.stringify(persisted, null, 2)}
40095
40443
  `, "utf8");
40096
- writeFileSync3(join14(i.repoDir, BORDER_STATE_DIR, relReport.replace("report.json", "report.md")), renderReportMd(i.report), "utf8");
40444
+ writeFileSync3(join15(i.repoDir, BORDER_STATE_DIR, relReport.replace("report.json", "report.md")), renderReportMd(i.report), "utf8");
40097
40445
  const record2 = {
40098
40446
  t: "check",
40099
40447
  key: i.report.key,
@@ -40118,13 +40466,13 @@ function recordCheckRun(i) {
40118
40466
  }
40119
40467
 
40120
40468
  // src/channels/pypi.ts
40121
- import { spawnSync as spawnSync7 } from "node:child_process";
40469
+ import { spawnSync as spawnSync8 } from "node:child_process";
40122
40470
  import { relative as relative5 } from "node:path";
40123
40471
 
40124
40472
  // src/artifacts/pypi.ts
40125
40473
  import { createHash as createHash9 } from "node:crypto";
40126
40474
  import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, readdirSync as readdirSync6, rmdirSync as rmdirSync2, rmSync as rmSync7 } from "node:fs";
40127
- import { basename as basename2, dirname as dirname7, join as join15, relative as relative4, resolve as resolve7 } from "node:path";
40475
+ import { basename as basename3, dirname as dirname7, join as join16, relative as relative4, resolve as resolve7 } from "node:path";
40128
40476
 
40129
40477
  // src/artifacts/residuePy.ts
40130
40478
  function residuePypiHits(pkgDir, innerRels) {
@@ -40188,13 +40536,13 @@ function requireModule(o, module) {
40188
40536
  function buildPypiArtifacts(o) {
40189
40537
  requireModule(o, "build");
40190
40538
  const repoDir = resolve7(o.repoDir);
40191
- if (!existsSync10(join15(repoDir, "pyproject.toml"))) {
40539
+ if (!existsSync10(join16(repoDir, "pyproject.toml"))) {
40192
40540
  throw new PypiPrerequisiteError(`no pyproject.toml in the repo root \u2014 the PyPI pipeline builds from pyproject's own [build-system]`);
40193
40541
  }
40194
- const distDir = join15(resolve7(o.stateDir ?? join15(repoDir, ".border")), "dist");
40542
+ const distDir = join16(resolve7(o.stateDir ?? join16(repoDir, ".border")), "dist");
40195
40543
  mkdirSync7(distDir, { recursive: true });
40196
40544
  for (const name of readdirSync6(distDir)) {
40197
- if (name.endsWith(".tar.gz") || name.endsWith(".whl")) rmSync7(join15(distDir, name), { force: true });
40545
+ if (name.endsWith(".tar.gz") || name.endsWith(".whl")) rmSync7(join16(distDir, name), { force: true });
40198
40546
  }
40199
40547
  const out = spawnEngine(binaryCandidates("python3", o), ["-m", "build", "--no-isolation", "--outdir", distDir, repoDir], o);
40200
40548
  if (out.status !== 0) {
@@ -40205,13 +40553,13 @@ function buildPypiArtifacts(o) {
40205
40553
  throw new EngineRunError(`\`python3 -m build\` produced no .tar.gz/.whl in ${distDir} (border exit 2)`, null);
40206
40554
  }
40207
40555
  const artifacts = names.map((name) => {
40208
- const path4 = join15(distDir, name);
40556
+ const path4 = join16(distDir, name);
40209
40557
  return { path: path4, kind: name.endsWith(".whl") ? "wheel" : "sdist", sha256: createHash9("sha256").update(readFileSync9(path4)).digest("hex") };
40210
40558
  });
40211
40559
  return { artifacts, distDir };
40212
40560
  }
40213
40561
  function walkFilesRel(root) {
40214
- return readdirSync6(root, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walkFilesRel(join15(root, e.name)).map((r) => `${e.name}/${r}`) : e.isFile() ? [e.name] : []);
40562
+ return readdirSync6(root, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walkFilesRel(join16(root, e.name)).map((r) => `${e.name}/${r}`) : e.isFile() ? [e.name] : []);
40215
40563
  }
40216
40564
  function stripWrapper(entries) {
40217
40565
  const tops = new Set(entries.map((e) => e.includes("/") ? e.slice(0, e.indexOf("/")) : "\0"));
@@ -40224,7 +40572,7 @@ function toInner(findingPath, scanRoot) {
40224
40572
  const head = bang === -1 ? findingPath : findingPath.slice(0, bang);
40225
40573
  const rest = bang === -1 ? "" : findingPath.slice(bang + 1);
40226
40574
  const rel = relative4(scanRoot, head);
40227
- const inner = rel !== "" && !rel.startsWith("..") ? rel : basename2(head);
40575
+ const inner = rel !== "" && !rel.startsWith("..") ? rel : basename3(head);
40228
40576
  return rest === "" ? inner : `${inner}!${rest}`;
40229
40577
  }
40230
40578
  function gitTracked(o, repoDir) {
@@ -40241,7 +40589,7 @@ function toRe(globs) {
40241
40589
  function explainMatchers(repoDir) {
40242
40590
  const inc = [];
40243
40591
  const exc = [];
40244
- const manifest = join15(repoDir, "MANIFEST.in");
40592
+ const manifest = join16(repoDir, "MANIFEST.in");
40245
40593
  if (existsSync10(manifest)) {
40246
40594
  for (const line of readFileSync9(manifest, "utf8").split(/\r?\n/)) {
40247
40595
  const t = line.split("#")[0]?.trim() ?? "";
@@ -40256,7 +40604,7 @@ function explainMatchers(repoDir) {
40256
40604
  else if (cmd === "prune") exc.push(`${dir}/**`);
40257
40605
  }
40258
40606
  }
40259
- const pyproject = join15(repoDir, "pyproject.toml");
40607
+ const pyproject = join16(repoDir, "pyproject.toml");
40260
40608
  if (existsSync10(pyproject)) {
40261
40609
  const text = readFileSync9(pyproject, "utf8");
40262
40610
  const readme = /^\s*readme\s*=\s*"([^"]+)"/m.exec(text);
@@ -40308,7 +40656,7 @@ ${out.stderr}`.replace(ANSI_RE, "");
40308
40656
  }
40309
40657
  return failed.map((m) => {
40310
40658
  const abs = m[1] ?? "";
40311
- const base = basename2(abs);
40659
+ const base = basename3(abs);
40312
40660
  const idx = text.indexOf(m[0]);
40313
40661
  const next = text.indexOf("Checking ", idx + m[0].length);
40314
40662
  const block = tail2(text.slice(idx, next === -1 ? void 0 : next), 400).replaceAll(abs, base);
@@ -40329,10 +40677,10 @@ async function scanPyPiArtifacts(o) {
40329
40677
  requireModule(o, "build");
40330
40678
  requireModule(o, "twine");
40331
40679
  const repoDir = resolve7(o.repoDir);
40332
- const stateDir = resolve7(o.stateDir ?? join15(repoDir, ".border"));
40680
+ const stateDir = resolve7(o.stateDir ?? join16(repoDir, ".border"));
40333
40681
  const target = o.target ?? "artifact";
40334
40682
  const { artifacts } = buildPypiArtifacts(o);
40335
- const tmpParent = join15(stateDir, "tmp");
40683
+ const tmpParent = join16(stateDir, "tmp");
40336
40684
  mkdirSync7(tmpParent, { recursive: true });
40337
40685
  const merged = /* @__PURE__ */ new Map();
40338
40686
  const absorb = (f, path4) => {
@@ -40341,16 +40689,16 @@ async function scanPyPiArtifacts(o) {
40341
40689
  };
40342
40690
  try {
40343
40691
  for (const artifact of artifacts) {
40344
- const sandbox = mkdtempSync2(join15(tmpParent, "pypi-"));
40692
+ const sandbox = mkdtempSync2(join16(tmpParent, "pypi-"));
40345
40693
  try {
40346
40694
  if (artifact.kind === "wheel") {
40347
40695
  const z2 = spawnEngine(binaryCandidates("python3", o), ["-m", "zipfile", "-e", artifact.path, sandbox], o);
40348
- if (z2.status !== 0) throw new EngineRunError(`\`python3 -m zipfile -e ${basename2(artifact.path)}\` exited ${String(z2.status)} (border exit 2): ${tail2(z2.stderr)}`, z2.status);
40696
+ if (z2.status !== 0) throw new EngineRunError(`\`python3 -m zipfile -e ${basename3(artifact.path)}\` exited ${String(z2.status)} (border exit 2): ${tail2(z2.stderr)}`, z2.status);
40349
40697
  } else {
40350
40698
  extractArchive(artifact.path, sandbox, o);
40351
40699
  }
40352
40700
  const { root, rest } = artifact.kind === "sdist" ? stripWrapper(walkFilesRel(sandbox)) : { root: "", rest: walkFilesRel(sandbox) };
40353
- const scanRoot = join15(sandbox, root);
40701
+ const scanRoot = join16(sandbox, root);
40354
40702
  const scanOpts = { ...o, target };
40355
40703
  if (o.skipGitleaks !== true) {
40356
40704
  for (const f of scanTree({ ...scanOpts, dir: scanRoot, stateDir })) absorb(f, toInner(f.path, scanRoot));
@@ -40359,9 +40707,23 @@ async function scanPyPiArtifacts(o) {
40359
40707
  for (const f of await scanPaths({ ...scanOpts, ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}, ...o.rules !== void 0 ? { rules: o.rules } : {}, dir: scanRoot, files: rest })) absorb(f, f.path ?? "");
40360
40708
  }
40361
40709
  for (const f of residueFindings(residuePypiHits(scanRoot, rest), {
40362
- root: basename2(artifact.path),
40710
+ root: basename3(artifact.path),
40363
40711
  sep: "!",
40364
- identity: basename2(artifact.path),
40712
+ identity: basename3(artifact.path),
40713
+ ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
40714
+ })) absorb(f, f.path ?? "");
40715
+ for (const f of pypiArtifactCoherenceFindings({
40716
+ kind: artifact.kind,
40717
+ archiveName: basename3(artifact.path),
40718
+ files: rest,
40719
+ read: (rel) => {
40720
+ try {
40721
+ return readFileSync9(join16(scanRoot, rel), "utf8");
40722
+ } catch {
40723
+ return null;
40724
+ }
40725
+ },
40726
+ identity: basename3(artifact.path),
40365
40727
  ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
40366
40728
  })) absorb(f, f.path ?? "");
40367
40729
  if (artifact.kind === "sdist") for (const f of manifestFindings(o, rest, target)) absorb(f, f.path ?? "");
@@ -40383,7 +40745,7 @@ async function scanPyPiArtifacts(o) {
40383
40745
  import { spawn } from "node:child_process";
40384
40746
  import { createHash as createHash10 } from "node:crypto";
40385
40747
  import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
40386
- import { basename as basename3, join as join16 } from "node:path";
40748
+ import { basename as basename4, join as join17 } from "node:path";
40387
40749
 
40388
40750
  // src/cli/exit.ts
40389
40751
  var EXIT_PASS = 0;
@@ -40424,7 +40786,7 @@ function translateError(err) {
40424
40786
 
40425
40787
  // src/push/core.ts
40426
40788
  var PUBLISH_WARNING = "WARNING: published versions cannot be re-uploaded unchanged; deletion windows differ per registry \u2014 verify version bump policy";
40427
- var DIST_DIR = join16(BORDER_STATE_DIR, "dist");
40789
+ var DIST_DIR = join17(BORDER_STATE_DIR, "dist");
40428
40790
  var defaultOut = (line) => void process.stdout.write(`${line}
40429
40791
  `);
40430
40792
  var defaultErr = (line) => void process.stderr.write(`${line}
@@ -40453,8 +40815,8 @@ function rehashRecordedArtifacts(kind, extensions, record2, repoDir) {
40453
40815
  }
40454
40816
  const files = [];
40455
40817
  for (const a of entries) {
40456
- const rel = join16(DIST_DIR, basename3(a.file));
40457
- const abs = join16(repoDir, rel);
40818
+ const rel = join17(DIST_DIR, basename4(a.file));
40819
+ const abs = join17(repoDir, rel);
40458
40820
  if (!existsSync11(abs)) return fail3(`${rel} is missing from the working tree`);
40459
40821
  const digest2 = createHash10("sha256").update(readFileSync10(abs)).digest("hex");
40460
40822
  if (digest2 !== a.sha256) return fail3(`${rel} no longer hashes to the digest recorded by the check`);
@@ -40630,7 +40992,7 @@ function twineAvailable(i, command) {
40630
40992
  let status = null;
40631
40993
  let failed = false;
40632
40994
  try {
40633
- const res = spawnSync7(command.bin, args, {
40995
+ const res = spawnSync8(command.bin, args, {
40634
40996
  encoding: "utf8",
40635
40997
  stdio: "ignore",
40636
40998
  ...i.env === void 0 ? {} : { env: { ...i.env } }
@@ -40945,7 +41307,7 @@ function npmNameClaimed(o) {
40945
41307
  return true;
40946
41308
  }
40947
41309
  function npmViewJson(args, registry2, timeoutMs, env2) {
40948
- return new Promise((resolve15, reject) => {
41310
+ return new Promise((resolve16, reject) => {
40949
41311
  const child = spawn2("npm", ["view", ...args, "--registry", registry2, "--fetch-retries=0", "--json"], {
40950
41312
  stdio: ["ignore", "pipe", "pipe"],
40951
41313
  ...env2 === void 0 ? {} : { env: env2 }
@@ -40964,7 +41326,7 @@ function npmViewJson(args, registry2, timeoutMs, env2) {
40964
41326
  });
40965
41327
  child.on("close", (status) => {
40966
41328
  clearTimeout(timer);
40967
- resolve15({ status: status ?? -1, stdout, stderr });
41329
+ resolve16({ status: status ?? -1, stdout, stderr });
40968
41330
  });
40969
41331
  });
40970
41332
  }
@@ -41060,14 +41422,14 @@ var npmChannel = {
41060
41422
  // src/channels/crates.ts
41061
41423
  import { createHash as createHash12 } from "node:crypto";
41062
41424
  import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync12, readdirSync as readdirSync8, rmSync as rmSync9 } from "node:fs";
41063
- import { join as join18, resolve as resolve8 } from "node:path";
41064
- import { spawnSync as spawnSync9 } from "node:child_process";
41425
+ import { join as join19, resolve as resolve8 } from "node:path";
41426
+ import { spawnSync as spawnSync10 } from "node:child_process";
41065
41427
 
41066
41428
  // src/artifacts/crates.ts
41067
41429
  import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, readdirSync as readdirSync7, renameSync, rmSync as rmSync8 } from "node:fs";
41068
41430
  import { createHash as createHash11 } from "node:crypto";
41069
- import { join as join17, relative as relative6 } from "node:path";
41070
- import { spawnSync as spawnSync8 } from "node:child_process";
41431
+ import { join as join18, relative as relative6 } from "node:path";
41432
+ import { spawnSync as spawnSync9 } from "node:child_process";
41071
41433
 
41072
41434
  // src/artifacts/residueRust.ts
41073
41435
  function residueCratesHits(pkgDir, innerRels) {
@@ -41266,7 +41628,7 @@ function listFiles3(dir) {
41266
41628
  while (stack.length > 0) {
41267
41629
  const cur = stack.pop();
41268
41630
  for (const ent of readdirSync7(cur, { withFileTypes: true })) {
41269
- const p = join17(cur, ent.name);
41631
+ const p = join18(cur, ent.name);
41270
41632
  if (ent.isDirectory()) stack.push(p);
41271
41633
  else if (ent.isFile()) out.push(p);
41272
41634
  }
@@ -41274,7 +41636,7 @@ function listFiles3(dir) {
41274
41636
  return out;
41275
41637
  }
41276
41638
  function manifestIdentity(repoDir, cfg) {
41277
- const manifestPath = join17(repoDir, "Cargo.toml");
41639
+ const manifestPath = join18(repoDir, "Cargo.toml");
41278
41640
  if (!existsSync12(manifestPath)) {
41279
41641
  throw new EngineRunError("targets.crates is configured but the repo has no Cargo.toml to package", null);
41280
41642
  }
@@ -41302,7 +41664,7 @@ async function runCargoArtifactStage(o) {
41302
41664
  const bin = o.cargoBinPath ?? "cargo";
41303
41665
  const identity = manifestIdentity(repoDir, o.cfg);
41304
41666
  const crateName = `${identity.name}-${identity.version}.crate`;
41305
- const versionCheck = spawnSync8(bin, ["--version"], {
41667
+ const versionCheck = spawnSync9(bin, ["--version"], {
41306
41668
  encoding: "utf8",
41307
41669
  stdio: "ignore",
41308
41670
  ...o.env === void 0 ? {} : { env: { ...o.env } }
@@ -41313,14 +41675,14 @@ async function runCargoArtifactStage(o) {
41313
41675
  );
41314
41676
  }
41315
41677
  ensureStateDir(repoDir);
41316
- const stateDir = join17(repoDir, BORDER_STATE_DIR);
41317
- const distDir = join17(stateDir, "dist");
41318
- const targetDir = join17(stateDir, "tmp", "cargo-target");
41678
+ const stateDir = join18(repoDir, BORDER_STATE_DIR);
41679
+ const distDir = join18(stateDir, "dist");
41680
+ const targetDir = join18(stateDir, "tmp", "cargo-target");
41319
41681
  mkdirSync8(distDir, { recursive: true });
41320
41682
  rmSync8(targetDir, { recursive: true, force: true });
41321
41683
  mkdirSync8(targetDir, { recursive: true });
41322
41684
  try {
41323
- const result = spawnSync8(bin, ["package", "--allow-dirty", "--no-verify"], {
41685
+ const result = spawnSync9(bin, ["package", "--allow-dirty", "--no-verify"], {
41324
41686
  cwd: repoDir,
41325
41687
  encoding: "utf8",
41326
41688
  timeout: o.cargoTimeoutMs ?? 12e4,
@@ -41335,17 +41697,17 @@ async function runCargoArtifactStage(o) {
41335
41697
  result.status
41336
41698
  );
41337
41699
  }
41338
- const packedDir = join17(targetDir, "package");
41339
- const packed = join17(packedDir, crateName);
41700
+ const packedDir = join18(targetDir, "package");
41701
+ const packed = join18(packedDir, crateName);
41340
41702
  if (!existsSync12(packed)) {
41341
41703
  throw new EngineRunError(`cargo package produced no ${crateName} in ${relative6(repoDir, packedDir)} \u2014 failing closed`, null);
41342
41704
  }
41343
- const dist = join17(distDir, crateName);
41705
+ const dist = join18(distDir, crateName);
41344
41706
  renameSync(packed, dist);
41345
41707
  const bytes = readFileSync11(dist);
41346
41708
  const sha2563 = createHash11("sha256").update(bytes).digest("hex");
41347
41709
  const findings = [];
41348
- const extractDir = join17(stateDir, "tmp", sha2563.slice(0, 8));
41710
+ const extractDir = join18(stateDir, "tmp", sha2563.slice(0, 8));
41349
41711
  rmSync8(extractDir, { recursive: true, force: true });
41350
41712
  mkdirSync8(extractDir, { recursive: true });
41351
41713
  try {
@@ -41384,6 +41746,15 @@ async function runCargoArtifactStage(o) {
41384
41746
  identity: crateName,
41385
41747
  ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
41386
41748
  }));
41749
+ const lockRel = `${identity.name}-${identity.version}/Cargo.lock`;
41750
+ findings.push(...cratesCoherenceFindings({
41751
+ name: identity.name,
41752
+ version: identity.version,
41753
+ lockText: relPaths.includes(lockRel) ? readFileSync11(join18(extractDir, lockRel), "utf8") : null,
41754
+ lockRelPath: lockRel,
41755
+ identity: crateName,
41756
+ ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
41757
+ }));
41387
41758
  } finally {
41388
41759
  removeSandbox(extractDir);
41389
41760
  }
@@ -41539,11 +41910,11 @@ function cratesPublishArgv(files, _cfg) {
41539
41910
  return files.map(() => ["publish", "--allow-dirty", "--no-verify"]);
41540
41911
  }
41541
41912
  function repackageSameBytes(i, bin) {
41542
- const targetDir = join18(resolve8(i.repoDir), BORDER_STATE_DIR, "tmp", "publish-repackage");
41913
+ const targetDir = join19(resolve8(i.repoDir), BORDER_STATE_DIR, "tmp", "publish-repackage");
41543
41914
  rmSync9(targetDir, { recursive: true, force: true });
41544
41915
  mkdirSync9(targetDir, { recursive: true });
41545
41916
  try {
41546
- const r = spawnSync9(bin, ["package", "--allow-dirty", "--no-verify"], {
41917
+ const r = spawnSync10(bin, ["package", "--allow-dirty", "--no-verify"], {
41547
41918
  cwd: i.repoDir,
41548
41919
  encoding: "utf8",
41549
41920
  timeout: 12e4,
@@ -41555,14 +41926,14 @@ function repackageSameBytes(i, bin) {
41555
41926
  if (r.status !== 0) {
41556
41927
  return { ok: false, message: `cargo package --allow-dirty --no-verify (pre-publish repackage) exited ${String(r.status)} \u2014 no retry loop, push blocked (fail-closed): ${(r.stderr ?? "").trim().slice(0, 300)}`, exit: EXIT_ERROR };
41557
41928
  }
41558
- const packedDir = join18(targetDir, "package");
41929
+ const packedDir = join19(targetDir, "package");
41559
41930
  const crates = readdirSync8(packedDir).filter((f) => f.endsWith(".crate")).sort();
41560
41931
  if (crates.length !== 1) {
41561
41932
  return { ok: false, message: `pre-publish repackage produced ${String(crates.length)} .crate file(s) \u2014 expected exactly 1, push blocked (fail-closed)`, exit: EXIT_ERROR };
41562
41933
  }
41563
41934
  const crateFile = crates[0] ?? "";
41564
- const freshSha = createHash12("sha256").update(readFileSync12(join18(packedDir, crateFile))).digest("hex");
41565
- const staged = join18(resolve8(i.repoDir), BORDER_STATE_DIR, "dist", crateFile);
41935
+ const freshSha = createHash12("sha256").update(readFileSync12(join19(packedDir, crateFile))).digest("hex");
41936
+ const staged = join19(resolve8(i.repoDir), BORDER_STATE_DIR, "dist", crateFile);
41566
41937
  if (!existsSync13(staged)) {
41567
41938
  return { ok: false, message: `${crateFile} is missing from .border/dist \u2014 the PASSED check recorded no such crate; re-run border check (push blocked)`, exit: EXIT_ERROR };
41568
41939
  }
@@ -41955,8 +42326,32 @@ function buildBorderConfigSchema() {
41955
42326
  // of the residue scan is this toggle; a scan that cannot RUN is a FAIL
41956
42327
  // (fail-closed doctrine), and the digest seam keeps every cached PASS
41957
42328
  // honest about the residue sources it was certified against.
42329
+ // W2.2 (border-inspect-roadmap): requireProof arms the PROOF VALVE — a
42330
+ // blocking residue-* row on a staged artifact then additionally demands a
42331
+ // fresh `t:"roundtrip"` ledger record for that artifact's sha256 before
42332
+ // the channel may PASS. Default FALSE (0.3.x behavior byte-identical);
42333
+ // the valve reads `residue?.requireProof === true`, so absence IS the
42334
+ // false default — same construction contract as the outer key. The flag
42335
+ // rides the whole-object configDigest (stableStringify), so flipping it
42336
+ // rotates the check rulesHash and the ledger refuses every cached PASS.
41958
42337
  residue: external_exports.object({
41959
- enabled: external_exports.boolean().default(true)
42338
+ enabled: external_exports.boolean().default(true),
42339
+ requireProof: external_exports.boolean().default(false)
42340
+ }).strict().optional(),
42341
+ // W4.1 release-coherence twin obligations (plan border-inspect-roadmap §127-138):
42342
+ // the ONLY cross-manager declaration border can statically enforce — when a project
42343
+ // ships a PyPI/npm twin pair under one release train, their versions must be equal.
42344
+ // The per-artifact internal coherence rules (release-coherence-*) are ALWAYS on and
42345
+ // need no opt-in; this list is opt-in and strict — unknown keys fail typed exit 2.
42346
+ // The pair list rides the whole-object configDigest, so editing it rotates the check
42347
+ // rulesHash exactly like the residue block above.
42348
+ release: external_exports.object({
42349
+ twin: external_exports.array(
42350
+ external_exports.object({
42351
+ pypi: external_exports.string().min(1),
42352
+ npm: external_exports.string().min(1)
42353
+ }).strict()
42354
+ ).default([])
41960
42355
  }).strict().optional()
41961
42356
  }).strict();
41962
42357
  }
@@ -42044,7 +42439,7 @@ function deepMergeRaw(base, overlay) {
42044
42439
  return overlay;
42045
42440
  }
42046
42441
  function runGit3(cwd, args) {
42047
- const r = spawnSync10("git", args, { cwd, encoding: "utf8" });
42442
+ const r = spawnSync11("git", args, { cwd, encoding: "utf8" });
42048
42443
  return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
42049
42444
  }
42050
42445
  function gitToplevel(cwd) {
@@ -42104,7 +42499,7 @@ function loadConfig(options = {}) {
42104
42499
  throw new ConfigError("unreadable", `config file not found: ${basePath}`);
42105
42500
  }
42106
42501
  } else {
42107
- for (const candidate of [join19(cwd, "border.yaml"), toplevel === void 0 ? "" : join19(toplevel, "border.yaml")]) {
42502
+ for (const candidate of [join20(cwd, "border.yaml"), toplevel === void 0 ? "" : join20(toplevel, "border.yaml")]) {
42108
42503
  if (candidate !== "" && existsSync14(candidate)) {
42109
42504
  basePath = candidate;
42110
42505
  baseText = readFileSync13(candidate, "utf8");
@@ -42119,7 +42514,7 @@ function loadConfig(options = {}) {
42119
42514
  ])];
42120
42515
  let overlay;
42121
42516
  for (const dir of overlayDirs) {
42122
- const p = join19(dir, ".border", "config.local.yaml");
42517
+ const p = join20(dir, ".border", "config.local.yaml");
42123
42518
  const text = readFileOrUndefined(p);
42124
42519
  if (text !== void 0) {
42125
42520
  overlay = { path: p, doc: yamlToJson(text, p) };
@@ -42164,10 +42559,10 @@ function exposureSet(cfg, options = {}) {
42164
42559
  }
42165
42560
 
42166
42561
  // src/check.ts
42167
- import { join as join21 } from "node:path";
42562
+ import { join as join22 } from "node:path";
42168
42563
 
42169
42564
  // src/rules/aiArtifacts.ts
42170
- import { spawnSync as spawnSync11 } from "node:child_process";
42565
+ import { spawnSync as spawnSync12 } from "node:child_process";
42171
42566
  import { dirname as dirname9, resolve as resolve10 } from "node:path";
42172
42567
  var AI_ARTIFACTS_ENGINE = "ai-artifacts";
42173
42568
  var BINARY_SNIFF_BYTES = 8e3;
@@ -42186,7 +42581,7 @@ function makeCtx(o) {
42186
42581
  return { repoDir, env: env2, timeoutMs: o.timeoutMs ?? 3e5, maxBufferBytes: o.maxBufferBytes ?? 32 * 1024 * 1024 };
42187
42582
  }
42188
42583
  function gitSpawn2(c, args, input2) {
42189
- const r = spawnSync11("git", ["-C", c.repoDir, ...args], {
42584
+ const r = spawnSync12("git", ["-C", c.repoDir, ...args], {
42190
42585
  encoding: "buffer",
42191
42586
  timeout: c.timeoutMs,
42192
42587
  maxBuffer: c.maxBufferBytes,
@@ -42407,11 +42802,49 @@ function applyAllowList(findings, allow, repoDir) {
42407
42802
  return { kept, allowHits: [...hits.values()] };
42408
42803
  }
42409
42804
 
42805
+ // src/check/proofValve.ts
42806
+ var ROUNDTRIP_PROOF_MISSING_RULE = "roundtrip-proof-missing";
42807
+ var ROUNDTRIP_PROOF_STALE_RULE = "roundtrip-proof-stale";
42808
+ var RESIDUE_RULE_IDS = new Set(Object.keys(RESIDUE_SEVERITIES));
42809
+ function hasBlockingResidueCapability(findings) {
42810
+ return findings.some((f) => RESIDUE_RULE_IDS.has(f.rule) && isBlocking(f.severity));
42811
+ }
42812
+ function proofFinding(rule, art, cause) {
42813
+ return {
42814
+ rule,
42815
+ severity: "CRITICAL",
42816
+ target: "artifact",
42817
+ path: art.file,
42818
+ engine: "native",
42819
+ message: `${art.file} (sha256 ${art.sha256.slice(0, 12)}\u2026) tripped the residue scan while residue.requireProof is on, but ${cause}. Run 'border roundtrip <spec>' for exactly these bytes to mint the ledger proof.`,
42820
+ ...redact(`${art.sha256}:${cause}`),
42821
+ snippet: `${art.file} ${art.sha256.slice(0, 12)}`
42822
+ };
42823
+ }
42824
+ function proofFindings(o) {
42825
+ const out = [];
42826
+ for (const art of o.artifacts) {
42827
+ const rec = latestRoundtripForSha(o.records, art.sha256);
42828
+ if (rec === null) {
42829
+ out.push(proofFinding(ROUNDTRIP_PROOF_MISSING_RULE, art, "no roundtrip proof record exists for its digest"));
42830
+ } else if (rec.rulesHash !== o.rulesHash) {
42831
+ out.push(
42832
+ proofFinding(
42833
+ ROUNDTRIP_PROOF_STALE_RULE,
42834
+ art,
42835
+ `its proof record predates the current rules policy (recorded ${rec.rulesHash.slice(0, 12)}\u2026, required ${o.rulesHash.slice(0, 12)}\u2026)`
42836
+ )
42837
+ );
42838
+ }
42839
+ }
42840
+ return out;
42841
+ }
42842
+
42410
42843
  // src/check/tagScan.ts
42411
- import { spawnSync as spawnSync12 } from "node:child_process";
42844
+ import { spawnSync as spawnSync13 } from "node:child_process";
42412
42845
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync10, writeFileSync as writeFileSync4 } from "node:fs";
42413
42846
  import { tmpdir as tmpdir2 } from "node:os";
42414
- import { join as join20, resolve as resolve12 } from "node:path";
42847
+ import { join as join21, resolve as resolve12 } from "node:path";
42415
42848
  var TAG_MESSAGE_RULE = "tag-message-secret";
42416
42849
  function isEnoent2(error61) {
42417
42850
  return typeof error61 === "object" && error61 !== null && error61.code === "ENOENT";
@@ -42431,7 +42864,7 @@ function stdinFindings(text, ignorePath, o) {
42431
42864
  ];
42432
42865
  let lastError;
42433
42866
  for (const bin of binaryCandidates("gitleaks", o)) {
42434
- const r = spawnSync12(bin, args, {
42867
+ const r = spawnSync13(bin, args, {
42435
42868
  encoding: "utf8",
42436
42869
  input: text,
42437
42870
  timeout: o.timeoutMs ?? 3e5,
@@ -42502,8 +42935,8 @@ function scanTagMessages(o) {
42502
42935
  const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
42503
42936
  const refs = runGitChecked(repoDir, ["for-each-ref", "--format=%(refname)", "refs/tags"], gitOpts).split("\n").filter((ref) => ref !== "");
42504
42937
  if (refs.length === 0) return [];
42505
- const ignoreDir = mkdtempSync3(join20(tmpdir2(), "border-gitleaks-ignore-"));
42506
- const ignorePath = join20(ignoreDir, ".gitleaksignore");
42938
+ const ignoreDir = mkdtempSync3(join21(tmpdir2(), "border-gitleaks-ignore-"));
42939
+ const ignorePath = join21(ignoreDir, ".gitleaksignore");
42507
42940
  writeFileSync4(ignorePath, "");
42508
42941
  try {
42509
42942
  const findings = [];
@@ -42523,7 +42956,6 @@ function scanTagMessages(o) {
42523
42956
 
42524
42957
  // src/check.ts
42525
42958
  var TRACKED_BORDER_RULE = "repo-tracks-border-state";
42526
- var RESIDUE_RULE_IDS = new Set(Object.keys(RESIDUE_SEVERITIES));
42527
42959
  var GUARD_PATH_CAP = 50;
42528
42960
  function trackedBorderStateFinding(repoDir, headSha, env2) {
42529
42961
  const listing = runGitChecked(repoDir, ["ls-files", "--", `${BORDER_STATE_DIR}/`], { ...env2 !== void 0 ? { env: env2 } : {} }).split("\n").filter((p) => p !== "");
@@ -42580,7 +43012,7 @@ async function runPipeline(o, ctx, lockWarning) {
42580
43012
  if (!broken.has("gitleaks")) {
42581
43013
  const eng = legOptions(o, sanitizer);
42582
43014
  findings.push(...filterBorderStateFindings(scanGitHistory({ repoDir, refRange: historyRefRange(ctx), target: "git", ...eng }), repoDir));
42583
- findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join21(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
43015
+ findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join22(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
42584
43016
  findings.push(...filterBorderStateFindings(scanTagMessages({ repoDir, target: "git", ...eng }), repoDir));
42585
43017
  }
42586
43018
  if (!broken.has("secretlint")) {
@@ -42605,6 +43037,17 @@ async function runPipeline(o, ctx, lockWarning) {
42605
43037
  findings.push(...stage.findings);
42606
43038
  ledgerArtifacts.push(...stage.artifacts);
42607
43039
  }
43040
+ if ((o.cfg.release?.twin.length ?? 0) > 0) {
43041
+ findings.push(
43042
+ ...await twinCoherenceFindings({
43043
+ pairs: o.cfg.release?.twin ?? [],
43044
+ artifacts: ledgerArtifacts,
43045
+ repoDir,
43046
+ ...envOpt,
43047
+ sanitizer
43048
+ })
43049
+ );
43050
+ }
42608
43051
  findings.push(...await runRegistryProbes({ repoDir, cfg: o.cfg, effectiveTargets: o.effectiveTargets, ...envOpt }));
42609
43052
  if (o.cfg.residue?.enabled === false) {
42610
43053
  for (let i = findings.length - 1; i >= 0; i -= 1) {
@@ -42612,13 +43055,16 @@ async function runPipeline(o, ctx, lockWarning) {
42612
43055
  if (f !== void 0 && RESIDUE_RULE_IDS.has(f.rule)) findings.splice(i, 1);
42613
43056
  }
42614
43057
  }
42615
- const exposure = [...exposureSet(o.cfg, { cwd: repoDir })];
42616
- const allow = applyAllowList(findings, o.cfg.allow, repoDir);
42617
43058
  const rulesHash = await computeCheckRulesHash({
42618
43059
  engineVersions: probe.engineVersions,
42619
43060
  configDigest: o.configDigest,
42620
43061
  ...o.env !== void 0 ? { env: o.env } : {}
42621
43062
  });
43063
+ if (o.cfg.residue?.requireProof === true && ledgerArtifacts.length > 0 && hasBlockingResidueCapability(findings)) {
43064
+ findings.push(...proofFindings({ artifacts: ledgerArtifacts, records: readLedger(repoDir).records, rulesHash }));
43065
+ }
43066
+ const exposure = [...exposureSet(o.cfg, { cwd: repoDir })];
43067
+ const allow = applyAllowList(findings, o.cfg.allow, repoDir);
42622
43068
  const report = {
42623
43069
  schemaVersion: 1,
42624
43070
  key: computeCheckKey({
@@ -42754,16 +43200,16 @@ var NO_TARGETS_PROBE_CFG = {
42754
43200
  // src/llm/bundle.ts
42755
43201
  import { createHash as createHash13 } from "node:crypto";
42756
43202
  import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "node:fs";
42757
- import { dirname as dirname11, join as join23 } from "node:path";
43203
+ import { dirname as dirname11, join as join24 } from "node:path";
42758
43204
 
42759
43205
  // src/llm/diff.ts
42760
- import { spawnSync as spawnSync13 } from "node:child_process";
43206
+ import { spawnSync as spawnSync14 } from "node:child_process";
42761
43207
  import { mkdtempSync as mkdtempSync4, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
42762
- import { dirname as dirname10, join as join22, resolve as resolve13 } from "node:path";
43208
+ import { dirname as dirname10, join as join23, resolve as resolve13 } from "node:path";
42763
43209
  var GIT_DIFF_MAX_BUFFER = 64 * 1024 * 1024;
42764
43210
  function gitOut(repoDir, args, o, allowFail = false) {
42765
43211
  const abs = resolve13(repoDir);
42766
- const r = spawnSync13("git", ["-C", abs, ...args], {
43212
+ const r = spawnSync14("git", ["-C", abs, ...args], {
42767
43213
  encoding: "utf8",
42768
43214
  env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname10(abs) },
42769
43215
  timeout: 12e4,
@@ -42856,18 +43302,18 @@ function collectRawDeltas(repoDir, baseSha, headSha, o = {}) {
42856
43302
  var MASK_CHUNK_CHARS = 2e6;
42857
43303
  function maskViaEngineDetection(repoDir, texts, o = {}) {
42858
43304
  const sanitizer = new TextSanitizer();
42859
- const sandbox = mkdtempSync4(join22(repoDir, BORDER_STATE_DIR, "llm-mask-"));
43305
+ const sandbox = mkdtempSync4(join23(repoDir, BORDER_STATE_DIR, "llm-mask-"));
42860
43306
  try {
42861
43307
  let idx = 0;
42862
43308
  for (const text of texts) {
42863
43309
  if (text === "") continue;
42864
43310
  for (let off = 0; off < text.length; off += MASK_CHUNK_CHARS, idx += 1) {
42865
- writeFileSync5(join22(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
43311
+ writeFileSync5(join23(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
42866
43312
  }
42867
43313
  }
42868
43314
  scanTree({
42869
43315
  dir: sandbox,
42870
- stateDir: join22(repoDir, BORDER_STATE_DIR),
43316
+ stateDir: join23(repoDir, BORDER_STATE_DIR),
42871
43317
  target: "llm-mask",
42872
43318
  sanitizer,
42873
43319
  ...engineEnv(o)
@@ -42925,7 +43371,7 @@ async function runLlmRequestCore(ctx) {
42925
43371
  ctx.stderr("border: llm-request: no check record for the current state \u2014 run 'border check' first (the bundle derives from the recorded ctx)");
42926
43372
  return EXIT_ERROR;
42927
43373
  }
42928
- const absReport = join23(repoDir, record2.reportPath);
43374
+ const absReport = join24(repoDir, record2.reportPath);
42929
43375
  if (!existsSync15(absReport)) {
42930
43376
  ctx.stderr(`border: llm-request: report archive missing (${record2.reportPath} pruned?) \u2014 run 'border check' again`);
42931
43377
  return EXIT_ERROR;
@@ -42984,7 +43430,7 @@ async function runLlmRequestCore(ctx) {
42984
43430
  deterministic: { verdict: report.verdict, counts: report.counts, findings: report.findings },
42985
43431
  promptTemplate: { path: templatePath, sha256: createHash13("sha256").update(readFileSync14(templatePath)).digest("hex") }
42986
43432
  };
42987
- const outPath = join23(dirname11(absReport), "llm-request.json");
43433
+ const outPath = join24(dirname11(absReport), "llm-request.json");
42988
43434
  mkdirSync10(dirname11(outPath), { recursive: true });
42989
43435
  writeFileSync6(outPath, `${JSON.stringify(bundle, null, 2)}
42990
43436
  `, "utf8");
@@ -42996,7 +43442,7 @@ async function runLlmRequestCore(ctx) {
42996
43442
  // src/llm/ingest.ts
42997
43443
  import { createHash as createHash14 } from "node:crypto";
42998
43444
  import { existsSync as existsSync16, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "node:fs";
42999
- import { isAbsolute, join as join24, resolve as resolve14 } from "node:path";
43445
+ import { isAbsolute, join as join25, resolve as resolve14 } from "node:path";
43000
43446
  var RULE_RE = /^[a-z0-9-]+$/;
43001
43447
  var MAX_TEXT = 4e3;
43002
43448
  var ALLOWED_KEYS = /* @__PURE__ */ new Set(["rule", "severity", "target", "path", "line", "commit", "message", "snippet", "engine", "valueDigest"]);
@@ -43015,7 +43461,7 @@ function findLatestBundle(repoDir) {
43015
43461
  return "no llm-request bundle found \u2014 run 'border llm-request' first";
43016
43462
  }
43017
43463
  for (const d of dirs.sort().reverse()) {
43018
- const p = join24(runsDir(repoDir), d, "llm-request.json");
43464
+ const p = join25(runsDir(repoDir), d, "llm-request.json");
43019
43465
  if (!existsSync16(p)) continue;
43020
43466
  let parsed;
43021
43467
  try {
@@ -43028,7 +43474,7 @@ function findLatestBundle(repoDir) {
43028
43474
  if (b.schemaVersion !== 1 || typeof b.key !== "string" || !/^[0-9a-f]{64}$/.test(b.key) || typeof b.head !== "string" || !/^[0-9a-f]{40}$/.test(b.head) || !Array.isArray(b.fileDeltas) || b.deterministic === void 0) {
43029
43475
  return `bundle ${p} fails its own schema \u2014 re-run 'border llm-request'`;
43030
43476
  }
43031
- return { dir: join24(runsDir(repoDir), d), bundle: b };
43477
+ return { dir: join25(runsDir(repoDir), d), bundle: b };
43032
43478
  }
43033
43479
  return "no llm-request bundle found \u2014 run 'border llm-request' first";
43034
43480
  }
@@ -43173,7 +43619,7 @@ async function runLlmRequest(ctx) {
43173
43619
  }
43174
43620
 
43175
43621
  // src/push/git.ts
43176
- import { spawnSync as spawnSync14 } from "node:child_process";
43622
+ import { spawnSync as spawnSync15 } from "node:child_process";
43177
43623
  var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
43178
43624
  function pushArgv(url2, branch, mode) {
43179
43625
  return mode === "dry-run" ? ["push", "--dry-run", url2, branch, "--follow-tags"] : ["push", url2, branch, "--follow-tags"];
@@ -43208,7 +43654,7 @@ function lsRemoteMap(repoDir, remote, env2) {
43208
43654
  return parseLsRemote(text, remote.target);
43209
43655
  }
43210
43656
  function isAncestor(repoDir, remoteSha, localSha, env2) {
43211
- const r = spawnSync14("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
43657
+ const r = spawnSync15("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
43212
43658
  encoding: "utf8",
43213
43659
  env: { ...env2, GIT_CEILING_DIRECTORIES: repoDir }
43214
43660
  });
@@ -43243,7 +43689,7 @@ function pushTimeoutMs(env2) {
43243
43689
  }
43244
43690
  function executePush(i) {
43245
43691
  const timeoutMs = pushTimeoutMs(i.env);
43246
- const r = spawnSync14("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
43692
+ const r = spawnSync15("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
43247
43693
  stdio: "inherit",
43248
43694
  env: i.env,
43249
43695
  timeout: timeoutMs
@@ -43436,41 +43882,18 @@ async function executeYesPush(ctx, loaded) {
43436
43882
  return EXIT_PASS;
43437
43883
  }
43438
43884
 
43439
- // src/scan/scan.ts
43440
- import { mkdtempSync as mkdtempSync5 } from "node:fs";
43885
+ // src/roundtrip/orchestrate.ts
43886
+ import { mkdtempSync as mkdtempSync5, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
43887
+ import { randomBytes } from "node:crypto";
43441
43888
  import { tmpdir as tmpdir3 } from "node:os";
43442
43889
  import { join as join26 } from "node:path";
43443
43890
 
43444
- // src/scan/config.ts
43445
- function buildScanConfigYaml(eco) {
43446
- return [
43447
- "version: 1",
43448
- "targets:",
43449
- " git:",
43450
- " remotes: []",
43451
- ` ${eco}: {}`,
43452
- "rules:",
43453
- " authors:",
43454
- " emails: []",
43455
- " names: []",
43456
- " hosts: []",
43457
- " ips: []",
43458
- " pathPatterns: []",
43459
- "residue:",
43460
- " enabled: true",
43461
- ""
43462
- ].join("\n");
43463
- }
43464
- function synthesizeScanConfig(eco) {
43465
- return parseConfig(buildScanConfigYaml(eco), "<border scan>");
43466
- }
43467
-
43468
43891
  // src/scan/fetch.ts
43469
43892
  import { createHash as createHash15 } from "node:crypto";
43470
- import { basename as basename4 } from "node:path";
43893
+ import { basename as basename5 } from "node:path";
43471
43894
  var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
43472
43895
  var SCAN_REQUEST_TIMEOUT_MS = 6e4;
43473
- var SCAN_USER_AGENT = "border-customs/0.3.2 (+https://github.com/sumteclab/border; border scan)";
43896
+ var SCAN_USER_AGENT = "border-customs/0.4.1 (+https://github.com/sumteclab/border; border scan)";
43474
43897
  var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
43475
43898
  var defaultScanFetcher = async (url2, init) => {
43476
43899
  if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
@@ -43546,7 +43969,7 @@ function rubygemsDownloadUrl(name, version2, base = channelBaseUrl("rubygems"))
43546
43969
  return `${base}/downloads/${name}-${version2}.gem`;
43547
43970
  }
43548
43971
  function safeArtifactName(raw, fallback) {
43549
- const base = basename4(raw.split("?")[0] ?? "").replace(/[^A-Za-z0-9._+-]/g, "_");
43972
+ const base = basename5(raw.split("?")[0] ?? "").replace(/[^A-Za-z0-9._+-]/g, "_");
43550
43973
  return base.length > 0 && base !== "." && base !== ".." ? base : fallback;
43551
43974
  }
43552
43975
  async function fetchArtifact(spec, o) {
@@ -43599,9 +44022,746 @@ function sha256Hex(data) {
43599
44022
  return createHash15("sha256").update(data).digest("hex");
43600
44023
  }
43601
44024
 
44025
+ // src/scan/spec.ts
44026
+ var ECOSYSTEMS = ["npm", "pypi", "crates", "rubygems"];
44027
+ var SCAN_SPEC_USAGE = "usage: border scan <[ecosystem:]name@version> \u2014 e.g. border scan puppeteer@23.11.1 | pypi:requests@2.32.3 | crates:serde@1.0.219 | rubygems:rails@7.1.1";
44028
+ var PLAIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
44029
+ var SCOPED_NAME = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
44030
+ var VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
44031
+ var bad2 = (why) => {
44032
+ throw new UnknownArgError(`${why}; ${SCAN_SPEC_USAGE}`);
44033
+ };
44034
+ function parseScanSpec(raw) {
44035
+ const text = raw.trim();
44036
+ if (text.length === 0) return bad2("empty spec");
44037
+ let ecosystem = "npm";
44038
+ let rest = text;
44039
+ const colon = text.indexOf(":");
44040
+ if (colon >= 0) {
44041
+ const prefix = text.slice(0, colon);
44042
+ const found = ECOSYSTEMS.find((e) => e === prefix);
44043
+ if (found === void 0) {
44044
+ return bad2(`unknown ecosystem '${prefix}' \u2014 expected one of: ${ECOSYSTEMS.join(", ")}`);
44045
+ }
44046
+ ecosystem = found;
44047
+ rest = text.slice(colon + 1);
44048
+ if (rest.length === 0) return bad2("missing package name after the ecosystem prefix");
44049
+ }
44050
+ const scoped = rest.startsWith("@");
44051
+ const at = rest.lastIndexOf("@");
44052
+ if (at <= 0) return bad2(`spec '${raw}' is missing '@version'`);
44053
+ const name = rest.slice(0, at);
44054
+ const version2 = rest.slice(at + 1);
44055
+ if (!(scoped ? SCOPED_NAME : PLAIN_NAME).test(name)) {
44056
+ return bad2(`invalid package name '${name}'`);
44057
+ }
44058
+ if (!VERSION.test(version2)) {
44059
+ return bad2(`invalid version '${version2}' in spec '${raw}'`);
44060
+ }
44061
+ return { ecosystem, name, version: version2 };
44062
+ }
44063
+
44064
+ // src/roundtrip/localArtifact.ts
44065
+ import { createReadStream, existsSync as existsSync17, statSync as statSync2 } from "node:fs";
44066
+ import { createHash as createHash16 } from "node:crypto";
44067
+ import { basename as basename6, resolve as resolve15 } from "node:path";
44068
+ var EXT_TABLE = /* @__PURE__ */ new Map([
44069
+ [".whl", "pypi"],
44070
+ [".tgz", "npm"],
44071
+ [".crate", "crates"],
44072
+ [".gem", "rubygems"]
44073
+ ]);
44074
+ var EXT_LIST = ".whl (pypi), .tgz (npm), .crate (crates), .gem (rubygems)";
44075
+ var CONTAINER_NAME = /* @__PURE__ */ new Map([
44076
+ [".tgz", "/root/p.tgz"],
44077
+ [".crate", "/root/p.crate"],
44078
+ [".gem", "/root/p.gem"]
44079
+ ]);
44080
+ var wheelContainerPath = (base, name, version2) => `/root/${safeArtifactName(base, `${name}-${version2}-py3-none-any.whl`)}`;
44081
+ var bad3 = (why) => {
44082
+ throw new UnknownArgError(`border roundtrip: ${why}`);
44083
+ };
44084
+ function identityFromFilename(base, eco) {
44085
+ if (eco === "pypi") {
44086
+ const parts = base.slice(0, -".whl".length).split("-");
44087
+ const name = parts[0] ?? "";
44088
+ const version2 = parts[1] ?? "";
44089
+ if (parts.length < 5 || !/^[A-Za-z0-9][A-Za-z0-9._]*$/.test(name) || !/^\d[A-Za-z0-9._+!]*$/.test(version2)) return null;
44090
+ return { name, version: version2 };
44091
+ }
44092
+ if (eco === "npm") {
44093
+ const m2 = /^(.+)-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\.tgz$/i.exec(base);
44094
+ if (m2 === null || m2[1] === void 0 || m2[2] === void 0) return null;
44095
+ return { name: m2[1], version: m2[2] };
44096
+ }
44097
+ if (eco === "crates") {
44098
+ const m2 = /^([a-z0-9_][a-z0-9._-]*?)-(\d[A-Za-z0-9._+-]*)\.crate$/i.exec(base);
44099
+ if (m2 === null || m2[1] === void 0 || m2[2] === void 0) return null;
44100
+ return { name: m2[1], version: m2[2] };
44101
+ }
44102
+ const m = /^([A-Za-z0-9][A-Za-z0-9._-]*?)-(\d[A-Za-z0-9._+-]*)\.gem$/i.exec(base);
44103
+ if (m === null || m[1] === void 0 || m[2] === void 0) return null;
44104
+ return { name: m[1], version: m[2] };
44105
+ }
44106
+ var GRAMMAR_HINT = {
44107
+ pypi: "PEP 427 wheel names, e.g. jupyter_core-5.8.1-py3-none-any.whl",
44108
+ npm: "npm pack names, e.g. node-fetch-2.6.1.tgz (scoped packages flatten to scope-pkg-1.0.0.tgz)",
44109
+ crates: "cargo package names, e.g. rt-crate-1.0.0.crate",
44110
+ rubygems: "gem package names, e.g. rt-gem-1.0.0.gem"
44111
+ };
44112
+ function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
44113
+ const abs = resolve15(cwd, raw);
44114
+ if (existsSync17(abs)) {
44115
+ const st = statSync2(abs);
44116
+ if (st.isDirectory()) {
44117
+ bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
44118
+ }
44119
+ if (!st.isFile()) {
44120
+ bad3(`'${raw}' resolves to something that is not a regular file (${abs})`);
44121
+ }
44122
+ const lower = basename6(abs).toLowerCase();
44123
+ const ext = [...EXT_TABLE.keys()].find((e) => lower.endsWith(e)) ?? null;
44124
+ if (ext === null) {
44125
+ const known = [...EXT_TABLE.keys()].map((e) => `\`${e}\``).join(", ");
44126
+ bad3(`'${basename6(abs)}' exists but its extension is not in the closed local-artifact table (${known}) \u2014 no content sniffing is done; supported: ${EXT_LIST}`);
44127
+ }
44128
+ const eco = EXT_TABLE.get(ext);
44129
+ const identity = identityFromFilename(basename6(abs), eco);
44130
+ if (identity === null) {
44131
+ bad3(`cannot derive package identity from '${basename6(abs)}' \u2014 expected ${GRAMMAR_HINT[eco]}`);
44132
+ }
44133
+ return {
44134
+ absPath: abs,
44135
+ ecosystem: eco,
44136
+ name: identity.name,
44137
+ version: identity.version,
44138
+ containerPath: ext === ".whl" ? wheelContainerPath(basename6(abs), identity.name, identity.version) : CONTAINER_NAME.get(ext),
44139
+ source: `local:${abs}`
44140
+ };
44141
+ }
44142
+ const looksLocal = raw.includes("/") || [...EXT_TABLE.keys()].some((e) => raw.toLowerCase().endsWith(e));
44143
+ if (looksLocal && !parsesAsRegistrySpec(raw)) {
44144
+ bad3(`'${raw}' (resolved: ${abs}) is not an existing file \u2014 a path-shaped argument is LOCAL mode and is never fetched; fix the path or pass a registry spec`);
44145
+ }
44146
+ return null;
44147
+ }
44148
+ async function readLocalArtifact(absPath, cap = SCAN_MAX_ARTIFACT_BYTES) {
44149
+ const hash2 = createHash16("sha256");
44150
+ const parts = [];
44151
+ let total = 0;
44152
+ for await (const chunk of createReadStream(absPath)) {
44153
+ const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
44154
+ total += b.byteLength;
44155
+ if (total > cap) {
44156
+ throw new EngineRunError(
44157
+ `border roundtrip: ${absPath} streams past the 200 MiB artifact size cap after ${String(total)} bytes \u2014 refusing to prove a partial read (fail-closed)`,
44158
+ null
44159
+ );
44160
+ }
44161
+ hash2.update(b);
44162
+ parts.push(b);
44163
+ }
44164
+ return { bytes: Buffer.concat(parts), sha256: hash2.digest("hex") };
44165
+ }
44166
+
44167
+ // src/roundtrip/docker.ts
44168
+ var DOCKER_PROBE_TIMEOUT_MS = 15e3;
44169
+ var DOCKER_RUN_TIMEOUT_MS = 12e4;
44170
+ var DOCKER_CP_TIMEOUT_MS = 12e4;
44171
+ var SNAPSHOT_TIMEOUT_MS = 12e4;
44172
+ var PHASE_TIMEOUT_MS = 6e5;
44173
+ var DOCKER_RM_TIMEOUT_MS = 6e4;
44174
+ var CONTAINER_TTL_SEC = 900;
44175
+ var BASE_EXCLUDES = [
44176
+ "/proc",
44177
+ "/sys",
44178
+ "/dev",
44179
+ "/root/.npm",
44180
+ "/tmp/node-compile-cache",
44181
+ "/etc/hostname",
44182
+ "/etc/resolv.conf",
44183
+ "/etc/hosts"
44184
+ ];
44185
+ var PYTHON_EXCLUDES = ["/root/.cache", "/var/log", "/var/lib/apt/lists", "*/__pycache__"];
44186
+ var CARGO_EXCLUDES = ["/root/.cargo", "/usr/local/cargo", "/usr/local/.crates2.json", "/usr/local/.crates.toml"];
44187
+ function snapshotScript(excludes) {
44188
+ const prune = `"(" ${excludes.map((p) => `-path "${p}"`).join(" -o ")} ")" -prune -o`;
44189
+ return [
44190
+ "set -e",
44191
+ 'T=/dev/shm/rt-snap.$$; mkdir -p "$T"',
44192
+ `find / -xdev ${prune} -type f -print0 > "$T/f0"`,
44193
+ // || true: a file can vanish between find passes (package managers churn
44194
+ // temp state); the missing row is consistent noise, not a false residue.
44195
+ `xargs -0 -r sha256sum < "$T/f0" 2>/dev/null > "$T/hashes" || true`,
44196
+ // sha256sum text mode = 64 hex + 2 spaces + path; substr keeps spaced paths whole.
44197
+ `awk '{ printf "F\\t%s\\t%s\\n", substr($0,67), substr($0,1,64) }' "$T/hashes" > "$T/files"`,
44198
+ `find / -xdev ${prune} -type l -printf 'L\\t%p\\t%l\\n' > "$T/links"`,
44199
+ `find / -xdev ${prune} -type d -printf 'D\\t%p\\t-\\n' > "$T/dirs"`,
44200
+ 'LC_ALL=C sort "$T/files" "$T/links" "$T/dirs"',
44201
+ 'rm -rf "$T"'
44202
+ ].join("\n");
44203
+ }
44204
+ function realDockerExec() {
44205
+ return (args, timeoutMs) => {
44206
+ const r = spawnEngine(binaryCandidates("docker", {}), [...args], { timeoutMs });
44207
+ return { status: r.status, stdout: r.stdout, stderr: r.stderr };
44208
+ };
44209
+ }
44210
+
44211
+ // src/roundtrip/calibrate.ts
44212
+ var RT_DEP_RULE = "residue-roundtrip-dep-owned";
44213
+ var PIP_REPORT_PATH = "/dev/shm/rt-report.json";
44214
+ function normalizePipName(name) {
44215
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
44216
+ }
44217
+ function parsePipReport(text) {
44218
+ const die = (why) => {
44219
+ throw new EngineRunError(`roundtrip: pypi closure report unusable (${why}) \u2014 cannot verify (fail-closed)`, null);
44220
+ };
44221
+ let doc;
44222
+ try {
44223
+ doc = JSON.parse(text);
44224
+ } catch {
44225
+ die("not JSON");
44226
+ }
44227
+ if (typeof doc !== "object" || doc === null) die("not an object");
44228
+ const rec = doc;
44229
+ if (String(rec.version) !== "1") die(`report format v${String(rec.version)} is not the v1 surface we validated on pip 25.0.1`);
44230
+ if (!Array.isArray(rec.install)) die("install is not an array");
44231
+ const closure = /* @__PURE__ */ new Map();
44232
+ for (const item of rec.install) {
44233
+ const name = item?.metadata?.name;
44234
+ const version2 = item?.metadata?.version;
44235
+ if (typeof name !== "string" || name.length === 0 || typeof version2 !== "string" || version2.length === 0) {
44236
+ throw new EngineRunError("roundtrip: pypi closure report unusable (an install entry lacks metadata.name/version) \u2014 cannot verify (fail-closed)", null);
44237
+ }
44238
+ closure.set(normalizePipName(name), version2);
44239
+ }
44240
+ if (closure.size === 0) die("empty install list \u2014 a resolvable target must report at least itself");
44241
+ return closure;
44242
+ }
44243
+ function claim2(map2, ambiguous, path4, owner) {
44244
+ if (ambiguous.has(path4)) return;
44245
+ const cur = map2.get(path4);
44246
+ if (cur === void 0) map2.set(path4, owner);
44247
+ else if (cur !== owner) {
44248
+ map2.delete(path4);
44249
+ ambiguous.add(path4);
44250
+ }
44251
+ }
44252
+ function parentOf(path4) {
44253
+ const cut = path4.lastIndexOf("/");
44254
+ return cut <= 0 ? "/" : path4.slice(0, cut);
44255
+ }
44256
+ function buildAttribution(claimText, guard) {
44257
+ const exactOwners = /* @__PURE__ */ new Map();
44258
+ const dirOwners = /* @__PURE__ */ new Map();
44259
+ const ambiguous = /* @__PURE__ */ new Set();
44260
+ const claimants = /* @__PURE__ */ new Map();
44261
+ const normCount = /* @__PURE__ */ new Map();
44262
+ const lines = claimText.split("\n");
44263
+ if (guard !== void 0) {
44264
+ for (const line of lines) {
44265
+ if (!line.startsWith("@ ")) continue;
44266
+ const f = line.slice(2).split(" ");
44267
+ if (f.length !== 6) continue;
44268
+ const [owner, norm, ver, di] = f;
44269
+ normCount.set(norm, (normCount.get(norm) ?? 0) + 1);
44270
+ if (!claimants.has(owner)) claimants.set(owner, { owner, norm, ver, di, root: parentOf(di), dirName: f[4], dirVer: f[5] });
44271
+ }
44272
+ }
44273
+ const legit = (owner) => {
44274
+ if (guard === void 0) return {};
44275
+ const c = claimants.get(owner);
44276
+ if (c === void 0) return void 0;
44277
+ if (c.norm === guard.target) return void 0;
44278
+ if (guard.pins.get(c.norm) !== c.ver) return void 0;
44279
+ if (c.dirName !== c.norm || c.dirVer !== c.ver) return void 0;
44280
+ if ((normCount.get(c.norm) ?? 0) !== 1) return void 0;
44281
+ return c;
44282
+ };
44283
+ for (const line of lines) {
44284
+ if (line.trim().length === 0 || line.startsWith("@ ")) continue;
44285
+ const tab = line.indexOf(" ");
44286
+ if (tab <= 0 || line.indexOf(" ", tab + 1) !== -1) continue;
44287
+ const owner = line.slice(0, tab);
44288
+ const path4 = line.slice(tab + 1);
44289
+ if (!path4.startsWith("/") || path4.length === 1) continue;
44290
+ if (guard !== void 0) {
44291
+ const c = legit(owner);
44292
+ if (c === void 0 || c.root === void 0 || !path4.startsWith(`${c.root}/`)) continue;
44293
+ }
44294
+ claim2(exactOwners, ambiguous, path4, owner);
44295
+ for (let dir = parentOf(path4); dir.length > 1; dir = parentOf(dir)) {
44296
+ claim2(dirOwners, ambiguous, dir, owner);
44297
+ }
44298
+ }
44299
+ return { exactOwners, dirOwners, ambiguous };
44300
+ }
44301
+ function ownerOf(entry, attribution) {
44302
+ const exact = attribution.exactOwners.get(entry.path);
44303
+ if (exact !== void 0) return exact;
44304
+ return entry.kind === "D" ? attribution.dirOwners.get(entry.path) : void 0;
44305
+ }
44306
+ var CLAIM_SCANNER_PY = [
44307
+ "import csv, json, os, re, site, sys",
44308
+ "try:",
44309
+ " closure = json.loads(os.environ['BORDER_CLOSURE'])",
44310
+ " assert isinstance(closure, dict) and closure",
44311
+ "except Exception:",
44312
+ " sys.exit(4)",
44313
+ "def norm(n):",
44314
+ " return re.sub(r'[-_.]+', '-', n).lower()",
44315
+ "def read_meta(di):",
44316
+ " name = ''",
44317
+ " ver = ''",
44318
+ " try:",
44319
+ " f = open(os.path.join(di, 'METADATA'), encoding='utf-8', errors='replace')",
44320
+ " except OSError:",
44321
+ " return None",
44322
+ " with f:",
44323
+ " for raw in f:",
44324
+ " line = raw.rstrip('\\n')",
44325
+ " if line == '':",
44326
+ " break",
44327
+ " if not name and line.startswith('Name:'):",
44328
+ " name = line[5:].strip()",
44329
+ " elif not ver and line.startswith('Version:'):",
44330
+ " ver = line[8:].strip()",
44331
+ " return (name, ver) if name and ver else None",
44332
+ "roots = []",
44333
+ "try:",
44334
+ " roots.extend(site.getsitepackages())",
44335
+ "except Exception:",
44336
+ " pass",
44337
+ "out = []",
44338
+ "seen = set()",
44339
+ "for root in roots:",
44340
+ " if not os.path.isdir(root) or root in seen:",
44341
+ " continue",
44342
+ " seen.add(root)",
44343
+ " for d in sorted(os.listdir(root)):",
44344
+ " if not d.endswith('.dist-info'):",
44345
+ " continue",
44346
+ " di = os.path.join(root, d)",
44347
+ " meta = read_meta(di)",
44348
+ " if meta is None or norm(meta[0]) not in closure:",
44349
+ " continue",
44350
+ " owner = meta[0] + '==' + meta[1]",
44351
+ " core = d[:-10]",
44352
+ " cut = core.rfind('-')",
44353
+ " dn = norm(core[:cut]) if cut > 0 else ''",
44354
+ " dv = core[cut + 1:] if cut > 0 else ''",
44355
+ " out.append('@\\t' + owner + '\\t' + norm(meta[0]) + '\\t' + meta[1] + '\\t' + di + '\\t' + dn + '\\t' + dv)",
44356
+ " out.append(owner + '\\t' + di)",
44357
+ " for base, _dirs, files in os.walk(di):",
44358
+ " for fn in sorted(files):",
44359
+ " out.append(owner + '\\t' + os.path.join(base, fn))",
44360
+ " try:",
44361
+ " rf = open(os.path.join(di, 'RECORD'), newline='', encoding='utf-8', errors='replace')",
44362
+ " with rf:",
44363
+ " for row in csv.reader(rf):",
44364
+ " if not row or not row[0].strip():",
44365
+ " continue",
44366
+ " p = os.path.normpath(os.path.join(root, row[0].strip()))",
44367
+ " if p.startswith(root + os.sep):",
44368
+ " out.append(owner + '\\t' + p)",
44369
+ " except Exception:",
44370
+ " pass",
44371
+ "sys.stdout.write(''.join(x + '\\n' for x in out))"
44372
+ ].join("\n");
44373
+
44374
+ // src/roundtrip/manifest.ts
44375
+ import { createHash as createHash17 } from "node:crypto";
44376
+ var RT_ENGINE = "roundtrip";
44377
+ var RT_RESIDUE_RULE = "roundtrip-residue-file";
44378
+ var RT_MODIFY_RULE = "roundtrip-modified-file";
44379
+ var RT_PERSIST_RULE = "roundtrip-modified-persistence";
44380
+ var KEY_SEP = "";
44381
+ var keyOf = (kind, path4) => `${kind}${KEY_SEP}${path4}`;
44382
+ function parseManifest(text) {
44383
+ const out = /* @__PURE__ */ new Map();
44384
+ for (const line of text.split("\n")) {
44385
+ if (line.length === 0) continue;
44386
+ const parts = line.split(" ");
44387
+ if (parts.length < 3) continue;
44388
+ const kind = parts[0];
44389
+ const path4 = parts[1];
44390
+ const value = parts.slice(2).join(" ");
44391
+ if (kind !== "F" && kind !== "L" && kind !== "D") continue;
44392
+ out.set(keyOf(kind, path4), { kind, path: path4, value });
44393
+ }
44394
+ return out;
44395
+ }
44396
+ function diffManifests(before, after) {
44397
+ const added = [];
44398
+ const removed = [];
44399
+ const modified = [];
44400
+ for (const [key, entry] of after) {
44401
+ const old = before.get(key);
44402
+ if (old === void 0) added.push(entry);
44403
+ else if (old.value !== entry.value) modified.push(entry);
44404
+ }
44405
+ for (const [key, entry] of before) {
44406
+ if (!after.has(key)) removed.push(entry);
44407
+ }
44408
+ return { added, removed, modified };
44409
+ }
44410
+ var PERSISTENCE_PATTERNS = [
44411
+ /(^|\/)\.(bashrc|bash_profile|bash_login|profile|zshrc|zprofile|zshenv|kshrc|cshrc|login\.sh)$/,
44412
+ /^\/etc\/profile\.d\//,
44413
+ /^\/etc\/(bash\.bashrc|profile|environment|zsh\/zshrc|inputrc)$/,
44414
+ /^\/etc\/systemd\//,
44415
+ /(^|\/)\.config\/systemd\//,
44416
+ /(^|\/)systemd\/system\//,
44417
+ /(^|\/)(cron|cron\.d|cron\.[0-9dhmwy]+|spool\/cron)\//,
44418
+ /^\/etc\/crontab$/,
44419
+ /(^|\/)\.config\/fish\//,
44420
+ /^\/(usr\/)?(local\/)?(s?bin)\//,
44421
+ /(^|\/)\.local\/bin\//
44422
+ ];
44423
+ function isPersistenceSurface(path4) {
44424
+ return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
44425
+ }
44426
+ var digestOf = (what) => createHash17("sha256").update(what).digest("hex");
44427
+ function classifyResidue(label, diff, attribution = null) {
44428
+ const findings = [];
44429
+ for (const e of diff.added) {
44430
+ const owner = attribution !== null ? ownerOf(e, attribution) : void 0;
44431
+ if (owner !== void 0) {
44432
+ findings.push({
44433
+ rule: RT_DEP_RULE,
44434
+ severity: "LOW",
44435
+ target: label,
44436
+ path: e.path,
44437
+ engine: RT_ENGINE,
44438
+ message: `ADDED path survives uninstall but is claimed by still-installed dependency '${owner}' (pip-accountable) \u2014 demoted, non-blocking`,
44439
+ valueDigest: digestOf(`ADDED:${e.path}:${e.value}`),
44440
+ snippet: `dep-owned: ${e.path}`
44441
+ });
44442
+ continue;
44443
+ }
44444
+ findings.push({
44445
+ rule: RT_RESIDUE_RULE,
44446
+ severity: "HIGH",
44447
+ target: label,
44448
+ path: e.path,
44449
+ engine: RT_ENGINE,
44450
+ message: `installed file survived uninstall (ADDED in post-uninstall manifest)`,
44451
+ valueDigest: digestOf(`ADDED:${e.path}:${e.value}`),
44452
+ snippet: `residue: ${e.path}`
44453
+ });
44454
+ }
44455
+ for (const e of diff.modified) {
44456
+ const persist = isPersistenceSurface(e.path);
44457
+ findings.push({
44458
+ rule: persist ? RT_PERSIST_RULE : RT_MODIFY_RULE,
44459
+ severity: persist ? "CRITICAL" : "HIGH",
44460
+ target: label,
44461
+ path: e.path,
44462
+ engine: RT_ENGINE,
44463
+ message: persist ? `uninstall mutated a persistence surface (rc/systemd/cron/PATH) \u2014 content differs pre-install vs post-uninstall` : `file content differs pre-install vs post-uninstall (MODIFIED, not reverted by uninstall)`,
44464
+ valueDigest: digestOf(`MODIFIED:${e.path}:${e.value}`),
44465
+ snippet: persist ? `persistence: ${e.path}` : `modified: ${e.path}`
44466
+ });
44467
+ }
44468
+ return findings;
44469
+ }
44470
+
44471
+ // src/roundtrip/orchestrate.ts
44472
+ function containerNameFor(label, rand = randomBytes(3).toString("hex")) {
44473
+ return `rt-${sha256Hex(label).slice(0, 8)}-${rand}`;
44474
+ }
44475
+ var shArgv = (name) => {
44476
+ if (!/^[A-Za-z0-9@._+/-]+$/.test(name)) {
44477
+ throw new EngineRunError(`roundtrip: package name '${name}' rejected for shell composition`, null);
44478
+ }
44479
+ return [name];
44480
+ };
44481
+ var PROFILES = {
44482
+ npm: {
44483
+ image: "node:24-slim",
44484
+ artifactPath: "/root/p.tgz",
44485
+ excludes: BASE_EXCLUDES,
44486
+ install: (p) => ["npm", "install", "-g", "--foreground-scripts", "--no-audit", "--no-fund", p],
44487
+ uninstall: (s) => ["npm", "uninstall", "-g", shArgv(s.name)[0]]
44488
+ },
44489
+ pypi: {
44490
+ image: "python:3.12-slim",
44491
+ artifactPath: "/root/p.tar.gz",
44492
+ excludes: [...BASE_EXCLUDES, ...PYTHON_EXCLUDES],
44493
+ install: (p) => ["pip", "install", "--no-input", "--root-user-action=ignore", p],
44494
+ uninstall: (s) => ["pip", "uninstall", "-y", shArgv(s.name)[0]]
44495
+ },
44496
+ rubygems: {
44497
+ image: "ruby:3.4-slim",
44498
+ artifactPath: "/root/p.gem",
44499
+ excludes: BASE_EXCLUDES,
44500
+ install: (p) => ["gem", "install", "--no-document", p],
44501
+ uninstall: (s) => ["gem", "uninstall", shArgv(s.name)[0], "-x", "--force"]
44502
+ },
44503
+ crates: {
44504
+ image: "rust:1-slim",
44505
+ artifactPath: "/root/p.crate",
44506
+ excludes: [...BASE_EXCLUDES, ...CARGO_EXCLUDES],
44507
+ // W2.1 docker legs, verified on rust:1-slim / cargo 1.98: a bare positional
44508
+ // is parsed as a CRATE@VER spec (a directory 400s on "invalid character `/`"),
44509
+ // so the unpacked staging dir goes through --path; --no-track must NOT be
44510
+ // used because it suppresses the $ROOT/.crates2.json install record, which is
44511
+ // the only thing `cargo uninstall` reads (with it, every uninstall dies on
44512
+ // "package ID specification did not match"); and border's own /opt/<dir>
44513
+ // staging tree is never owned by cargo, so the chain that creates it removes
44514
+ // it — after `cargo install` succeeded, keeping install failure fail-closed.
44515
+ install: (p, s) => {
44516
+ const name = shArgv(s.name)[0];
44517
+ const dir = `${name}-${s.version}`;
44518
+ return ["sh", "-c", `tar xf ${p} -C /opt && cargo install --root /usr/local --path "/opt/${dir}" && rm -rf "/opt/${dir}"`];
44519
+ },
44520
+ uninstall: (s) => ["cargo", "uninstall", "--root", "/usr/local", shArgv(s.name)[0]]
44521
+ }
44522
+ };
44523
+ function profileFor(eco) {
44524
+ return PROFILES[eco];
44525
+ }
44526
+ function execChecked(exec, args, timeoutMs, what) {
44527
+ const r = exec(args, timeoutMs);
44528
+ if (r.status !== 0) {
44529
+ const tail3 = r.stderr.replace(/\s+/g, " ").slice(-240);
44530
+ throw new EngineRunError(`roundtrip: ${what} failed (exit ${String(r.status)})${tail3 ? `: ${tail3}` : ""} \u2014 cannot verify (fail-closed)`, r.status);
44531
+ }
44532
+ return r;
44533
+ }
44534
+ function resolvePipClosure(exec, profile, hostPath, label, spec) {
44535
+ const ctr = containerNameFor(`${label}#closure`);
44536
+ try {
44537
+ execChecked(exec, ["run", "-d", "--name", ctr, profile.image, "sleep", String(CONTAINER_TTL_SEC)], DOCKER_RUN_TIMEOUT_MS, "closure resolver start");
44538
+ execChecked(exec, ["cp", hostPath, `${ctr}:${profile.artifactPath}`], DOCKER_CP_TIMEOUT_MS, "resolver artifact copy");
44539
+ execChecked(
44540
+ exec,
44541
+ ["exec", ctr, "pip", "install", "--dry-run", "--report", PIP_REPORT_PATH, "--no-input", "--root-user-action=ignore", profile.artifactPath],
44542
+ PHASE_TIMEOUT_MS,
44543
+ "pypi closure dry-run"
44544
+ );
44545
+ const rep = execChecked(exec, ["exec", ctr, "cat", PIP_REPORT_PATH], SNAPSHOT_TIMEOUT_MS, "pypi closure report read");
44546
+ const closure = parsePipReport(rep.stdout);
44547
+ if (!closure.has(normalizePipName(spec.name))) {
44548
+ throw new EngineRunError(`roundtrip: pip closure report does not list the target ${spec.name} \u2014 cannot verify (fail-closed)`, null);
44549
+ }
44550
+ return closure;
44551
+ } finally {
44552
+ try {
44553
+ exec(["rm", "-f", ctr], DOCKER_RM_TIMEOUT_MS);
44554
+ } catch {
44555
+ }
44556
+ }
44557
+ }
44558
+ function scanDepClaims(exec, container, closure, target, note) {
44559
+ const guard = { pins: closure, target };
44560
+ const argv = ["exec", "-e", `BORDER_CLOSURE=${JSON.stringify(Object.fromEntries(closure))}`, container, "python3", "-c", CLAIM_SCANNER_PY];
44561
+ try {
44562
+ const r = exec(argv, SNAPSHOT_TIMEOUT_MS);
44563
+ if (r.status !== 0) {
44564
+ note(`roundtrip: dep-attribution scan failed (exit ${String(r.status)}) \u2014 every row keeps its blocking grade (over-report tolerated)`);
44565
+ return buildAttribution("", guard);
44566
+ }
44567
+ return buildAttribution(r.stdout, guard);
44568
+ } catch {
44569
+ note("roundtrip: dep-attribution scan could not run \u2014 every row keeps its blocking grade (over-report tolerated)");
44570
+ return buildAttribution("", guard);
44571
+ }
44572
+ }
44573
+ function runPipeline2(exec, profile, spec, label, hostPath, container, closure, note) {
44574
+ execChecked(exec, ["run", "-d", "--name", container, profile.image, "sleep", String(CONTAINER_TTL_SEC)], DOCKER_RUN_TIMEOUT_MS, "container start");
44575
+ try {
44576
+ execChecked(exec, ["cp", hostPath, `${container}:${profile.artifactPath}`], DOCKER_CP_TIMEOUT_MS, "artifact copy");
44577
+ const m1 = parseManifest(takeSnapshot(exec, container, profile.excludes));
44578
+ execChecked(exec, ["exec", container, ...profile.install(profile.artifactPath, spec)], PHASE_TIMEOUT_MS, `install ${label}`);
44579
+ const m2 = parseManifest(takeSnapshot(exec, container, profile.excludes));
44580
+ const installDiff = diffManifests(m1, m2);
44581
+ const installDelta = installDiff.added.length + installDiff.removed.length + installDiff.modified.length;
44582
+ if (installDelta === 0) {
44583
+ throw new EngineRunError(
44584
+ `roundtrip: install of ${label} produced an empty filesystem delta \u2014 cannot verify it ran, refusing to grade a no-op`,
44585
+ null
44586
+ );
44587
+ }
44588
+ execChecked(exec, ["exec", container, ...profile.uninstall(spec)], PHASE_TIMEOUT_MS, `uninstall ${label}`);
44589
+ const m3 = parseManifest(takeSnapshot(exec, container, profile.excludes));
44590
+ const attribution = closure !== null ? scanDepClaims(exec, container, closure, normalizePipName(spec.name), note) : null;
44591
+ return {
44592
+ findings: classifyResidue(label, diffManifests(m1, m3), attribution),
44593
+ installDelta,
44594
+ baselineDigest: sha256Hex([...m1.keys()].join("\n"))
44595
+ };
44596
+ } finally {
44597
+ try {
44598
+ exec(["rm", "-f", container], DOCKER_RM_TIMEOUT_MS);
44599
+ } catch {
44600
+ }
44601
+ }
44602
+ }
44603
+ function takeSnapshot(exec, container, excludes) {
44604
+ return execChecked(
44605
+ exec,
44606
+ ["exec", container, "sh", "-c", snapshotScript(excludes)],
44607
+ SNAPSHOT_TIMEOUT_MS,
44608
+ "manifest snapshot"
44609
+ ).stdout;
44610
+ }
44611
+ async function runRoundtripCore(ctx, deps = {}) {
44612
+ const raw = ctx.positionals[0];
44613
+ if (ctx.positionals.length !== 1 || raw === void 0) {
44614
+ throw new UnknownArgError(
44615
+ `border roundtrip expects exactly one argument \u2014 a registry spec or a local artifact file (got ${String(ctx.positionals.length)}); ${SCAN_SPEC_USAGE}`
44616
+ );
44617
+ }
44618
+ const local = classifyLocalInput(raw, ctx.cwd, parsesRegistrySpec);
44619
+ const spec = local !== null ? { ecosystem: local.ecosystem, name: local.name, version: local.version } : parseScanSpec(raw);
44620
+ const laneProfile = profileFor(spec.ecosystem);
44621
+ const profile = local !== null ? { ...laneProfile, artifactPath: local.containerPath } : laneProfile;
44622
+ const label = `${spec.ecosystem}:${spec.name}@${spec.version}`;
44623
+ const exec = deps.exec ?? realDockerExec();
44624
+ if (!dockerAvailableGuard(exec)) {
44625
+ ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
44626
+ return EXIT_ERROR;
44627
+ }
44628
+ const baseDir = mkdtempSync5(join26(tmpdir3(), "border-roundtrip-"));
44629
+ try {
44630
+ const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
44631
+ const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
44632
+ const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex(bytes);
44633
+ const hostPath = join26(baseDir, "artifact");
44634
+ writeFileSync7(hostPath, bytes);
44635
+ const container = containerNameFor(label);
44636
+ const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
44637
+ const { findings, installDelta, baselineDigest } = runPipeline2(exec, profile, spec, label, hostPath, container, closure, (msg) => ctx.stderr(msg));
44638
+ const verdict = computeVerdict(findings);
44639
+ const report = {
44640
+ schemaVersion: 1,
44641
+ key: sha256Hex(`roundtrip:${label}:${baselineDigest}:${verdict}`),
44642
+ head: baselineDigest.slice(0, 40),
44643
+ dirty: false,
44644
+ exposureSet: [label],
44645
+ refSet: [],
44646
+ rulesHash: sha256Hex(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
44647
+ verdict,
44648
+ counts: countFindings(findings),
44649
+ findings,
44650
+ // W2.4(b) provenance honesty: a local run proves ITS OWN bytes. The
44651
+ // fields are absent (byte-identical JSON) on the registry lane and
44652
+ // never phrase the artifact as registry-verified.
44653
+ ...local !== null ? { source: local.source, artifactSha256 } : {},
44654
+ ts: (/* @__PURE__ */ new Date()).toISOString()
44655
+ };
44656
+ const sanitizer = new TextSanitizer();
44657
+ const line = (text) => sanitizer.sanitize(text).replace(/\s+/g, " ").trim();
44658
+ if (ctx.flags.json) {
44659
+ ctx.stdout(renderReportJson(report));
44660
+ } else {
44661
+ if (local !== null) {
44662
+ ctx.stdout(line(`roundtrip: source ${local.source} artifact sha256 ${artifactSha256}`));
44663
+ }
44664
+ for (const f of report.findings) {
44665
+ ctx.stdout(line(` ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.target} ${f.message}`));
44666
+ }
44667
+ const depOwned = findings.reduce((n, f) => n + (f.rule === RT_DEP_RULE ? 1 : 0), 0);
44668
+ if (depOwned > 0) {
44669
+ ctx.stdout(line(`roundtrip: ${String(depOwned)} row(s) attributed to still-installed pip dependencies \u2014 demoted to ${RT_DEP_RULE} (non-blocking, informational)`));
44670
+ }
44671
+ ctx.stdout(line(`roundtrip: ${String(report.findings.length)} residue row(s); install-delta ${String(installDelta)} files`));
44672
+ }
44673
+ if (ctx.flags.record !== false) {
44674
+ await recordRoundtripProof(ctx, artifactSha256, report.findings.length, report.findings.length === 0 ? "clean" : "residue");
44675
+ }
44676
+ return exitCodeFromVerdict(verdict);
44677
+ } finally {
44678
+ rmSync12(baseDir, { recursive: true, force: true });
44679
+ }
44680
+ }
44681
+ function parsesRegistrySpec(raw) {
44682
+ try {
44683
+ parseScanSpec(raw);
44684
+ return true;
44685
+ } catch {
44686
+ return false;
44687
+ }
44688
+ }
44689
+ async function recordRoundtripProof(ctx, artifactSha256, rows, verdict) {
44690
+ const notice = (msg) => ctx.stderr(`roundtrip: ${msg}`);
44691
+ try {
44692
+ const env2 = ctx.env;
44693
+ const load = loadConfig({
44694
+ cwd: ctx.cwd,
44695
+ ...ctx.flags.config !== void 0 ? { configPath: ctx.flags.config } : {},
44696
+ ...env2 !== void 0 ? { env: env2 } : {}
44697
+ });
44698
+ let effective = null;
44699
+ if (load.kind === "loaded") effective = load;
44700
+ else if (load.explicit !== void 0) effective = { kind: "loaded", config: load.explicit.config, warnings: [], source: load.explicit.source };
44701
+ if (effective === null) {
44702
+ notice("no border.yaml here \u2014 proof NOT recorded");
44703
+ return;
44704
+ }
44705
+ const repoDir = resolveRepoDir(ctx.cwd, { ...env2 !== void 0 ? { env: env2 } : {} });
44706
+ const probe = await probeEngines(effective.config, { ...env2 !== void 0 ? { env: env2 } : {} });
44707
+ if (probe.degraded) {
44708
+ notice("engine probe degraded \u2014 cannot mint the rulesHash this proof must match; proof NOT recorded");
44709
+ return;
44710
+ }
44711
+ const rulesHash = await computeCheckRulesHash({
44712
+ engineVersions: probe.engineVersions,
44713
+ configDigest: computeConfigDigest(effective),
44714
+ ...env2 !== void 0 ? { env: env2 } : {}
44715
+ });
44716
+ appendRecord(repoDir, buildRoundtripRecord({ artifactSha256, verdict, rulesHash, rows }));
44717
+ notice(`proof recorded for ${artifactSha256.slice(0, 12)}\u2026 (verdict ${verdict}, ${String(rows)} residue row(s))`);
44718
+ } catch (err) {
44719
+ notice(`proof NOT recorded (${err instanceof Error ? err.message : String(err)}) \u2014 'border check' with residue.requireProof will treat this artifact as unproven`);
44720
+ }
44721
+ }
44722
+ function dockerAvailableGuard(exec) {
44723
+ try {
44724
+ return exec(["--version"], DOCKER_PROBE_TIMEOUT_MS).status === 0;
44725
+ } catch {
44726
+ return false;
44727
+ }
44728
+ }
44729
+
44730
+ // src/commands/roundtrip.ts
44731
+ var runRoundtrip = (ctx) => runRoundtripCore(ctx);
44732
+
44733
+ // src/scan/scan.ts
44734
+ import { mkdtempSync as mkdtempSync6 } from "node:fs";
44735
+ import { tmpdir as tmpdir4 } from "node:os";
44736
+ import { join as join28 } from "node:path";
44737
+
44738
+ // src/scan/config.ts
44739
+ function buildScanConfigYaml(eco) {
44740
+ return [
44741
+ "version: 1",
44742
+ "targets:",
44743
+ " git:",
44744
+ " remotes: []",
44745
+ ` ${eco}: {}`,
44746
+ "rules:",
44747
+ " authors:",
44748
+ " emails: []",
44749
+ " names: []",
44750
+ " hosts: []",
44751
+ " ips: []",
44752
+ " pathPatterns: []",
44753
+ "residue:",
44754
+ " enabled: true",
44755
+ ""
44756
+ ].join("\n");
44757
+ }
44758
+ function synthesizeScanConfig(eco) {
44759
+ return parseConfig(buildScanConfigYaml(eco), "<border scan>");
44760
+ }
44761
+
43602
44762
  // src/scan/materialize.ts
43603
- import { existsSync as existsSync17, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
43604
- import { join as join25 } from "node:path";
44763
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync16, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
44764
+ import { join as join27 } from "node:path";
43605
44765
  var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
43606
44766
  var fail2 = (why) => {
43607
44767
  throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
@@ -43618,12 +44778,12 @@ function unwrapSingleDir(dir) {
43618
44778
  const entries = readdirSync10(dir, { withFileTypes: true });
43619
44779
  const dirs = entries.filter((e) => e.isDirectory());
43620
44780
  if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
43621
- return join25(dir, dirs[0].name);
44781
+ return join27(dir, dirs[0].name);
43622
44782
  }
43623
44783
  return dir;
43624
44784
  }
43625
44785
  function requireManifest(repoDir, ecosystem, candidates) {
43626
- if (!candidates.some((rel) => existsSync17(join25(repoDir, rel)))) {
44786
+ if (!candidates.some((rel) => existsSync18(join27(repoDir, rel)))) {
43627
44787
  fail2(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
43628
44788
  }
43629
44789
  }
@@ -43633,18 +44793,18 @@ function materializePackage(o) {
43633
44793
  `);
43634
44794
  });
43635
44795
  const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
43636
- const archive = join25(o.baseDir, safe);
43637
- writeFileSync7(archive, o.bytes);
43638
- const extractDir = join25(o.baseDir, "extract");
44796
+ const archive = join27(o.baseDir, safe);
44797
+ writeFileSync8(archive, o.bytes);
44798
+ const extractDir = join27(o.baseDir, "extract");
43639
44799
  mkdirSync11(extractDir, { recursive: true });
43640
44800
  extractFlat(archive, extractDir);
43641
44801
  let repoDir = unwrapSingleDir(extractDir);
43642
44802
  if (o.ecosystem === "rubygems") {
43643
44803
  const outer = repoDir;
43644
44804
  const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
43645
- const inner = dataCandidates[0] ?? join25(outer, "data.tar.gz");
43646
- if (!existsSync17(inner)) fail2(".gem archive contains no data.tar.gz");
43647
- const gemTree = join25(o.baseDir, "gem-tree");
44805
+ const inner = dataCandidates[0] ?? join27(outer, "data.tar.gz");
44806
+ if (!existsSync18(inner)) fail2(".gem archive contains no data.tar.gz");
44807
+ const gemTree = join27(o.baseDir, "gem-tree");
43648
44808
  mkdirSync11(gemTree, { recursive: true });
43649
44809
  extractFlat(inner, gemTree);
43650
44810
  repoDir = unwrapSingleDir(gemTree);
@@ -43655,10 +44815,10 @@ function materializePackage(o) {
43655
44815
  } else {
43656
44816
  requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
43657
44817
  for (const file2 of CRATES_ENVELOPE_FILES) {
43658
- const path4 = join25(repoDir, file2);
43659
- if (!existsSync17(path4)) continue;
44818
+ const path4 = join27(repoDir, file2);
44819
+ if (!existsSync18(path4)) continue;
43660
44820
  const original = readFileSync16(path4);
43661
- rmSync12(path4, { force: true });
44821
+ rmSync13(path4, { force: true });
43662
44822
  note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex(original)}`);
43663
44823
  }
43664
44824
  }
@@ -43670,45 +44830,6 @@ function materializePackage(o) {
43670
44830
  return repoDir;
43671
44831
  }
43672
44832
 
43673
- // src/scan/spec.ts
43674
- var ECOSYSTEMS = ["npm", "pypi", "crates", "rubygems"];
43675
- var SCAN_SPEC_USAGE = "usage: border scan <[ecosystem:]name@version> \u2014 e.g. border scan puppeteer@23.11.1 | pypi:requests@2.32.3 | crates:serde@1.0.219 | rubygems:rails@7.1.1";
43676
- var PLAIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
43677
- var SCOPED_NAME = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
43678
- var VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
43679
- var bad2 = (why) => {
43680
- throw new UnknownArgError(`${why}; ${SCAN_SPEC_USAGE}`);
43681
- };
43682
- function parseScanSpec(raw) {
43683
- const text = raw.trim();
43684
- if (text.length === 0) return bad2("empty spec");
43685
- let ecosystem = "npm";
43686
- let rest = text;
43687
- const colon = text.indexOf(":");
43688
- if (colon >= 0) {
43689
- const prefix = text.slice(0, colon);
43690
- const found = ECOSYSTEMS.find((e) => e === prefix);
43691
- if (found === void 0) {
43692
- return bad2(`unknown ecosystem '${prefix}' \u2014 expected one of: ${ECOSYSTEMS.join(", ")}`);
43693
- }
43694
- ecosystem = found;
43695
- rest = text.slice(colon + 1);
43696
- if (rest.length === 0) return bad2("missing package name after the ecosystem prefix");
43697
- }
43698
- const scoped = rest.startsWith("@");
43699
- const at = rest.lastIndexOf("@");
43700
- if (at <= 0) return bad2(`spec '${raw}' is missing '@version'`);
43701
- const name = rest.slice(0, at);
43702
- const version2 = rest.slice(at + 1);
43703
- if (!(scoped ? SCOPED_NAME : PLAIN_NAME).test(name)) {
43704
- return bad2(`invalid package name '${name}'`);
43705
- }
43706
- if (!VERSION.test(version2)) {
43707
- return bad2(`invalid version '${version2}' in spec '${raw}'`);
43708
- }
43709
- return { ecosystem, name, version: version2 };
43710
- }
43711
-
43712
44833
  // src/scan/scan.ts
43713
44834
  async function runScanCore(ctx, deps = {}) {
43714
44835
  const raw = ctx.positionals[0];
@@ -43724,7 +44845,7 @@ async function runScanCore(ctx, deps = {}) {
43724
44845
  }
43725
44846
  const cfg = synthesizeScanConfig(spec.ecosystem);
43726
44847
  const fetcher = deps.fetcher ?? defaultScanFetcher;
43727
- const baseDir = mkdtempSync5(join26(tmpdir3(), "border-scan-"));
44848
+ const baseDir = mkdtempSync6(join28(tmpdir4(), "border-scan-"));
43728
44849
  try {
43729
44850
  const { bytes, filename } = await fetchArtifact(spec, { fetcher });
43730
44851
  const env2 = { ...ctx.env };
@@ -43848,11 +44969,12 @@ var handlers = {
43848
44969
  status: runStatus,
43849
44970
  "llm-request": runLlmRequest,
43850
44971
  "llm-ingest": runLlmIngest,
43851
- scan: runScan2
44972
+ scan: runScan2,
44973
+ roundtrip: runRoundtrip
43852
44974
  };
43853
44975
 
43854
44976
  // src/cli/types.ts
43855
- var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan"];
44977
+ var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip"];
43856
44978
 
43857
44979
  // src/cli.ts
43858
44980
  var VALID_TARGETS = targetIds();
@@ -43869,6 +44991,7 @@ function usage() {
43869
44991
  " llm-request emit the provenance request for LLM-authored commits",
43870
44992
  " llm-ingest record provenance captured by the harness",
43871
44993
  " scan inspect a third-party package ([ecosystem:]name@version) for residue",
44994
+ " roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
43872
44995
  "",
43873
44996
  "flags:",
43874
44997
  " --config <path> config file (default: ./border.yaml, then git-remote fallback)",
@@ -43878,11 +45001,12 @@ function usage() {
43878
45001
  " --require-engine <list> comma-separated engines that must be healthy (else exit 2)",
43879
45002
  " --llm include the optional LLM review layer",
43880
45003
  " --json machine-readable report on stdout",
45004
+ " --record / --no-record `border roundtrip` records a ledger proof by default; --no-record opts out",
43881
45005
  " --help, -h this table",
43882
45006
  "",
43883
45007
  "exit codes: 0 pass (MEDIUM/INFO/LOW allowed) | 1 gate-blocked (CRITICAL/HIGH) or partial push | 2 config/tool error",
43884
45008
  "",
43885
- "subcommands: check, push, status, llm-request, llm-ingest, scan"
45009
+ "subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip"
43886
45010
  ].join("\n");
43887
45011
  }
43888
45012
  function isSubcommand(value) {
@@ -43913,10 +45037,15 @@ function parseFlags(argv) {
43913
45037
  "require-engine": { type: "string" },
43914
45038
  llm: { type: "boolean" },
43915
45039
  json: { type: "boolean" },
45040
+ record: { type: "boolean" },
45041
+ "no-record": { type: "boolean" },
43916
45042
  help: { type: "boolean" }
43917
45043
  }
43918
45044
  });
43919
45045
  const { config: config2, targets, force, yes, llm, json: json2 } = values;
45046
+ if (values.record === true && values["no-record"] === true) {
45047
+ throw new UnknownArgError("--record and --no-record contradict each other");
45048
+ }
43920
45049
  const requireEngine = values["require-engine"];
43921
45050
  const flags = {
43922
45051
  ...config2 !== void 0 ? { config: config2 } : {},
@@ -43925,7 +45054,8 @@ function parseFlags(argv) {
43925
45054
  yes: yes === true,
43926
45055
  ...requireEngine !== void 0 ? { requireEngine: splitList(requireEngine, { flag: "--require-engine", item: "engine" }) } : {},
43927
45056
  llm: llm === true,
43928
- json: json2 === true
45057
+ json: json2 === true,
45058
+ ...values["no-record"] === true ? { record: false } : values.record === true ? { record: true } : {}
43929
45059
  };
43930
45060
  return { flags, positionals, help: values.help === true };
43931
45061
  } catch (err) {