@intentius/behold 0.15.0 → 0.17.0

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/cli.js CHANGED
@@ -716,11 +716,11 @@ async function createK8sClient(options = {}) {
716
716
  }
717
717
  return void 0;
718
718
  }
719
- async function resolve9(selector, signal) {
719
+ async function resolve12(selector, signal) {
720
720
  return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
721
721
  }
722
722
  async function resolveOrThrow(selector, signal) {
723
- const info = await resolve9(selector, signal);
723
+ const info = await resolve12(selector, signal);
724
724
  if (!info) throw new UnknownResourceError(selectorText(selector));
725
725
  return info;
726
726
  }
@@ -732,7 +732,7 @@ async function createK8sClient(options = {}) {
732
732
  return parts.join("/");
733
733
  }
734
734
  async function pathFor(ref, signal) {
735
- const info = await resolve9({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
735
+ const info = await resolve12({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
736
736
  if (!info) return void 0;
737
737
  return objectPath(info, ref.name, ref.namespace);
738
738
  }
@@ -872,7 +872,7 @@ async function createK8sClient(options = {}) {
872
872
  return {
873
873
  provenance,
874
874
  defaultNamespace,
875
- resolve: resolve9,
875
+ resolve: resolve12,
876
876
  read,
877
877
  pathFor,
878
878
  readIfPresent,
@@ -1065,8 +1065,8 @@ var init_src = __esm({
1065
1065
  });
1066
1066
 
1067
1067
  // src/cli.ts
1068
- import { resolve as resolve8, dirname as dirname7, join as join20 } from "node:path";
1069
- import { realpathSync, existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync2 } from "node:fs";
1068
+ import { resolve as resolve11, dirname as dirname7, join as join22 } from "node:path";
1069
+ import { realpathSync, existsSync as existsSync15, readFileSync as readFileSync17, rmSync as rmSync3 } from "node:fs";
1070
1070
  import { fileURLToPath as fileURLToPath5 } from "node:url";
1071
1071
  import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
1072
1072
 
@@ -1147,8 +1147,8 @@ import { serve } from "@hono/node-server";
1147
1147
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1148
1148
  import { execFile as execFile3 } from "node:child_process";
1149
1149
  import { promisify as promisify3 } from "node:util";
1150
- import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
1151
- import { basename, dirname as dirname5, join as join18, relative as relative4, resolve as resolve6, sep as sep4 } from "node:path";
1150
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
1151
+ import { basename, dirname as dirname5, join as join20, relative as relative5, resolve as resolve9, sep as sep6 } from "node:path";
1152
1152
 
1153
1153
  // src/recents.ts
1154
1154
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -1182,10 +1182,10 @@ function addRecent(dir, now = () => (/* @__PURE__ */ new Date()).toISOString())
1182
1182
 
1183
1183
  // src/chant.ts
1184
1184
  import { spawn } from "node:child_process";
1185
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1185
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
1186
1186
  import { createRequire } from "node:module";
1187
1187
  import { fileURLToPath } from "node:url";
1188
- import { dirname as dirname2, join as join3, resolve } from "node:path";
1188
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
1189
1189
 
1190
1190
  // src/project.ts
1191
1191
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
@@ -1305,6 +1305,30 @@ function readTiers(cfg) {
1305
1305
  if (!values.length) return void 0;
1306
1306
  return { envVar: tiers.envVar, values };
1307
1307
  }
1308
+ var EXECUTOR_FORGES = ["github", "gitlab", "forgejo"];
1309
+ function readExecutor(cfg) {
1310
+ const raw = cfg?.executor;
1311
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
1312
+ const out = {};
1313
+ for (const [env, value] of Object.entries(raw)) {
1314
+ if (!env) continue;
1315
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1316
+ out[env] = { invalid: `executor.${env} must be {forge, workflow}, got ${JSON.stringify(value)}` };
1317
+ continue;
1318
+ }
1319
+ const v = value;
1320
+ if (typeof v.forge !== "string" || !EXECUTOR_FORGES.includes(v.forge)) {
1321
+ out[env] = { invalid: `executor.${env}.forge must be one of ${EXECUTOR_FORGES.join(", ")}, got ${JSON.stringify(v.forge)}` };
1322
+ continue;
1323
+ }
1324
+ if (typeof v.workflow !== "string" || !/^[^/\\]+\.ya?ml$/.test(v.workflow)) {
1325
+ out[env] = { invalid: `executor.${env}.workflow must name a file in .github/workflows (e.g. deploy-${env}.yml), got ${JSON.stringify(v.workflow)}` };
1326
+ continue;
1327
+ }
1328
+ out[env] = { forge: v.forge, workflow: v.workflow };
1329
+ }
1330
+ return Object.keys(out).length ? out : void 0;
1331
+ }
1308
1332
  function loadBeholdConfig(projectDir) {
1309
1333
  const path = join2(projectDir, BEHOLD_CONFIG_NAME);
1310
1334
  if (!existsSync2(path)) return {};
@@ -1312,7 +1336,8 @@ function loadBeholdConfig(projectDir) {
1312
1336
  const raw = JSON.parse(readFileSync2(path, "utf8"));
1313
1337
  const tiers = readTiers(raw);
1314
1338
  const members = Array.isArray(raw.members) ? raw.members.filter((m) => typeof m === "string" && !!m) : [];
1315
- return { ...tiers ? { tiers } : {}, ...members.length ? { members } : {} };
1339
+ const executor = readExecutor(raw);
1340
+ return { ...tiers ? { tiers } : {}, ...members.length ? { members } : {}, ...executor ? { executor } : {} };
1316
1341
  } catch {
1317
1342
  return {};
1318
1343
  }
@@ -1664,6 +1689,367 @@ function unescapeDoubleQuoted(body) {
1664
1689
  return out;
1665
1690
  }
1666
1691
 
1692
+ // src/carve-manifest.ts
1693
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
1694
+ import { dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
1695
+ var CARVE_MANIFEST_SUFFIX = ".carve.json";
1696
+ var APPLY_IS_HUMAN = "`chant carve apply` graduates ownership \u2014 it resolves the marker that makes chant the owner of a live resource. behold shows what the manifest records and echoes the command; it has no apply endpoint and no apply button, by design. Read the graduation plan, then run it yourself.";
1697
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1698
+ var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
1699
+ function parseCarveManifest(value) {
1700
+ if (!isRecord(value)) return null;
1701
+ if (value.version !== 1 && value.version !== 2) return null;
1702
+ if (typeof value.target !== "string" || !value.target) return null;
1703
+ return value;
1704
+ }
1705
+ function readCarveManifest(path, readFile = (p) => readFileSync3(p, "utf8")) {
1706
+ let text;
1707
+ try {
1708
+ text = readFile(path);
1709
+ } catch {
1710
+ return null;
1711
+ }
1712
+ try {
1713
+ return parseCarveManifest(JSON.parse(text));
1714
+ } catch {
1715
+ return null;
1716
+ }
1717
+ }
1718
+ function carveStageOf(m) {
1719
+ if (m.apply) return "applied";
1720
+ if (m.bridge) return "bridged";
1721
+ return "emitted";
1722
+ }
1723
+ function carveWordFor(stage) {
1724
+ switch (stage) {
1725
+ case "applied":
1726
+ return "carved \u2192 chant";
1727
+ case "bridged":
1728
+ return "bridged \u2014 apply is yours";
1729
+ default:
1730
+ return "emitted \u2014 not bridged";
1731
+ }
1732
+ }
1733
+ function carveToneFor(stage) {
1734
+ return stage === "applied" ? "good" : "accent";
1735
+ }
1736
+ function carveStageNote(state) {
1737
+ switch (state.stage) {
1738
+ case "applied":
1739
+ return `graduated \u2014 \`carve apply\` recorded the ownership marker` + (state.ownership?.stack ? ` (stack ${state.ownership.stack}${state.ownership.env ? `, env ${state.ownership.env}` : ""})` : "") + ". chant owns it.";
1740
+ case "bridged":
1741
+ return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
1742
+ default:
1743
+ return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
1744
+ }
1745
+ }
1746
+ function carveStateOf(m, path) {
1747
+ const stage = carveStageOf(m);
1748
+ const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
1749
+ return {
1750
+ target: m.target,
1751
+ ...m.tfType ? { tfType: m.tfType } : {},
1752
+ stage,
1753
+ graduated: stage === "applied",
1754
+ // The last stamped step wins — that is the age of the state, not the age
1755
+ // of the carve.
1756
+ ...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
1757
+ files: strings(m.emit?.files),
1758
+ sourceFiles: strings(m.emit?.files).map((f) => isAbsolute(f) ? f : resolve(dirname2(path), f)),
1759
+ excised: strings(m.bridge?.excised),
1760
+ deferredInputs: deferred,
1761
+ ...m.apply ? {
1762
+ ownership: {
1763
+ ...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
1764
+ ...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
1765
+ ...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
1766
+ }
1767
+ } : {},
1768
+ ...m.from ? { from: m.from } : {},
1769
+ outDir: dirname2(path),
1770
+ path
1771
+ };
1772
+ }
1773
+ var nodeCarveIo = {
1774
+ readdir: (dir) => readdirSync(dir),
1775
+ isDirectory: (p) => {
1776
+ try {
1777
+ return statSync(p).isDirectory();
1778
+ } catch {
1779
+ return false;
1780
+ }
1781
+ },
1782
+ readFile: (p) => readFileSync3(p, "utf8")
1783
+ };
1784
+ var MANIFEST_SCAN_DEPTH = 2;
1785
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
1786
+ function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
1787
+ const out = [];
1788
+ const walk = (d, level) => {
1789
+ let entries;
1790
+ try {
1791
+ entries = io.readdir(d).sort();
1792
+ } catch {
1793
+ return;
1794
+ }
1795
+ for (const name of entries) {
1796
+ const full = join3(d, name);
1797
+ if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
1798
+ out.push(full);
1799
+ continue;
1800
+ }
1801
+ if (level >= depth) continue;
1802
+ if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
1803
+ if (io.isDirectory(full)) walk(full, level + 1);
1804
+ }
1805
+ };
1806
+ walk(dir, 0);
1807
+ return out;
1808
+ }
1809
+ function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
1810
+ const byTarget = /* @__PURE__ */ new Map();
1811
+ for (const dir of dirs) mergeCarveStates(byTarget, listCarveManifests(dir, io, depth), io);
1812
+ return byTarget;
1813
+ }
1814
+ function mergeCarveStates(byTarget, paths, io) {
1815
+ const rank = { emitted: 0, bridged: 1, applied: 2 };
1816
+ for (const path of paths) {
1817
+ const manifest = readCarveManifest(path, io.readFile);
1818
+ if (!manifest) continue;
1819
+ const state = carveStateOf(manifest, path);
1820
+ const held = byTarget.get(state.target);
1821
+ if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
1822
+ }
1823
+ }
1824
+ var CARVE_STATUS_FLOOR = "0.54.0";
1825
+ function carveStatusArgs(from) {
1826
+ return ["carve", "status", "--from", from, "--json"];
1827
+ }
1828
+ async function discoverCarveStates(dirs, read, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
1829
+ const byTarget = /* @__PURE__ */ new Map();
1830
+ for (const dir of dirs) {
1831
+ const status = await read(dir).catch(() => void 0);
1832
+ const paths = status?.carves ? status.carves.map((row) => resolve(status.from ?? dir, row.path)) : listCarveManifests(dir, io, depth);
1833
+ mergeCarveStates(byTarget, paths, io);
1834
+ }
1835
+ return byTarget;
1836
+ }
1837
+ function joinCarvedSources(ir, states, memberDir) {
1838
+ const root = resolve(memberDir);
1839
+ const candidates = [];
1840
+ for (const s of states) {
1841
+ for (const f of s.sourceFiles) {
1842
+ const abs = resolve(f);
1843
+ if (abs === root || abs.startsWith(root + sep)) candidates.push({ abs, state: s });
1844
+ }
1845
+ }
1846
+ if (candidates.length === 0) return { ir, joined: 0 };
1847
+ let joined = 0;
1848
+ const nodes = ir.nodes.map((n) => {
1849
+ const file = n.sourceLoc?.file;
1850
+ if (!file) return n;
1851
+ const hit = emittedFileFor(file, root, candidates);
1852
+ if (!hit) return n;
1853
+ joined++;
1854
+ const { state, abs } = hit;
1855
+ const carve = {
1856
+ target: state.target,
1857
+ ...state.tfType ? { tfType: state.tfType } : {},
1858
+ stage: state.stage,
1859
+ graduated: state.graduated,
1860
+ ...state.at ? { at: state.at } : {},
1861
+ ...state.from ? { from: state.from } : {},
1862
+ file: relative(root, abs).split(sep).join("/")
1863
+ };
1864
+ return { ...n, attrs: { ...n.attrs, carved: state.target, _carve: carve } };
1865
+ });
1866
+ return { ir: joined ? { ...ir, nodes } : ir, joined };
1867
+ }
1868
+ function emittedFileFor(file, root, candidates) {
1869
+ const rel = file.split("\\").join("/").replace(/^\.\//, "");
1870
+ if (isAbsolute(rel)) {
1871
+ const abs = resolve(rel);
1872
+ return candidates.find((c) => c.abs === abs);
1873
+ }
1874
+ const exact = resolve(root, rel);
1875
+ const direct = candidates.find((c) => c.abs === exact);
1876
+ if (direct) return direct;
1877
+ const suffix = candidates.filter((c) => c.abs.split(sep).join("/").endsWith("/" + rel));
1878
+ return suffix.length === 1 ? suffix[0] : void 0;
1879
+ }
1880
+ function emittedNodesFor(ir, states, memberDir) {
1881
+ const out = /* @__PURE__ */ new Map();
1882
+ if (!ir) return out;
1883
+ for (const n of joinCarvedSources(ir, states, memberDir).ir.nodes) {
1884
+ const carve = n.attrs._carve;
1885
+ if (carve && !out.has(carve.target)) out.set(carve.target, n);
1886
+ }
1887
+ return out;
1888
+ }
1889
+ function carveProgress(states, total) {
1890
+ let applied = 0;
1891
+ let bridged = 0;
1892
+ let emitted = 0;
1893
+ for (const s of states) {
1894
+ if (s.stage === "applied") applied++;
1895
+ else if (s.stage === "bridged") bridged++;
1896
+ else emitted++;
1897
+ }
1898
+ const inFlight = bridged + emitted;
1899
+ const parts = [];
1900
+ if (emitted) parts.push(`${emitted} emitted`);
1901
+ if (bridged) parts.push(`${bridged} bridged`);
1902
+ return {
1903
+ applied,
1904
+ bridged,
1905
+ emitted,
1906
+ inFlight,
1907
+ total: typeof total === "number" ? total : null,
1908
+ label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
1909
+ detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
1910
+ };
1911
+ }
1912
+ function applyCommandFor(state, labels = {}) {
1913
+ const env = state.ownership?.env ?? "<env>";
1914
+ const stack = state.ownership?.stack ?? "<stack>";
1915
+ const from = labels.from ?? state.from ?? "<terraform-dir>";
1916
+ const out = labels.out ?? state.outDir;
1917
+ return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
1918
+ }
1919
+ function carveStatePayload(states, total, labels = () => ({})) {
1920
+ const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
1921
+ return {
1922
+ manifests: list3.length,
1923
+ progress: carveProgress(list3, total),
1924
+ states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
1925
+ apply: { human: true, note: APPLY_IS_HUMAN }
1926
+ };
1927
+ }
1928
+ function splitCarveState(tfIr, states, opts = {}) {
1929
+ if (states.size === 0) return { tf: tfIr, graduated: [] };
1930
+ const shorten = opts.shorten ?? ((p) => p);
1931
+ const emitted = opts.emitted ?? /* @__PURE__ */ new Map();
1932
+ const declaredAttrs = (n) => Object.fromEntries(Object.entries(n.attrs ?? {}).filter(([k]) => !k.startsWith("_") && k !== "carved"));
1933
+ const source = (files) => files.map(shorten).join(", ");
1934
+ const gone = /* @__PURE__ */ new Set();
1935
+ const graduated = [];
1936
+ const nodes = [];
1937
+ for (const node of tfIr.nodes) {
1938
+ const state = states.get(node.id);
1939
+ if (!state) {
1940
+ nodes.push(node);
1941
+ continue;
1942
+ }
1943
+ const became = emitted.get(node.id);
1944
+ if (state.graduated) {
1945
+ gone.add(node.id);
1946
+ graduated.push({
1947
+ ...node,
1948
+ ...became ? { kind: became.kind, lexicon: became.lexicon, ...became.sourceLoc ? { sourceLoc: became.sourceLoc } : {} } : {},
1949
+ attrs: {
1950
+ ...node.attrs,
1951
+ ...became ? { ...declaredAttrs(became), chantEntity: `${became.kind} ${became.id}` } : {},
1952
+ _status: "good",
1953
+ carve: carveWordFor(state.stage),
1954
+ carveState: state.stage,
1955
+ ...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
1956
+ ...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
1957
+ ...state.at ? { carvedAt: state.at } : {},
1958
+ ...state.files.length ? { chantSource: source(state.files) } : {}
1959
+ }
1960
+ });
1961
+ continue;
1962
+ }
1963
+ nodes.push({
1964
+ ...node,
1965
+ attrs: {
1966
+ ...node.attrs,
1967
+ _status: carveToneFor(state.stage),
1968
+ carve: carveWordFor(state.stage),
1969
+ carveState: state.stage,
1970
+ ...became ? { chantEntity: `${became.kind} ${became.id}` } : {},
1971
+ ...state.at ? { carvedAt: state.at } : {},
1972
+ ...state.files.length ? { chantSource: source(state.files) } : {},
1973
+ ...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
1974
+ ...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
1975
+ }
1976
+ });
1977
+ }
1978
+ const byStack = {};
1979
+ for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
1980
+ const left = members.filter((id) => !gone.has(id));
1981
+ if (left.length) byStack[band] = left;
1982
+ }
1983
+ return {
1984
+ tf: {
1985
+ nodes,
1986
+ edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
1987
+ groups: { ...tfIr.groups, byStack }
1988
+ },
1989
+ graduated
1990
+ };
1991
+ }
1992
+ var GRADUATED_BAND = "carved \u2192 chant";
1993
+ function bandGraduated(tf, graduated) {
1994
+ if (graduated.length === 0) return tf;
1995
+ return {
1996
+ nodes: [...graduated, ...tf.nodes],
1997
+ edges: tf.edges,
1998
+ groups: {
1999
+ ...tf.groups,
2000
+ byStack: {
2001
+ [GRADUATED_BAND]: graduated.map((n) => n.id),
2002
+ ...tf.groups?.byStack ?? {}
2003
+ }
2004
+ }
2005
+ };
2006
+ }
2007
+ function carveStateNote(progress, states) {
2008
+ if (states.size === 0) return "";
2009
+ const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
2010
+ return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
2011
+ }
2012
+
2013
+ // src/foreign.ts
2014
+ import { isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
2015
+ function dropForeignDeclarations(ir, projectDir) {
2016
+ const root = resolve2(projectDir);
2017
+ const foreign = [];
2018
+ for (const n of ir.nodes) {
2019
+ const file = n.sourceLoc?.file;
2020
+ if (!file || !isAbsolute2(file)) continue;
2021
+ const abs = resolve2(file);
2022
+ if (abs === root || abs.startsWith(root + sep2)) continue;
2023
+ foreign.push({ id: n.id, kind: n.kind, file });
2024
+ }
2025
+ if (foreign.length === 0) return ir;
2026
+ const gone = new Set(foreign.map((f) => f.id));
2027
+ const groups = {};
2028
+ for (const [axis, members] of Object.entries(ir.groups ?? {})) {
2029
+ if (!members || typeof members !== "object") continue;
2030
+ const kept = {};
2031
+ for (const [box, ids] of Object.entries(members)) {
2032
+ const left = ids.filter((id) => !gone.has(id));
2033
+ if (left.length) kept[box] = left;
2034
+ }
2035
+ groups[axis] = kept;
2036
+ }
2037
+ return {
2038
+ ...ir,
2039
+ nodes: ir.nodes.filter((n) => !gone.has(n.id)),
2040
+ edges: ir.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
2041
+ groups,
2042
+ foreign
2043
+ };
2044
+ }
2045
+ function foreignNote(ir) {
2046
+ const f = ir.foreign;
2047
+ if (!f || f.length === 0) return void 0;
2048
+ const named = f.slice(0, 3).map((d) => d.id).join(", ");
2049
+ const more = f.length > 3 ? ` +${f.length - 3}` : "";
2050
+ return `${f.length} declaration${f.length === 1 ? "" : "s"} outside this project dropped (${named}${more}) \u2014 chant graphs Ops from the git root (chant#2058)`;
2051
+ }
2052
+
1667
2053
  // src/chant.ts
1668
2054
  function envOverridesFor(opts) {
1669
2055
  const overrides = {};
@@ -1694,14 +2080,14 @@ function packageManifestFrom(req, name) {
1694
2080
  } catch {
1695
2081
  return void 0;
1696
2082
  }
1697
- let dir = dirname2(entry);
2083
+ let dir = dirname3(entry);
1698
2084
  for (; ; ) {
1699
2085
  try {
1700
- const pkg = createRequire(import.meta.url)(join3(dir, "package.json"));
2086
+ const pkg = createRequire(import.meta.url)(join4(dir, "package.json"));
1701
2087
  if (pkg.name === name) return { root: dir, manifest: pkg };
1702
2088
  } catch {
1703
2089
  }
1704
- const parent = dirname2(dir);
2090
+ const parent = dirname3(dir);
1705
2091
  if (parent === dir) break;
1706
2092
  dir = parent;
1707
2093
  }
@@ -1711,10 +2097,10 @@ function chantPackageFrom(req) {
1711
2097
  const found = packageManifestFrom(req, "@intentius/chant");
1712
2098
  if (!found) return void 0;
1713
2099
  const { root, manifest } = found;
1714
- return { root, bin: join3(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
2100
+ return { root, bin: join4(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
1715
2101
  }
1716
2102
  function resolveLexicons(projectDir, lexicons) {
1717
- const req = createRequire(join3(resolve(projectDir), "noop.js"));
2103
+ const req = createRequire(join4(resolve3(projectDir), "noop.js"));
1718
2104
  return lexicons.map((lexicon) => {
1719
2105
  const pkg = `@intentius/chant-lexicon-${lexicon}`;
1720
2106
  const found = packageManifestFrom(req, pkg);
@@ -1724,7 +2110,7 @@ function resolveLexicons(projectDir, lexicons) {
1724
2110
  function resolveChant(projectDir) {
1725
2111
  const own = chantPackageFrom(createRequire(import.meta.url));
1726
2112
  if (projectDir) {
1727
- const fromProject = chantPackageFrom(createRequire(join3(resolve(projectDir), "noop.js")));
2113
+ const fromProject = chantPackageFrom(createRequire(join4(resolve3(projectDir), "noop.js")));
1728
2114
  if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
1729
2115
  }
1730
2116
  if (own) return { source: "behold", ...own };
@@ -1732,8 +2118,8 @@ function resolveChant(projectDir) {
1732
2118
  }
1733
2119
  function chantFloor() {
1734
2120
  try {
1735
- const here = dirname2(fileURLToPath(import.meta.url));
1736
- const pkg = JSON.parse(readFileSync3(join3(here, "..", "package.json"), "utf8"));
2121
+ const here = dirname3(fileURLToPath(import.meta.url));
2122
+ const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
1737
2123
  return pkg.dependencies?.["@intentius/chant"]?.replace(/^[^0-9]*/, "") || void 0;
1738
2124
  } catch {
1739
2125
  return void 0;
@@ -1839,7 +2225,7 @@ function runCommandStream(cmd, args, cwd, onLine) {
1839
2225
  return { pid: proc.pid ?? -1, kill: () => proc.kill(), done };
1840
2226
  }
1841
2227
  function legacyGraphPath(projectDir) {
1842
- const src = join3(projectDir, "src");
2228
+ const src = join4(projectDir, "src");
1843
2229
  return existsSync3(src) ? src : projectDir;
1844
2230
  }
1845
2231
  async function graphPath(projectDir, opts = {}) {
@@ -1851,9 +2237,9 @@ async function graphPath(projectDir, opts = {}) {
1851
2237
  }
1852
2238
  if (info.stacks?.length) {
1853
2239
  const picked = opts.stack ? info.stacks.find((s) => s.name === opts.stack) : void 0;
1854
- return resolve(projectDir, (picked ?? info.stacks[0]).src);
2240
+ return resolve3(projectDir, (picked ?? info.stacks[0]).src);
1855
2241
  }
1856
- if (info.sourceDir) return resolve(projectDir, info.sourceDir);
2242
+ if (info.sourceDir) return resolve3(projectDir, info.sourceDir);
1857
2243
  return legacyGraphPath(projectDir);
1858
2244
  }
1859
2245
  function graphArgs(src, format, opts, components) {
@@ -1861,10 +2247,11 @@ function graphArgs(src, format, opts, components) {
1861
2247
  }
1862
2248
  async function graphIr(projectDir, opts = {}) {
1863
2249
  const src = await graphPath(projectDir, opts);
1864
- return runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
2250
+ const ir = await runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
2251
+ return dropForeignDeclarations(ir, projectDir);
1865
2252
  }
1866
2253
  async function clusterRootGraphIr(projectDir, opts = {}) {
1867
- if (!existsSync3(join3(projectDir, "cluster"))) return void 0;
2254
+ if (!existsSync3(join4(projectDir, "cluster"))) return void 0;
1868
2255
  const { live: _live, overlay: _overlay, env: _env, ...sourceOnly } = opts;
1869
2256
  try {
1870
2257
  return await runChantJson(graphArgs("cluster", "ir", sourceOnly, false), projectDir, envOverridesFor(sourceOnly));
@@ -1882,6 +2269,9 @@ function componentStatusArgs(env) {
1882
2269
  function componentStatus(projectDir, env, opts = {}) {
1883
2270
  return runChantJson(componentStatusArgs(env), projectDir, envOverridesFor(opts));
1884
2271
  }
2272
+ function ciWorkflowName(env) {
2273
+ return `chant-components-${env}`;
2274
+ }
1885
2275
  var CI_FORGES = ["gitlab", "github", "forgejo"];
1886
2276
  function ciForgeFor(lexicons) {
1887
2277
  return CI_FORGES.find((forge) => lexicons.includes(forge));
@@ -1899,7 +2289,21 @@ function parseCiPipeline(stdout) {
1899
2289
  const script = Array.isArray(props?.script) ? props.script.map(String) : [];
1900
2290
  return { ...j, script };
1901
2291
  });
1902
- return { stages: parsed.stages, jobs };
2292
+ const fromJson = typeof parsed.env === "string" && parsed.env ? parsed.env : void 0;
2293
+ const env = fromJson ?? pipelineEnvFromYaml(doc);
2294
+ return { stages: parsed.stages, jobs, ...env ? { env } : {} };
2295
+ }
2296
+ function pipelineEnvFromYaml(doc) {
2297
+ const name = doc?.name;
2298
+ if (typeof name === "string") {
2299
+ const m = /^chant-components-(.+)$/.exec(name);
2300
+ if (m) return m[1];
2301
+ }
2302
+ const env = doc?.env;
2303
+ if (env && typeof env === "object" && typeof env.CHANT_ENV === "string") {
2304
+ return env.CHANT_ENV;
2305
+ }
2306
+ return void 0;
1903
2307
  }
1904
2308
  function ciPipeline(projectDir, opts = {}, forge = "gitlab") {
1905
2309
  const args = ciPipelineArgs(opts, forge);
@@ -1920,6 +2324,9 @@ function lifecycleDiffArgs(env) {
1920
2324
  function lifecycleDiffLive(projectDir, env, opts = {}) {
1921
2325
  return runChantJson(lifecycleDiffArgs(env), projectDir, envOverridesFor(opts));
1922
2326
  }
2327
+ function carveStatus(from, projectDir) {
2328
+ return runChantJson(carveStatusArgs(from), projectDir);
2329
+ }
1923
2330
  function helmRendersArgs() {
1924
2331
  return ["helm", "renders", "--json"];
1925
2332
  }
@@ -1996,8 +2403,8 @@ function mergeClusterRoot(ir, clusterIr, running) {
1996
2403
  }
1997
2404
 
1998
2405
  // src/helm-releases.ts
1999
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
2000
- import { join as join4 } from "node:path";
2406
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
2407
+ import { join as join5 } from "node:path";
2001
2408
  var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
2002
2409
  function declaredChartNames(ir) {
2003
2410
  const names = /* @__PURE__ */ new Set();
@@ -2017,15 +2424,15 @@ function matchesDeclaredChart(chart, declared) {
2017
2424
  }
2018
2425
  function discoverReleaseUnits(projectDir) {
2019
2426
  const owners = /* @__PURE__ */ new Map();
2020
- const roots = [join4(projectDir, "src"), join4(projectDir, "ops")];
2427
+ const roots = [join5(projectDir, "src"), join5(projectDir, "ops")];
2021
2428
  const files = [];
2022
2429
  const walk = (dir, depth) => {
2023
2430
  if (depth > 4 || !existsSync4(dir)) return;
2024
- for (const f of readdirSync(dir)) {
2025
- const p = join4(dir, f);
2431
+ for (const f of readdirSync2(dir)) {
2432
+ const p = join5(dir, f);
2026
2433
  let s;
2027
2434
  try {
2028
- s = statSync(p);
2435
+ s = statSync2(p);
2029
2436
  } catch {
2030
2437
  continue;
2031
2438
  }
@@ -2037,7 +2444,7 @@ function discoverReleaseUnits(projectDir) {
2037
2444
  for (const file of files) {
2038
2445
  let content;
2039
2446
  try {
2040
- content = readFileSync4(file, "utf8");
2447
+ content = readFileSync5(file, "utf8");
2041
2448
  } catch {
2042
2449
  continue;
2043
2450
  }
@@ -2089,9 +2496,9 @@ function synthesizeHelmReleases(ir, observed, owners) {
2089
2496
  }
2090
2497
 
2091
2498
  // src/gh-run.ts
2092
- import { spawn as spawn2 } from "node:child_process";
2093
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
2094
- import { join as join5 } from "node:path";
2499
+ import { execFileSync, spawn as spawn2 } from "node:child_process";
2500
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
2501
+ import { join as join6 } from "node:path";
2095
2502
 
2096
2503
  // src/ci-run.ts
2097
2504
  function pipelineProgress(pipeline) {
@@ -2135,6 +2542,9 @@ function foldPipelineLine(state, line) {
2135
2542
  }
2136
2543
  return out;
2137
2544
  }
2545
+ function losePipelineProgress(state) {
2546
+ return { ...state, status: "lost" };
2547
+ }
2138
2548
  function finishPipelineProgress(state, exitCode) {
2139
2549
  const terminal = exitCode === 0 ? "ok" : "failed";
2140
2550
  let out = state;
@@ -2145,19 +2555,19 @@ function finishPipelineProgress(state, exitCode) {
2145
2555
  }
2146
2556
 
2147
2557
  // src/gh-run.ts
2148
- var defaultGhExec = (args) => new Promise((resolve9) => {
2558
+ var defaultGhExec = (args) => new Promise((resolve12) => {
2149
2559
  let out = "";
2150
2560
  let proc;
2151
2561
  try {
2152
2562
  proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
2153
2563
  } catch {
2154
- resolve9({ code: 127, out: "" });
2564
+ resolve12({ code: 127, out: "" });
2155
2565
  return;
2156
2566
  }
2157
2567
  proc.stdout.on("data", (d) => out += d);
2158
2568
  proc.stderr.on("data", (d) => out += d);
2159
- proc.on("error", () => resolve9({ code: 127, out }));
2160
- proc.on("close", (code) => resolve9({ code: code ?? 1, out }));
2569
+ proc.on("error", () => resolve12({ code: 127, out }));
2570
+ proc.on("close", (code) => resolve12({ code: code ?? 1, out }));
2161
2571
  });
2162
2572
  async function ghReady(exec = defaultGhExec) {
2163
2573
  const { code, out } = await exec(["auth", "status"]);
@@ -2176,31 +2586,64 @@ function parseWorkflow(file, text) {
2176
2586
  const jobs = d.jobs && typeof d.jobs === "object" && !Array.isArray(d.jobs) ? Object.keys(d.jobs) : [];
2177
2587
  const on = d.on ?? d[true];
2178
2588
  const dispatchable = on === "workflow_dispatch" || Array.isArray(on) && on.includes("workflow_dispatch") || !!on && typeof on === "object" && "workflow_dispatch" in on || /^\s{2,}workflow_dispatch\s*:?\s*$/m.test(text);
2179
- return { file, jobIds: jobs, dispatchable };
2589
+ return { file, jobIds: jobs, dispatchable, ...typeof d.name === "string" && d.name ? { name: d.name } : {} };
2590
+ }
2591
+ function workflowsDir(projectDir) {
2592
+ let root = projectDir;
2593
+ try {
2594
+ root = execFileSync("git", ["-C", projectDir, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || projectDir;
2595
+ } catch {
2596
+ }
2597
+ return join6(root, ".github", "workflows");
2180
2598
  }
2181
- function pickWorkflow(projectDir, pipeline) {
2182
- const dir = join5(projectDir, ".github", "workflows");
2599
+ function committedWorkflows(projectDir) {
2600
+ const dir = workflowsDir(projectDir);
2183
2601
  let files;
2184
2602
  try {
2185
- files = readdirSync2(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
2603
+ files = readdirSync3(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
2186
2604
  } catch {
2187
- return void 0;
2605
+ return [];
2188
2606
  }
2189
- const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
2190
- let best;
2607
+ const out = [];
2191
2608
  for (const f of files) {
2192
- let text;
2193
2609
  try {
2194
- text = readFileSync5(join5(dir, f), "utf8");
2610
+ out.push(parseWorkflow(f, readFileSync6(join6(dir, f), "utf8")));
2195
2611
  } catch {
2196
- continue;
2197
2612
  }
2198
- const info = parseWorkflow(f, text);
2199
- if (!info.dispatchable) continue;
2200
- const overlap = info.jobIds.filter((id) => wanted.has(id)).length;
2201
- if (overlap > 0 && (!best || overlap > best.overlap)) best = { info, overlap };
2202
2613
  }
2203
- return best?.info;
2614
+ return out;
2615
+ }
2616
+ function resolveWorkflow(projectDir, pipeline, designated) {
2617
+ const all = committedWorkflows(projectDir);
2618
+ if (designated) {
2619
+ const hit = all.find((w) => w.file === designated);
2620
+ if (!hit) return { reason: `the designated workflow .github/workflows/${designated} is not committed \u2014 Deploy is disabled for this env until it is (behold never falls back to a guess, or to running it here)` };
2621
+ if (!hit.dispatchable) return { reason: `the designated workflow ${designated} declares no workflow_dispatch \u2014 add it to its \`on:\` block; until then Deploy is disabled for this env` };
2622
+ return { workflow: hit };
2623
+ }
2624
+ const dispatchable = all.filter((w) => w.dispatchable);
2625
+ if (pipeline.env) {
2626
+ const named = dispatchable.filter((w) => w.name === ciWorkflowName(pipeline.env));
2627
+ if (named.length === 1) return { workflow: named[0] };
2628
+ if (named.length > 1) return { reason: `${named.length} committed workflows are named ${ciWorkflowName(pipeline.env)} (${named.map((w) => w.file).join(", ")}) \u2014 designate one in .behold.json's executor block` };
2629
+ }
2630
+ const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
2631
+ const otherEnv = (w) => {
2632
+ const m = w.name ? /^chant-components-(.+)$/.exec(w.name) : null;
2633
+ return !!m && !!pipeline.env && m[1] !== pipeline.env;
2634
+ };
2635
+ const scored = dispatchable.filter((w) => !otherEnv(w)).map((w) => ({ w, overlap: w.jobIds.filter((id) => wanted.has(id)).length })).filter((x) => x.overlap > 0).sort((a, b) => b.overlap - a.overlap);
2636
+ if (scored.length === 0) {
2637
+ const excluded = dispatchable.filter(otherEnv).map((w) => `${w.file} (${w.name})`);
2638
+ return {
2639
+ reason: "no committed workflow_dispatch workflow whose jobs match the generated pipeline \u2014 commit one (or add `workflow_dispatch:` to its `on:` block)" + (excluded.length ? `; ${excluded.join(", ")} ${excluded.length === 1 ? "is" : "are"} named for another env and never stand in` : "")
2640
+ };
2641
+ }
2642
+ if (scored.length > 1 && scored[1].overlap === scored[0].overlap) {
2643
+ const tied = scored.filter((x) => x.overlap === scored[0].overlap).map((x) => x.w.file);
2644
+ return { reason: `${tied.length} committed workflows match the pipeline equally (${tied.join(", ")}) \u2014 the generated job ids are the same for every env, so behold cannot tell which one deploys ${pipeline.env ?? "this env"}; designate it in .behold.json's executor block` };
2645
+ }
2646
+ return { workflow: scored[0].w };
2204
2647
  }
2205
2648
  async function latestRunId(workflowFile, exec = defaultGhExec) {
2206
2649
  const { code, out } = await exec(["run", "list", "--workflow", workflowFile, "--limit", "1", "--json", "databaseId"]);
@@ -2234,11 +2677,13 @@ function runViewToProgress(pipeline, view) {
2234
2677
  return { ...w, status };
2235
2678
  })
2236
2679
  };
2680
+ if (view.url) state = { ...state, url: view.url };
2237
2681
  if (view.status === "completed") {
2238
2682
  return { ...state, status: view.conclusion === "success" ? "ok" : "failed" };
2239
2683
  }
2240
2684
  return state;
2241
2685
  }
2686
+ var LOST_EXIT = 75;
2242
2687
  async function dispatchAndFollow(workflow, pipeline, ref, deps) {
2243
2688
  const exec = deps.exec ?? defaultGhExec;
2244
2689
  const pollMs = deps.pollMs ?? 5e3;
@@ -2267,9 +2712,28 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
2267
2712
  return 1;
2268
2713
  }
2269
2714
  deps.onLine(`\u25CF run ${runId} started \u2014 following`);
2715
+ deps.onAdopted?.({ runId });
2716
+ return followRun(runId, pipeline, deps);
2717
+ }
2718
+ async function followRun(runId, pipeline, deps) {
2719
+ const exec = deps.exec ?? defaultGhExec;
2720
+ const pollMs = deps.pollMs ?? 5e3;
2721
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2722
+ const pollFailBudget = deps.pollFailBudget ?? 24;
2723
+ const followDeadline = Date.now() + (deps.followTimeoutMs ?? 6 * 60 * 60 * 1e3);
2724
+ let urlSaid = false;
2725
+ let state = pipelineProgress(pipeline);
2726
+ let consecutiveFailures = 0;
2727
+ const lose = (why) => {
2728
+ deps.onLine(`\u2717 run ${runId}: ${why} \u2014 behold stopped following; the run itself may still be live${state.url ? ` at ${state.url}` : " on the forge"} (restart behold, or POST /api/ci/readopt, to re-adopt it)`);
2729
+ deps.onProgress(losePipelineProgress(state));
2730
+ deps.onConcluded?.({ verdict: "lost" });
2731
+ return LOST_EXIT;
2732
+ };
2270
2733
  for (; ; ) {
2271
- const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs"]);
2734
+ const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs,url"]);
2272
2735
  if (code === 0) {
2736
+ consecutiveFailures = 0;
2273
2737
  let view;
2274
2738
  try {
2275
2739
  view = JSON.parse(out);
@@ -2277,14 +2741,29 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
2277
2741
  view = void 0;
2278
2742
  }
2279
2743
  if (view) {
2280
- deps.onProgress(runViewToProgress(pipeline, view));
2744
+ if (view.url && !urlSaid) {
2745
+ urlSaid = true;
2746
+ deps.onAdopted?.({ runId, url: view.url });
2747
+ deps.onLine(`\u25CF ${view.url} \u2014 if the workflow's environment requires an approval, it is granted there, by a GitHub identity; behold holds none that could`);
2748
+ }
2749
+ state = runViewToProgress(pipeline, view);
2750
+ deps.onProgress(state);
2281
2751
  if (view.status === "completed") {
2282
2752
  deps.onLine(`\u25A0 run ${runId} ${view.conclusion ?? "completed"}`);
2283
- return view.conclusion === "success" ? 0 : 1;
2753
+ const ok = view.conclusion === "success";
2754
+ deps.onConcluded?.({ verdict: ok ? "ok" : "failed" });
2755
+ return ok ? 0 : 1;
2284
2756
  }
2285
2757
  }
2286
2758
  } else {
2287
- deps.onLine(`\u26A0 gh run view ${runId} failed (exit ${code}) \u2014 retrying`);
2759
+ consecutiveFailures++;
2760
+ deps.onLine(`\u26A0 gh run view ${runId} failed (exit ${code}) \u2014 retrying (${consecutiveFailures}/${pollFailBudget})`);
2761
+ if (consecutiveFailures >= pollFailBudget) {
2762
+ return lose(`${pollFailBudget} consecutive polls failed \u2014 the stream is dead, not flaky`);
2763
+ }
2764
+ }
2765
+ if (Date.now() >= followDeadline) {
2766
+ return lose("the follow deadline passed");
2288
2767
  }
2289
2768
  await sleep(pollMs);
2290
2769
  }
@@ -2310,18 +2789,57 @@ function joinCiProgress(ir, state) {
2310
2789
  return ir;
2311
2790
  }
2312
2791
 
2792
+ // src/ci-run-store.ts
2793
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2, rmSync } from "node:fs";
2794
+ import { homedir as homedir2 } from "node:os";
2795
+ import { join as join7, resolve as resolve4 } from "node:path";
2796
+ function slug(projectDir) {
2797
+ return resolve4(projectDir).replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-80);
2798
+ }
2799
+ function defaultRoot() {
2800
+ return process.env.BEHOLD_CI_RUN_DIR ?? join7(homedir2(), ".behold", "ci-runs");
2801
+ }
2802
+ function dispatchedRunPath(projectDir, root = defaultRoot()) {
2803
+ return join7(root, `${slug(projectDir)}.json`);
2804
+ }
2805
+ function saveDispatchedRun(run3, root = defaultRoot()) {
2806
+ try {
2807
+ const path = dispatchedRunPath(run3.projectDir, root);
2808
+ mkdirSync2(join7(path, ".."), { recursive: true });
2809
+ writeFileSync2(path, JSON.stringify({ version: 1, ...run3 }, null, 2) + "\n");
2810
+ } catch {
2811
+ }
2812
+ }
2813
+ function readDispatchedRun(projectDir, root) {
2814
+ try {
2815
+ const path = dispatchedRunPath(projectDir, root);
2816
+ if (!existsSync5(path)) return void 0;
2817
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
2818
+ if (parsed?.version !== 1 || typeof parsed.runId !== "number") return void 0;
2819
+ if (resolve4(parsed.projectDir) !== resolve4(projectDir)) return void 0;
2820
+ return parsed;
2821
+ } catch {
2822
+ return void 0;
2823
+ }
2824
+ }
2825
+ function concludeDispatchedRun(projectDir, runId, verdict, root) {
2826
+ const current = readDispatchedRun(projectDir, root);
2827
+ if (!current || current.runId !== runId) return;
2828
+ saveDispatchedRun({ ...current, concluded: { verdict, at: (/* @__PURE__ */ new Date()).toISOString() } }, root);
2829
+ }
2830
+
2313
2831
  // src/operator.ts
2314
2832
  var ANSI = /\x1b\[[0-9;]*m/g;
2315
2833
  var strip = (s) => s.replace(ANSI, "");
2316
- var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2834
+ var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2317
2835
  var HOW_TO_DECLARE = "An operating loop is a ConvergeOp: declare one in a `*.op.ts` (`ConvergeOp({ name, env, dial, rules })` from @intentius/chant-lexicon-temporal), build the project, and tick it with `chant operator` (or a cron / CronJob invoking it).";
2318
2836
  function readRow(value) {
2319
- if (!isRecord(value)) return null;
2837
+ if (!isRecord2(value)) return null;
2320
2838
  if (typeof value.op !== "string" || typeof value.env !== "string") return null;
2321
2839
  if (!Array.isArray(value.pendingGates)) return null;
2322
2840
  const gates = [];
2323
2841
  for (const g of value.pendingGates) {
2324
- if (!isRecord(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
2842
+ if (!isRecord2(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
2325
2843
  gates.push({
2326
2844
  rule: g.rule,
2327
2845
  gate: g.gate,
@@ -2333,9 +2851,9 @@ function readRow(value) {
2333
2851
  ...typeof g.url === "string" && g.url ? { url: g.url } : {}
2334
2852
  });
2335
2853
  }
2336
- const tick = isRecord(value.lastTick) ? value.lastTick : void 0;
2854
+ const tick = isRecord2(value.lastTick) ? value.lastTick : void 0;
2337
2855
  const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
2338
- const lease = isRecord(value.lease) ? value.lease : void 0;
2856
+ const lease = isRecord2(value.lease) ? value.lease : void 0;
2339
2857
  return {
2340
2858
  op: value.op,
2341
2859
  env: value.env,
@@ -2433,7 +2951,7 @@ var CHANT = "chant";
2433
2951
  var CONVERGE_TICK = "converge-tick";
2434
2952
  var CRONJOB_KIND = "K8s::Batch::CronJob";
2435
2953
  var NAMESPACE_KIND = "K8s::Core::Namespace";
2436
- var rec = (v) => isRecord(v) ? v : void 0;
2954
+ var rec = (v) => isRecord2(v) ? v : void 0;
2437
2955
  var str = (v) => typeof v === "string" && v ? v : void 0;
2438
2956
  var labelsOf = (n) => rec(rec(n.attrs?.metadata)?.labels) ?? {};
2439
2957
  function operatorHomes(ir) {
@@ -2541,10 +3059,10 @@ function tickFreshness(at, now = Date.now()) {
2541
3059
  }
2542
3060
  var RECONCILIATIONS = /* @__PURE__ */ new Set(["reconciled", "unrecorded", "stale", "drifted", "unknown"]);
2543
3061
  function readVerdict(value) {
2544
- if (!isRecord(value)) return null;
3062
+ if (!isRecord2(value)) return null;
2545
3063
  if (typeof value.component !== "string" || !value.component) return null;
2546
3064
  if (typeof value.reconciliation !== "string" || !RECONCILIATIONS.has(value.reconciliation)) return null;
2547
- const unobserved = isRecord(value.unobserved) && typeof value.unobserved.reason === "string" ? value.unobserved : void 0;
3065
+ const unobserved = isRecord2(value.unobserved) && typeof value.unobserved.reason === "string" ? value.unobserved : void 0;
2548
3066
  return {
2549
3067
  component: value.component,
2550
3068
  reconciliation: value.reconciliation,
@@ -2655,12 +3173,12 @@ function operatorLogArgs(window) {
2655
3173
  ];
2656
3174
  }
2657
3175
  function readTickRecord(value) {
2658
- if (!isRecord(value)) return null;
3176
+ if (!isRecord2(value)) return null;
2659
3177
  if (typeof value.op !== "string" || typeof value.timestamp !== "string" || typeof value.log !== "string") return null;
2660
3178
  return value;
2661
3179
  }
2662
3180
  function readResolutionRecord(value) {
2663
- if (!isRecord(value)) return null;
3181
+ if (!isRecord2(value)) return null;
2664
3182
  const needed = ["op", "gate", "resolvedBy", "timestamp"];
2665
3183
  if (needed.some((k) => typeof value[k] !== "string")) return null;
2666
3184
  return value;
@@ -2721,10 +3239,10 @@ function readOperatorLog(run3) {
2721
3239
  remedy: "behold reads `{entries: [{kind, timestamp, record}], malformed: {converge, gates}}`."
2722
3240
  }
2723
3241
  });
2724
- if (!isRecord(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
3242
+ if (!isRecord2(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
2725
3243
  const entries = [];
2726
3244
  for (const value of parsed.entries) {
2727
- if (!isRecord(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
3245
+ if (!isRecord2(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
2728
3246
  if (value.kind === "tick") {
2729
3247
  const record = readTickRecord(value.record);
2730
3248
  if (!record) return unreadable("a tick entry with no `op`/`timestamp`/`log`");
@@ -2737,7 +3255,7 @@ function readOperatorLog(run3) {
2737
3255
  return unreadable(`an entry of an unknown kind "${String(value.kind)}"`);
2738
3256
  }
2739
3257
  }
2740
- const counts = isRecord(parsed.malformed) ? parsed.malformed : {};
3258
+ const counts = isRecord2(parsed.malformed) ? parsed.malformed : {};
2741
3259
  const count = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
2742
3260
  return { ok: true, entries, malformed: { converge: count(counts.converge), gates: count(counts.gates) } };
2743
3261
  }
@@ -3369,7 +3887,9 @@ function applyHelmArtifacts(ir, observed) {
3369
3887
  release: key.replace(/^release\//, ""),
3370
3888
  ...o.status ? { status: o.status } : {},
3371
3889
  ...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
3372
- ...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {}
3890
+ ...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {},
3891
+ ...typeof o.attributes?.inputDigest === "string" ? { inputDigest: o.attributes.inputDigest } : {},
3892
+ ...typeof o.attributes?.contentDigest === "string" ? { contentDigest: o.attributes.contentDigest } : {}
3373
3893
  };
3374
3894
  attrs._artifact = match;
3375
3895
  attrs._status = o.status === "deployed" ? "good" : "warn";
@@ -3378,37 +3898,35 @@ function applyHelmArtifacts(ir, observed) {
3378
3898
  return { charts, installed };
3379
3899
  }
3380
3900
 
3381
- // src/estate.ts
3382
- import { statSync as statSync3 } from "node:fs";
3383
- import { availableParallelism } from "node:os";
3384
- import { join as join7, resolve as resolve3, sep } from "node:path";
3385
- import { composeStacks, shortStackNames } from "@intentius/pinhole";
3901
+ // src/carve-discovery.ts
3902
+ import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
3903
+ import { join as join9 } from "node:path";
3386
3904
 
3387
3905
  // src/member-ir.ts
3388
3906
  import { createHash } from "node:crypto";
3389
- import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
3390
- import { join as join6, relative, resolve as resolve2 } from "node:path";
3907
+ import { readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
3908
+ import { join as join8, relative as relative2, resolve as resolve5 } from "node:path";
3391
3909
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
3392
3910
  function memberIrCacheSize(env = process.env) {
3393
3911
  const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
3394
3912
  return Number.isInteger(override) && override >= 0 ? override : 64;
3395
3913
  }
3396
3914
  function memberSourceStamp(dir) {
3397
- const root = resolve2(dir);
3915
+ const root = resolve5(dir);
3398
3916
  const h = createHash("sha1");
3399
3917
  let any = false;
3400
3918
  const walk = (at) => {
3401
- const entries = readdirSync3(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
3919
+ const entries = readdirSync4(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
3402
3920
  for (const e of entries) {
3403
3921
  if (SKIP.has(e.name)) continue;
3404
- const path = join6(at, e.name);
3922
+ const path = join8(at, e.name);
3405
3923
  if (e.isDirectory()) {
3406
3924
  walk(path);
3407
3925
  continue;
3408
3926
  }
3409
3927
  if (!e.isFile()) continue;
3410
- const st = statSync2(path);
3411
- h.update(`${relative(root, path)}\0${st.mtimeMs}\0${st.size}
3928
+ const st = statSync3(path);
3929
+ h.update(`${relative2(root, path)}\0${st.mtimeMs}\0${st.size}
3412
3930
  `);
3413
3931
  any = true;
3414
3932
  }
@@ -3423,7 +3941,7 @@ function memberSourceStamp(dir) {
3423
3941
  function cacheKey(dir, opts) {
3424
3942
  const chant = resolveChant(dir);
3425
3943
  const canonical = JSON.stringify(opts, Object.keys(opts).sort());
3426
- return `${resolve2(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
3944
+ return `${resolve5(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
3427
3945
  }
3428
3946
  var cache = /* @__PURE__ */ new Map();
3429
3947
  var hits = 0;
@@ -3433,7 +3951,7 @@ function invalidateMember(dir) {
3433
3951
  cache.clear();
3434
3952
  return;
3435
3953
  }
3436
- const root = resolve2(dir);
3954
+ const root = resolve5(dir);
3437
3955
  for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
3438
3956
  }
3439
3957
  async function memberIr(dir, opts = {}) {
@@ -3448,18 +3966,82 @@ async function memberIr(dir, opts = {}) {
3448
3966
  cache.set(key, hit);
3449
3967
  return structuredClone(hit.ir);
3450
3968
  }
3451
- misses++;
3452
- const ir = await graphIr(dir, opts);
3453
- if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
3454
- cache.delete(key);
3455
- cache.set(key, { dir: resolve2(dir), stamp, ir });
3456
- for (const oldest of cache.keys()) {
3457
- if (cache.size <= size) break;
3458
- cache.delete(oldest);
3969
+ misses++;
3970
+ const ir = await graphIr(dir, opts);
3971
+ if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
3972
+ cache.delete(key);
3973
+ cache.set(key, { dir: resolve5(dir), stamp, ir });
3974
+ for (const oldest of cache.keys()) {
3975
+ if (cache.size <= size) break;
3976
+ cache.delete(oldest);
3977
+ }
3978
+ return structuredClone(ir);
3979
+ }
3980
+
3981
+ // src/carve-discovery.ts
3982
+ function carveStatusReader(projectDir) {
3983
+ return async (dir) => {
3984
+ const via = projectDir ?? dir;
3985
+ if (!meetsFloor(resolveChant(via).version, CARVE_STATUS_FLOOR)) return void 0;
3986
+ return carveStatus(dir, via).catch(() => void 0);
3987
+ };
3988
+ }
3989
+ var SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".chant", ".terraform", "cdk.out"]);
3990
+ function hasAnyCarveManifest(dir, io = fsIo) {
3991
+ const walk = (d) => {
3992
+ let entries;
3993
+ try {
3994
+ entries = io.readdir(d);
3995
+ } catch {
3996
+ return false;
3997
+ }
3998
+ for (const name of entries) {
3999
+ if (name.endsWith(CARVE_MANIFEST_SUFFIX)) return true;
4000
+ if (name.startsWith(".") || SKIP2.has(name)) continue;
4001
+ const full = join9(d, name);
4002
+ if (io.isDirectory(full) && walk(full)) return true;
4003
+ }
4004
+ return false;
4005
+ };
4006
+ return walk(dir);
4007
+ }
4008
+ var fsIo = {
4009
+ readdir: (d) => readdirSync5(d),
4010
+ isDirectory: (p) => {
4011
+ try {
4012
+ return statSync4(p).isDirectory();
4013
+ } catch {
4014
+ return false;
4015
+ }
4016
+ }
4017
+ };
4018
+ var cache2 = /* @__PURE__ */ new Map();
4019
+ async function carveStatesFor(dir) {
4020
+ const stamp = memberSourceStamp(dir);
4021
+ const held = cache2.get(dir);
4022
+ if (held && stamp !== void 0 && held.stamp === stamp) return held.states;
4023
+ const states = hasAnyCarveManifest(dir) ? await discoverCarveStates([dir], carveStatusReader(), nodeCarveIo, MANIFEST_SCAN_DEPTH) : /* @__PURE__ */ new Map();
4024
+ if (stamp !== void 0) cache2.set(dir, { stamp, states });
4025
+ return states;
4026
+ }
4027
+ async function carveStatesUnder(dirs) {
4028
+ const rank = { emitted: 0, bridged: 1, applied: 2 };
4029
+ const out = /* @__PURE__ */ new Map();
4030
+ for (const dir of dirs) {
4031
+ for (const s of (await carveStatesFor(dir)).values()) {
4032
+ const held = out.get(s.target);
4033
+ if (!held || rank[s.stage] > rank[held.stage]) out.set(s.target, s);
4034
+ }
3459
4035
  }
3460
- return structuredClone(ir);
4036
+ return out;
3461
4037
  }
3462
4038
 
4039
+ // src/estate.ts
4040
+ import { statSync as statSync5 } from "node:fs";
4041
+ import { availableParallelism } from "node:os";
4042
+ import { join as join10, resolve as resolve6, sep as sep3 } from "node:path";
4043
+ import { composeStacks, shortStackNames } from "@intentius/pinhole";
4044
+
3463
4045
  // src/zoom-notes.ts
3464
4046
  function zoomNote(zoom, ir, compositeEdgesAttached) {
3465
4047
  const empty = ir.nodes.length === 0;
@@ -3562,7 +4144,10 @@ async function composeEstate(projectDirs, opts = {}) {
3562
4144
  const names = shortStackNames(projectDirs);
3563
4145
  const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
3564
4146
  name: names[i],
3565
- ir: await memberIr(dir, opts)
4147
+ // chant#2040: the manifests a member holds name the files its carved
4148
+ // entities were emitted into — joined here, where the member's root is
4149
+ // known, onto a COPY (the member IR is the #312 cache's own object).
4150
+ ir: joinCarvedSources(await memberIr(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
3566
4151
  }));
3567
4152
  return composeStacks(stacks);
3568
4153
  }
@@ -3583,7 +4168,7 @@ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
3583
4168
  var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
3584
4169
  var realIsDir = (p) => {
3585
4170
  try {
3586
- return statSync3(p).isDirectory();
4171
+ return statSync5(p).isDirectory();
3587
4172
  } catch {
3588
4173
  return false;
3589
4174
  }
@@ -3613,11 +4198,11 @@ function declaredSegments(path) {
3613
4198
  function pathAlignment(declared, dir, isDir = realIsDir) {
3614
4199
  const want = declaredSegments(declared);
3615
4200
  if (!want) return 0;
3616
- const root = resolve3(dir);
3617
- const have = root.split(sep).filter(Boolean);
4201
+ const root = resolve6(dir);
4202
+ const have = root.split(sep3).filter(Boolean);
3618
4203
  for (let k = Math.min(want.length, have.length); k >= 1; k--) {
3619
4204
  if (!have.slice(-k).every((s, i) => s === want[i])) continue;
3620
- if (!isDir(join7(root, ...want.slice(k)))) continue;
4205
+ if (!isDir(join10(root, ...want.slice(k)))) continue;
3621
4206
  return k;
3622
4207
  }
3623
4208
  return 0;
@@ -3647,10 +4232,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
3647
4232
  if (!winner || tied) continue;
3648
4233
  const prev = best.get(winner.dir);
3649
4234
  if (prev === "ambiguous") continue;
3650
- const join21 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
3651
- if (!prev) best.set(winner.dir, { score: winner.score, join: join21 });
4235
+ const join23 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
4236
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join23 });
3652
4237
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
3653
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join21 });
4238
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join23 });
3654
4239
  }
3655
4240
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
3656
4241
  }
@@ -3691,7 +4276,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
3691
4276
  const reason = firstLine2(err);
3692
4277
  try {
3693
4278
  const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
3694
- const src = await memberIr(dir, srcOpts);
4279
+ const src = joinCarvedSources(await memberIr(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
3695
4280
  for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
3696
4281
  stacks[i] = { name, ir: src };
3697
4282
  unobserved.push({ name, reason });
@@ -3714,6 +4299,36 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
3714
4299
  }
3715
4300
 
3716
4301
  // src/helm-drift.ts
4302
+ function renderIdentity(record, observed) {
4303
+ const declared = {
4304
+ ...record.inputDigest ? { inputDigest: record.inputDigest } : {},
4305
+ ...record.contentDigest ? { contentDigest: record.contentDigest } : {}
4306
+ };
4307
+ const a = observed?.observation.attributes ?? {};
4308
+ const obs = {
4309
+ ...observed ? { release: observed.key.replace(/^release\//, "") } : {},
4310
+ ...typeof a.inputDigest === "string" ? { inputDigest: a.inputDigest } : {},
4311
+ ...typeof a.contentDigest === "string" ? { contentDigest: a.contentDigest } : {}
4312
+ };
4313
+ const base = { declared, observed: obs };
4314
+ if (!declared.inputDigest && !declared.contentDigest) return { match: "unpinned", ...base };
4315
+ if (!obs.inputDigest && !obs.contentDigest) return { match: "unrecorded", ...base };
4316
+ if (declared.contentDigest && obs.contentDigest) {
4317
+ return { match: declared.contentDigest === obs.contentDigest ? "content" : "mismatch", ...base };
4318
+ }
4319
+ if (declared.inputDigest && obs.inputDigest) {
4320
+ return { match: declared.inputDigest === obs.inputDigest ? "input" : "mismatch", ...base };
4321
+ }
4322
+ return { match: "unrecorded", ...base };
4323
+ }
4324
+ function observedReleaseFor(record, observed, namespace) {
4325
+ if (!observed) return void 0;
4326
+ const candidates = Object.entries(observed).filter(([key2]) => key2.startsWith("release/") && key2.split("/").pop() === record.name);
4327
+ if (candidates.length === 0) return void 0;
4328
+ const scoped = namespace ? candidates.find(([key2]) => key2 === `release/${namespace}/${record.name}`) : void 0;
4329
+ const [key, observation] = scoped ?? candidates[0];
4330
+ return { key, observation };
4331
+ }
3717
4332
  function countChanges(docs) {
3718
4333
  return docs.reduce((n, d) => n + d.changes.length, 0);
3719
4334
  }
@@ -3736,8 +4351,9 @@ function provenanceOf(live) {
3736
4351
  sourceRef: m.sourceRef
3737
4352
  };
3738
4353
  }
3739
- function helmRenderReport(record, live) {
3740
- const base = { render: record.name, chart: record.chart };
4354
+ function helmRenderReport(record, live, observed) {
4355
+ const identity = observed ? { identity: renderIdentity(record, observedReleaseFor(record, observed, live?.manifest.namespace)) } : {};
4356
+ const base = { render: record.name, chart: record.chart, ...identity };
3741
4357
  if (!live) {
3742
4358
  return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
3743
4359
  }
@@ -3761,21 +4377,24 @@ function helmRenderReport(record, live) {
3761
4377
  }
3762
4378
  return { ...base, verdict: "in-sync", counts, provenance };
3763
4379
  }
3764
- async function readHelmRenderDrift(projectDir, env, opts = {}) {
4380
+ async function readHelmRenderDrift(projectDir, env, opts = {}, observed) {
3765
4381
  const report = await helmRenders(projectDir, opts).catch(() => void 0);
3766
4382
  const records = report?.records ?? [];
3767
4383
  if (records.length === 0) return [];
3768
4384
  return mapPool(records, estateReadPool(records.length), async (record) => {
3769
- if (!record.contentDigest) return helmRenderReport(record, void 0);
4385
+ if (!record.contentDigest) return helmRenderReport(record, void 0, observed);
3770
4386
  const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
3771
- return helmRenderReport(record, live);
4387
+ return helmRenderReport(record, live, observed);
3772
4388
  });
3773
4389
  }
3774
4390
  function chartNameOf2(node) {
3775
4391
  const name = node.attrs?.name;
3776
4392
  return typeof name === "string" && name.length > 0 ? name : void 0;
3777
4393
  }
3778
- var SEVERITY = { drifted: 2, unobserved: 1, "in-sync": 0 };
4394
+ var SEVERITY = { drifted: 3, unobserved: 1, "in-sync": 0 };
4395
+ function severityOf(r) {
4396
+ return Math.max(SEVERITY[r.verdict], r.identity?.match === "mismatch" ? 2 : 0);
4397
+ }
3779
4398
  function applyHelmRenderDrift(ir, reports) {
3780
4399
  if (reports.length === 0) return { charts: 0, drifted: 0 };
3781
4400
  const byChart = /* @__PURE__ */ new Map();
@@ -3791,11 +4410,11 @@ function applyHelmRenderDrift(ir, reports) {
3791
4410
  const chartName = chartNameOf2(node);
3792
4411
  const matched = chartName ? byChart.get(chartName) : void 0;
3793
4412
  if (!matched || matched.length === 0) continue;
3794
- const worst = matched.reduce((a, b) => SEVERITY[b.verdict] > SEVERITY[a.verdict] ? b : a);
4413
+ const worst = matched.reduce((a, b) => severityOf(b) > severityOf(a) ? b : a);
3795
4414
  const attrs = node.attrs ??= {};
3796
4415
  attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
3797
4416
  charts++;
3798
- if (worst.verdict === "drifted") {
4417
+ if (worst.verdict === "drifted" || matched.some((r) => r.identity?.match === "mismatch")) {
3799
4418
  attrs._status = "warn";
3800
4419
  drifted++;
3801
4420
  }
@@ -4334,8 +4953,8 @@ function projectHelmLogical(ir) {
4334
4953
  }
4335
4954
 
4336
4955
  // src/logical-kustomize.ts
4337
- import { existsSync as existsSync5 } from "node:fs";
4338
- import { join as join8 } from "node:path";
4956
+ import { existsSync as existsSync6 } from "node:fs";
4957
+ import { join as join11 } from "node:path";
4339
4958
  function overlayBoxTitle(name) {
4340
4959
  return `overlay ${name}`;
4341
4960
  }
@@ -4355,7 +4974,7 @@ function kustomizationRoot(bases, dir, exists) {
4355
4974
  let current = dir;
4356
4975
  for (let i = 0; i < 4 && current; i++) {
4357
4976
  for (const base of bases) {
4358
- if (exists(join8(base, current, "kustomization.yaml")) || exists(join8(base, current, "kustomization.yml"))) {
4977
+ if (exists(join11(base, current, "kustomization.yaml")) || exists(join11(base, current, "kustomization.yml"))) {
4359
4978
  return current;
4360
4979
  }
4361
4980
  }
@@ -4364,7 +4983,7 @@ function kustomizationRoot(bases, dir, exists) {
4364
4983
  }
4365
4984
  return void 0;
4366
4985
  }
4367
- function projectKustomizeLogical(ir, sourceRoots, exists = existsSync5) {
4986
+ function projectKustomizeLogical(ir, sourceRoots, exists = existsSync6) {
4368
4987
  const bases = Array.isArray(sourceRoots) ? sourceRoots : [sourceRoots];
4369
4988
  const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
4370
4989
  if (k8s.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
@@ -5086,11 +5705,11 @@ import {
5086
5705
  } from "@intentius/pinhole";
5087
5706
 
5088
5707
  // src/icon-packs.ts
5089
- import { readFileSync as readFileSync6 } from "node:fs";
5090
- import { dirname as dirname3, join as join9 } from "node:path";
5708
+ import { readFileSync as readFileSync8 } from "node:fs";
5709
+ import { dirname as dirname4, join as join12 } from "node:path";
5091
5710
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5092
- var ICON_ROOT = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "web", "icons");
5093
- var cache2 = /* @__PURE__ */ new Map();
5711
+ var ICON_ROOT = join12(dirname4(fileURLToPath2(import.meta.url)), "..", "web", "icons");
5712
+ var cache3 = /* @__PURE__ */ new Map();
5094
5713
  function unprefix(body) {
5095
5714
  return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
5096
5715
  }
@@ -5109,9 +5728,9 @@ function plate(glyph) {
5109
5728
  return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
5110
5729
  }
5111
5730
  function loadIcon(rel) {
5112
- const hit = cache2.get(rel);
5731
+ const hit = cache3.get(rel);
5113
5732
  if (hit) return hit;
5114
- const raw = readFileSync6(join9(ICON_ROOT, `${rel}.svg`), "utf8");
5733
+ const raw = readFileSync8(join12(ICON_ROOT, `${rel}.svg`), "utf8");
5115
5734
  const open = raw.indexOf("<svg");
5116
5735
  const openEnd = raw.indexOf(">", open);
5117
5736
  const close = raw.lastIndexOf("</svg");
@@ -5120,7 +5739,7 @@ function loadIcon(rel) {
5120
5739
  if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
5121
5740
  const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
5122
5741
  const glyph = PLATED.has(rel) ? plate(bare) : bare;
5123
- cache2.set(rel, glyph);
5742
+ cache3.set(rel, glyph);
5124
5743
  return glyph;
5125
5744
  }
5126
5745
  var K8S_ICON_BY_KIND = {
@@ -5176,9 +5795,9 @@ function helmIconFor(kind) {
5176
5795
  var SUPPORTED_MAJOR = 1;
5177
5796
  var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
5178
5797
  var HOW_TO_GET_ONE = "Generate one with `chant carve advise --from <terraform-dir> --report report.json` (chant's read-only Terraform peelability advisor), then `behold carve report.json`.";
5179
- var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5798
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5180
5799
  function parseCarveReport(value) {
5181
- if (!isRecord2(value)) {
5800
+ if (!isRecord3(value)) {
5182
5801
  return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
5183
5802
  }
5184
5803
  if (!Array.isArray(value.resources)) {
@@ -5188,7 +5807,7 @@ function parseCarveReport(value) {
5188
5807
  );
5189
5808
  }
5190
5809
  const bad = value.resources.findIndex(
5191
- (r) => !isRecord2(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
5810
+ (r) => !isRecord3(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
5192
5811
  );
5193
5812
  if (bad >= 0) {
5194
5813
  return refuse(
@@ -5253,364 +5872,116 @@ var directionOf = (e) => e.direction ?? "inbound";
5253
5872
  function boundaryEdgesOf(r) {
5254
5873
  const b = r.boundary;
5255
5874
  if (!b) return [];
5256
- const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
5257
- return list3.filter((e) => isRecord2(e) && typeof e.survivor === "string" && typeof e.carved === "string");
5258
- }
5259
- function scoreArithmetic(r) {
5260
- const b = r.breakdown;
5261
- if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
5262
- const p = b.penalties ?? {};
5263
- const terms = [];
5264
- if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
5265
- if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
5266
- if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
5267
- if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
5268
- if (p.dynamic) terms.push(`- 10 dynamic`);
5269
- if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
5270
- if (!terms.length) return `100 (no penalties) = ${r.score}`;
5271
- const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
5272
- const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
5273
- return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
5274
- }
5275
- function boundaryWorkOf(r) {
5276
- const b = r.breakdown;
5277
- if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
5278
- const parts = [];
5279
- if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
5280
- if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
5281
- if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
5282
- if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
5283
- if (b.hasDynamic) parts.push("count/for_each/data present");
5284
- if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
5285
- return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
5286
- }
5287
- function carveCardFields(node) {
5288
- const score = node.attrs.score;
5289
- const carve = node.attrs.carve;
5290
- if (typeof score !== "number") return void 0;
5291
- return [
5292
- { label: "score", value: String(score) },
5293
- ...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
5294
- ];
5295
- }
5296
- function carveReportToIr(report) {
5297
- const resources = report.resources ?? [];
5298
- const known = new Set(resources.map((r) => r.address));
5299
- const nodes = resources.map((r) => {
5300
- const status = statusForBand(r.band, r.score);
5301
- const b = r.breakdown ?? {};
5302
- const edges2 = boundaryEdgesOf(r);
5303
- const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
5304
- const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
5305
- return {
5306
- id: r.address,
5307
- kind: tfTypeOf(r.address, r.kind),
5308
- lexicon: "terraform",
5309
- attrs: {
5310
- // The drift palette's channel — `_`-prefixed, so it paints the card and
5311
- // stays out of both the card's fields and the inspect pane's list.
5312
- _status: status,
5313
- score: r.score,
5314
- carve: verdictForStatus(status),
5315
- band: r.band,
5316
- arithmetic: scoreArithmetic(r),
5317
- boundaryWork: boundaryWorkOf(r),
5318
- ...r.mapsTo ? { mapsTo: r.mapsTo } : {},
5319
- tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
5320
- inbound: b.inbound ?? 0,
5321
- outbound: b.outbound ?? 0,
5322
- outputs: b.outputs ?? 0,
5323
- instances: b.instances ?? 1,
5324
- dynamic: b.hasDynamic ?? false,
5325
- // The predicted diff, when the report carries the edge lists: who needs
5326
- // a `data` source the moment this is carved, and what becomes a
5327
- // deploy-time input. Absent (not empty) when the report has no lists —
5328
- // "none" and "not reported" are different claims.
5329
- ...patches.length ? { patchOnCarve: patches.join(", ") } : {},
5330
- ...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
5331
- }
5332
- };
5333
- });
5334
- const seen = /* @__PURE__ */ new Set();
5335
- const edges = [];
5336
- for (const r of resources) {
5337
- for (const e of boundaryEdgesOf(r)) {
5338
- const direction = directionOf(e);
5339
- const from = direction === "inbound" ? e.survivor : e.carved;
5340
- const to = direction === "inbound" ? e.carved : e.survivor;
5341
- if (!known.has(from) || !known.has(to)) continue;
5342
- const key = `${from}|${to}`;
5343
- if (seen.has(key)) continue;
5344
- seen.add(key);
5345
- edges.push({
5346
- from,
5347
- to,
5348
- kind: "ref",
5349
- viaAttr: direction,
5350
- ...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
5351
- });
5352
- }
5353
- }
5354
- const byStack = {};
5355
- for (const order of ["good", "warn", "neutral"]) {
5356
- const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
5357
- if (members.length) byStack[verdictForStatus(order)] = members;
5358
- }
5359
- return { nodes, edges, groups: { byStack } };
5360
- }
5361
- function carveNote(report, ir) {
5362
- const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
5363
- const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
5364
- if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
5365
- return `${head}. This report carries per-resource boundary COUNTS but no edge lists, so no boundary edges are drawn (chant#1636). Read-only: nothing is emitted, patched, or applied.`;
5366
- }
5367
-
5368
- // src/carve-manifest.ts
5369
- import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
5370
- import { dirname as dirname4, join as join10 } from "node:path";
5371
- var CARVE_MANIFEST_SUFFIX = ".carve.json";
5372
- var APPLY_IS_HUMAN = "`chant carve apply` graduates ownership \u2014 it resolves the marker that makes chant the owner of a live resource. behold shows what the manifest records and echoes the command; it has no apply endpoint and no apply button, by design. Read the graduation plan, then run it yourself.";
5373
- var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5374
- var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
5375
- function parseCarveManifest(value) {
5376
- if (!isRecord3(value)) return null;
5377
- if (value.version !== 1) return null;
5378
- if (typeof value.target !== "string" || !value.target) return null;
5379
- return value;
5380
- }
5381
- function readCarveManifest(path, readFile = (p) => readFileSync7(p, "utf8")) {
5382
- let text;
5383
- try {
5384
- text = readFile(path);
5385
- } catch {
5386
- return null;
5387
- }
5388
- try {
5389
- return parseCarveManifest(JSON.parse(text));
5390
- } catch {
5391
- return null;
5392
- }
5393
- }
5394
- function carveStageOf(m) {
5395
- if (m.apply) return "applied";
5396
- if (m.bridge) return "bridged";
5397
- return "emitted";
5398
- }
5399
- function carveWordFor(stage) {
5400
- switch (stage) {
5401
- case "applied":
5402
- return "carved \u2192 chant";
5403
- case "bridged":
5404
- return "bridged \u2014 apply is yours";
5405
- default:
5406
- return "emitted \u2014 not bridged";
5407
- }
5408
- }
5409
- function carveToneFor(stage) {
5410
- return stage === "applied" ? "good" : "accent";
5411
- }
5412
- function carveStageNote(state) {
5413
- switch (state.stage) {
5414
- case "applied":
5415
- return `graduated \u2014 \`carve apply\` recorded the ownership marker` + (state.ownership?.stack ? ` (stack ${state.ownership.stack}${state.ownership.env ? `, env ${state.ownership.env}` : ""})` : "") + ". chant owns it.";
5416
- case "bridged":
5417
- return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
5418
- default:
5419
- return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
5420
- }
5421
- }
5422
- function carveStateOf(m, path) {
5423
- const stage = carveStageOf(m);
5424
- const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
5425
- return {
5426
- target: m.target,
5427
- ...m.tfType ? { tfType: m.tfType } : {},
5428
- stage,
5429
- graduated: stage === "applied",
5430
- // The last stamped step wins — that is the age of the state, not the age
5431
- // of the carve.
5432
- ...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
5433
- files: strings(m.emit?.files),
5434
- excised: strings(m.bridge?.excised),
5435
- deferredInputs: deferred,
5436
- ...m.apply ? {
5437
- ownership: {
5438
- ...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
5439
- ...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
5440
- ...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
5441
- }
5442
- } : {},
5443
- ...m.from ? { from: m.from } : {},
5444
- outDir: dirname4(path),
5445
- path
5446
- };
5447
- }
5448
- var nodeCarveIo = {
5449
- readdir: (dir) => readdirSync4(dir),
5450
- isDirectory: (p) => {
5451
- try {
5452
- return statSync4(p).isDirectory();
5453
- } catch {
5454
- return false;
5455
- }
5456
- },
5457
- readFile: (p) => readFileSync7(p, "utf8")
5458
- };
5459
- var MANIFEST_SCAN_DEPTH = 2;
5460
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
5461
- function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
5462
- const out = [];
5463
- const walk = (d, level) => {
5464
- let entries;
5465
- try {
5466
- entries = io.readdir(d).sort();
5467
- } catch {
5468
- return;
5469
- }
5470
- for (const name of entries) {
5471
- const full = join10(d, name);
5472
- if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
5473
- out.push(full);
5474
- continue;
5475
- }
5476
- if (level >= depth) continue;
5477
- if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
5478
- if (io.isDirectory(full)) walk(full, level + 1);
5479
- }
5480
- };
5481
- walk(dir, 0);
5482
- return out;
5875
+ const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
5876
+ return list3.filter((e) => isRecord3(e) && typeof e.survivor === "string" && typeof e.carved === "string");
5483
5877
  }
5484
- function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
5485
- const rank = { emitted: 0, bridged: 1, applied: 2 };
5486
- const byTarget = /* @__PURE__ */ new Map();
5487
- for (const dir of dirs) {
5488
- for (const path of listCarveManifests(dir, io, depth)) {
5489
- const manifest = readCarveManifest(path, io.readFile);
5490
- if (!manifest) continue;
5491
- const state = carveStateOf(manifest, path);
5492
- const held = byTarget.get(state.target);
5493
- if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
5494
- }
5495
- }
5496
- return byTarget;
5878
+ function scoreArithmetic(r) {
5879
+ const b = r.breakdown;
5880
+ if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
5881
+ const p = b.penalties ?? {};
5882
+ const terms = [];
5883
+ if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
5884
+ if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
5885
+ if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
5886
+ if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
5887
+ if (p.dynamic) terms.push(`- 10 dynamic`);
5888
+ if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
5889
+ if (!terms.length) return `100 (no penalties) = ${r.score}`;
5890
+ const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
5891
+ const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
5892
+ return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
5497
5893
  }
5498
- function carveProgress(states, total) {
5499
- let applied = 0;
5500
- let bridged = 0;
5501
- let emitted = 0;
5502
- for (const s of states) {
5503
- if (s.stage === "applied") applied++;
5504
- else if (s.stage === "bridged") bridged++;
5505
- else emitted++;
5506
- }
5507
- const inFlight = bridged + emitted;
5894
+ function boundaryWorkOf(r) {
5895
+ const b = r.breakdown;
5896
+ if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
5508
5897
  const parts = [];
5509
- if (emitted) parts.push(`${emitted} emitted`);
5510
- if (bridged) parts.push(`${bridged} bridged`);
5511
- return {
5512
- applied,
5513
- bridged,
5514
- emitted,
5515
- inFlight,
5516
- total: typeof total === "number" ? total : null,
5517
- label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
5518
- detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
5519
- };
5520
- }
5521
- function applyCommandFor(state, labels = {}) {
5522
- const env = state.ownership?.env ?? "<env>";
5523
- const stack = state.ownership?.stack ?? "<stack>";
5524
- const from = labels.from ?? state.from ?? "<terraform-dir>";
5525
- const out = labels.out ?? state.outDir;
5526
- return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
5898
+ if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
5899
+ if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
5900
+ if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
5901
+ if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
5902
+ if (b.hasDynamic) parts.push("count/for_each/data present");
5903
+ if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
5904
+ return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
5527
5905
  }
5528
- function carveStatePayload(states, total, labels = () => ({})) {
5529
- const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
5530
- return {
5531
- manifests: list3.length,
5532
- progress: carveProgress(list3, total),
5533
- states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
5534
- apply: { human: true, note: APPLY_IS_HUMAN }
5535
- };
5906
+ function carveCardFields(node) {
5907
+ const score = node.attrs.score;
5908
+ const carve = node.attrs.carve;
5909
+ if (typeof score !== "number") return void 0;
5910
+ return [
5911
+ { label: "score", value: String(score) },
5912
+ ...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
5913
+ ];
5536
5914
  }
5537
- function splitCarveState(tfIr, states, opts = {}) {
5538
- if (states.size === 0) return { tf: tfIr, graduated: [] };
5539
- const shorten = opts.shorten ?? ((p) => p);
5540
- const source = (files) => files.map(shorten).join(", ");
5541
- const gone = /* @__PURE__ */ new Set();
5542
- const graduated = [];
5543
- const nodes = [];
5544
- for (const node of tfIr.nodes) {
5545
- const state = states.get(node.id);
5546
- if (!state) {
5547
- nodes.push(node);
5548
- continue;
5549
- }
5550
- if (state.graduated) {
5551
- gone.add(node.id);
5552
- graduated.push({
5553
- ...node,
5554
- attrs: {
5555
- ...node.attrs,
5556
- _status: "good",
5557
- carve: carveWordFor(state.stage),
5558
- carveState: state.stage,
5559
- ...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
5560
- ...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
5561
- ...state.at ? { carvedAt: state.at } : {},
5562
- ...state.files.length ? { chantSource: source(state.files) } : {}
5563
- }
5564
- });
5565
- continue;
5566
- }
5567
- nodes.push({
5568
- ...node,
5915
+ function carveReportToIr(report) {
5916
+ const resources = report.resources ?? [];
5917
+ const known = new Set(resources.map((r) => r.address));
5918
+ const nodes = resources.map((r) => {
5919
+ const status = statusForBand(r.band, r.score);
5920
+ const b = r.breakdown ?? {};
5921
+ const edges2 = boundaryEdgesOf(r);
5922
+ const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
5923
+ const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
5924
+ return {
5925
+ id: r.address,
5926
+ kind: tfTypeOf(r.address, r.kind),
5927
+ lexicon: "terraform",
5569
5928
  attrs: {
5570
- ...node.attrs,
5571
- _status: carveToneFor(state.stage),
5572
- carve: carveWordFor(state.stage),
5573
- carveState: state.stage,
5574
- ...state.at ? { carvedAt: state.at } : {},
5575
- ...state.files.length ? { chantSource: source(state.files) } : {},
5576
- ...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
5577
- ...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
5929
+ // The drift palette's channel — `_`-prefixed, so it paints the card and
5930
+ // stays out of both the card's fields and the inspect pane's list.
5931
+ _status: status,
5932
+ score: r.score,
5933
+ carve: verdictForStatus(status),
5934
+ band: r.band,
5935
+ arithmetic: scoreArithmetic(r),
5936
+ boundaryWork: boundaryWorkOf(r),
5937
+ ...r.mapsTo ? { mapsTo: r.mapsTo } : {},
5938
+ tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
5939
+ inbound: b.inbound ?? 0,
5940
+ outbound: b.outbound ?? 0,
5941
+ outputs: b.outputs ?? 0,
5942
+ instances: b.instances ?? 1,
5943
+ dynamic: b.hasDynamic ?? false,
5944
+ // The predicted diff, when the report carries the edge lists: who needs
5945
+ // a `data` source the moment this is carved, and what becomes a
5946
+ // deploy-time input. Absent (not empty) when the report has no lists —
5947
+ // "none" and "not reported" are different claims.
5948
+ ...patches.length ? { patchOnCarve: patches.join(", ") } : {},
5949
+ ...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
5578
5950
  }
5579
- });
5951
+ };
5952
+ });
5953
+ const seen = /* @__PURE__ */ new Set();
5954
+ const edges = [];
5955
+ for (const r of resources) {
5956
+ for (const e of boundaryEdgesOf(r)) {
5957
+ const direction = directionOf(e);
5958
+ const from = direction === "inbound" ? e.survivor : e.carved;
5959
+ const to = direction === "inbound" ? e.carved : e.survivor;
5960
+ if (!known.has(from) || !known.has(to)) continue;
5961
+ const key = `${from}|${to}`;
5962
+ if (seen.has(key)) continue;
5963
+ seen.add(key);
5964
+ edges.push({
5965
+ from,
5966
+ to,
5967
+ kind: "ref",
5968
+ viaAttr: direction,
5969
+ ...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
5970
+ });
5971
+ }
5580
5972
  }
5581
5973
  const byStack = {};
5582
- for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
5583
- const left = members.filter((id) => !gone.has(id));
5584
- if (left.length) byStack[band] = left;
5974
+ for (const order of ["good", "warn", "neutral"]) {
5975
+ const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
5976
+ if (members.length) byStack[verdictForStatus(order)] = members;
5585
5977
  }
5586
- return {
5587
- tf: {
5588
- nodes,
5589
- edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
5590
- groups: { ...tfIr.groups, byStack }
5591
- },
5592
- graduated
5593
- };
5594
- }
5595
- var GRADUATED_BAND = "carved \u2192 chant";
5596
- function bandGraduated(tf, graduated) {
5597
- if (graduated.length === 0) return tf;
5598
- return {
5599
- nodes: [...graduated, ...tf.nodes],
5600
- edges: tf.edges,
5601
- groups: {
5602
- ...tf.groups,
5603
- byStack: {
5604
- [GRADUATED_BAND]: graduated.map((n) => n.id),
5605
- ...tf.groups?.byStack ?? {}
5606
- }
5607
- }
5608
- };
5978
+ return { nodes, edges, groups: { byStack } };
5609
5979
  }
5610
- function carveStateNote(progress, states) {
5611
- if (states.size === 0) return "";
5612
- const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
5613
- return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
5980
+ function carveNote(report, ir) {
5981
+ const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
5982
+ const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
5983
+ if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
5984
+ return `${head}. This report carries per-resource boundary COUNTS but no edge lists, so no boundary edges are drawn (chant#1636). Read-only: nothing is emitted, patched, or applied.`;
5614
5985
  }
5615
5986
 
5616
5987
  // src/ops-lens.ts
@@ -5884,9 +6255,12 @@ function nodeFor(op, placed, guards) {
5884
6255
  ...profile ? { profile } : {},
5885
6256
  ...policy ? { policy } : {},
5886
6257
  // The resolved args, verbatim from the IR. They carry the Op's SCOPE
5887
- // (env, stack, template path) and never an estate entity id — see the
5888
- // module doc on why no edge is drawn from them.
6258
+ // (env, stack, template path); which of them identify an ENTITY is the
6259
+ // contract's to say, and 0.54's op.json says it in `entities` below.
5889
6260
  ...args ? { args } : {},
6261
+ // chant#2022: what the step touches, verbatim. `entities` prints on the
6262
+ // card; `_entities` is what `joinStepEntities` resolves.
6263
+ ...step.entities?.length ? { entities: step.entities.join(", "), _entities: step.entities } : {},
5890
6264
  ...step.outcomeAttribute ? { outcome: step.outcomeAttribute.from ? `${step.outcomeAttribute.name} \u2190 ${step.outcomeAttribute.from}` : step.outcomeAttribute.name } : {},
5891
6265
  ...op.activityContracts?.[step.fn] ? { contract: "declared (args/returns JSON Schema in op.json)" } : {}
5892
6266
  }
@@ -5988,17 +6362,83 @@ function opCardFields(node) {
5988
6362
  function opsNote(ops, ir) {
5989
6363
  const gates = ir.nodes.filter((n) => n.attrs._step === "gate").length;
5990
6364
  const rules = ir.nodes.filter((n) => n.attrs._step === "rule");
5991
- const steps = ir.nodes.length - rules.length;
6365
+ const estate = ir.nodes.filter((n) => n.lexicon !== "op").length;
6366
+ const steps = ir.nodes.length - rules.length - estate;
5992
6367
  const dangling = rules.filter((n) => n.attrs.dangling !== void 0).length;
5993
6368
  const counts = [
5994
6369
  `${ops.length} declared Op${ops.length === 1 ? "" : "s"}`,
5995
6370
  `${steps} step${steps === 1 ? "" : "s"}`,
5996
6371
  ...gates ? [`${gates} gate${gates === 1 ? "" : "s"}`] : [],
5997
- ...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : []
6372
+ ...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : [],
6373
+ ...entityCountClause(ir)
5998
6374
  ].join(" \xB7 ");
5999
6375
  const ruleNote = rules.length ? ` Each converge rule carries the why chant requires of it, verbatim, and a rule whose then is run(<op>) is drawn as an edge to that Op's first step.` + (dangling ? ` ${dangling} rule${dangling === 1 ? " names an Op" : "s name Ops"} not in this set \u2014 stated on the card, not drawn.` : "") : "";
6000
6376
  return `${counts}, from each Op's emitted dist/ops/<name>/op.json.${ruleNote} No estate cross-links: op.json names an Op's env and stack but never an estate entity id, so nothing here is joined to the graph's nodes (chant ask). Read-only: this renders the declared Op \u2014 nothing is started, signalled, or applied.`;
6001
6377
  }
6378
+ var ESTATE_BOX = "estate";
6379
+ function joinStepEntities(ir, estate) {
6380
+ const steps = ir.nodes.filter((n) => Array.isArray(n.attrs._entities));
6381
+ if (steps.length === 0) return { refs: 0, resolved: 0 };
6382
+ const byId = /* @__PURE__ */ new Map();
6383
+ const byLeaf = /* @__PURE__ */ new Map();
6384
+ for (const n of estate?.nodes ?? []) {
6385
+ byId.set(n.id, n);
6386
+ for (const leaf of stringLeaves(n.attrs)) {
6387
+ const list3 = byLeaf.get(leaf.value);
6388
+ if (list3) {
6389
+ if (!list3.includes(n)) list3.push(n);
6390
+ } else byLeaf.set(leaf.value, [n]);
6391
+ }
6392
+ }
6393
+ const present = new Set(ir.nodes.map((n) => n.id));
6394
+ let refs = 0;
6395
+ let resolvedCount = 0;
6396
+ for (const step of steps) {
6397
+ const links = { resolved: [], unresolved: [] };
6398
+ for (const value of step.attrs._entities) {
6399
+ refs++;
6400
+ const targets = [];
6401
+ const idHit = byId.get(value);
6402
+ if (idHit) targets.push({ node: idHit, via: "id" });
6403
+ for (const n of byLeaf.get(value) ?? []) if (n !== idHit) targets.push({ node: n, via: "attr" });
6404
+ if (targets.length === 0) {
6405
+ links.unresolved.push(value);
6406
+ continue;
6407
+ }
6408
+ resolvedCount++;
6409
+ for (const { node, via } of targets) {
6410
+ links.resolved.push({ value, node: node.id, via });
6411
+ if (!present.has(node.id)) {
6412
+ present.add(node.id);
6413
+ ir.nodes.push({ ...node, attrs: { ...node.attrs, _touchedBy: [step.id] } });
6414
+ (ir.groups.byStack ??= {})[ESTATE_BOX] = [...ir.groups.byStack[ESTATE_BOX] ?? [], node.id];
6415
+ } else {
6416
+ const held = ir.nodes.find((n) => n.id === node.id);
6417
+ const touched = held.attrs._touchedBy ?? [];
6418
+ if (!touched.includes(step.id)) held.attrs._touchedBy = [...touched, step.id];
6419
+ }
6420
+ ir.edges.push({ from: step.id, to: node.id, kind: "ref", viaAttr: "entities" });
6421
+ }
6422
+ }
6423
+ step.attrs._entityLinks = links;
6424
+ }
6425
+ return { refs, resolved: resolvedCount };
6426
+ }
6427
+ function entityCountClause(ir) {
6428
+ let refs = 0;
6429
+ let resolved = 0;
6430
+ for (const n of ir.nodes) {
6431
+ const links = n.attrs._entityLinks;
6432
+ if (links) {
6433
+ refs += links.resolved.length + links.unresolved.length;
6434
+ resolved += links.resolved.length;
6435
+ } else if (Array.isArray(n.attrs._entities)) {
6436
+ refs += n.attrs._entities.length;
6437
+ }
6438
+ }
6439
+ if (refs === 0) return [];
6440
+ return [`${refs} entity ref${refs === 1 ? "" : "s"}, ${resolved} linked`];
6441
+ }
6002
6442
 
6003
6443
  // src/render.ts
6004
6444
  registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
@@ -6112,7 +6552,10 @@ function renderCarveEstate(tfIr, appIr, opts) {
6112
6552
  }
6113
6553
  function withCarveState(tfIr, appIr, opts) {
6114
6554
  const states = opts.carved ?? /* @__PURE__ */ new Map();
6115
- const { tf, graduated } = splitCarveState(tfIr, states, { ...opts.shorten ? { shorten: opts.shorten } : {} });
6555
+ const { tf, graduated } = splitCarveState(tfIr, states, {
6556
+ ...opts.shorten ? { shorten: opts.shorten } : {},
6557
+ ...opts.emitted ? { emitted: opts.emitted } : {}
6558
+ });
6116
6559
  const ns = namespaceAppIr(appIr, opts.appTitle);
6117
6560
  const progress = carveProgress(states.values(), tfIr.nodes.length);
6118
6561
  return {
@@ -6452,12 +6895,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
6452
6895
  }
6453
6896
 
6454
6897
  // src/carve-actions.ts
6455
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
6456
- import { isAbsolute, join as join13, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
6898
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync6 } from "node:fs";
6899
+ import { isAbsolute as isAbsolute3, join as join15, relative as relative3, resolve as resolve7, sep as sep4 } from "node:path";
6457
6900
 
6458
6901
  // src/layout.ts
6459
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
6460
- import { join as join11 } from "node:path";
6902
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3, accessSync, constants } from "node:fs";
6903
+ import { join as join13 } from "node:path";
6461
6904
  var LAYOUT_DIR = ".behold";
6462
6905
  var LAYOUT_FILE = "layout.json";
6463
6906
  var MAX_BODY_BYTES = 64 * 1024;
@@ -6467,12 +6910,12 @@ var MAX_LENSES = 64;
6467
6910
  var MAX_ID_LENGTH = 512;
6468
6911
  var MAX_LENS_LENGTH = 128;
6469
6912
  var NUM = ["dx", "dy", "dw", "dh"];
6470
- function slug(s) {
6913
+ function slug2(s) {
6471
6914
  return String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
6472
6915
  }
6473
6916
  function normalizeLens(raw) {
6474
6917
  if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_LENS_LENGTH) return null;
6475
- const parts = raw.split("+").map((p) => slug(p)).filter((p) => p && p !== "unknown");
6918
+ const parts = raw.split("+").map((p) => slug2(p)).filter((p) => p && p !== "unknown");
6476
6919
  if (!parts.length) return null;
6477
6920
  const key = parts.join("+");
6478
6921
  return key === "__proto__" || key === "constructor" || key === "prototype" ? null : key;
@@ -6495,13 +6938,13 @@ function normalizeDeltas(raw) {
6495
6938
  function lensFromQuery(params) {
6496
6939
  const zoom = params.get("components") === "1" ? "components" : params.get("logical") === "1" ? "logical" : params.get("runtime") === "1" ? "runtime" : { 1: "composites", 2: "resources", 3: "attributes" }[Number(params.get("detail") ?? 2)] ?? "resources";
6497
6940
  const stack = params.get("stack");
6498
- return [slug(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug(`stack-${stack}`) : null].filter(Boolean).join("+");
6941
+ return [slug2(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug2(`stack-${stack}`) : null].filter(Boolean).join("+");
6499
6942
  }
6500
6943
  function layoutPath(projectDir) {
6501
- return join11(projectDir, LAYOUT_DIR, LAYOUT_FILE);
6944
+ return join13(projectDir, LAYOUT_DIR, LAYOUT_FILE);
6502
6945
  }
6503
6946
  function unwritableReason(projectDir) {
6504
- if (!projectDir || !existsSync6(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
6947
+ if (!projectDir || !existsSync7(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
6505
6948
  try {
6506
6949
  accessSync(projectDir, constants.W_OK);
6507
6950
  } catch {
@@ -6513,8 +6956,8 @@ function readLayoutFile(projectDir) {
6513
6956
  const empty = { version: 1, lenses: {} };
6514
6957
  const file = layoutPath(projectDir);
6515
6958
  try {
6516
- if (!existsSync6(file)) return empty;
6517
- const raw = readFileSync8(file, "utf8");
6959
+ if (!existsSync7(file)) return empty;
6960
+ const raw = readFileSync9(file, "utf8");
6518
6961
  if (raw.length > MAX_FILE_BYTES) return empty;
6519
6962
  const parsed = JSON.parse(raw);
6520
6963
  if (!parsed || typeof parsed !== "object" || !parsed.lenses || typeof parsed.lenses !== "object") return empty;
@@ -6549,14 +6992,14 @@ function writeLens(projectDir, lens, deltas) {
6549
6992
  if (Object.keys(lenses).length > MAX_LENSES) throw new LayoutTooLarge(`a layout sidecar holds at most ${MAX_LENSES} lenses`);
6550
6993
  const file = layoutPath(projectDir);
6551
6994
  if (!Object.keys(lenses).length) {
6552
- rmSync(file, { force: true });
6995
+ rmSync2(file, { force: true });
6553
6996
  return { lens: key, deltas: clean, bytes: 0 };
6554
6997
  }
6555
6998
  const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
6556
6999
  if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
6557
- mkdirSync2(join11(projectDir, LAYOUT_DIR), { recursive: true });
7000
+ mkdirSync3(join13(projectDir, LAYOUT_DIR), { recursive: true });
6558
7001
  const tmp = `${file}.tmp`;
6559
- writeFileSync2(tmp, body, "utf8");
7002
+ writeFileSync3(tmp, body, "utf8");
6560
7003
  renameSync(tmp, file);
6561
7004
  return { lens: key, deltas: clean, bytes: body.length };
6562
7005
  }
@@ -6639,8 +7082,8 @@ function reanchorEdges(svg, moved) {
6639
7082
 
6640
7083
  // src/carve-live.ts
6641
7084
  import { spawn as spawn3 } from "node:child_process";
6642
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
6643
- import { join as join12 } from "node:path";
7085
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "node:fs";
7086
+ import { join as join14 } from "node:path";
6644
7087
 
6645
7088
  // src/scratch.ts
6646
7089
  var PROTECTED_INFRA = [
@@ -6728,9 +7171,9 @@ async function teardownScratchFloci() {
6728
7171
  await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
6729
7172
  }
6730
7173
  async function applyIntoFloci(fromDir, step) {
6731
- const template = join12(fromDir, "floci-override.tf.disabled");
6732
- if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
6733
- writeFileSync3(join12(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
7174
+ const template = join14(fromDir, "floci-override.tf.disabled");
7175
+ if (!existsSync8(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
7176
+ writeFileSync4(join14(fromDir, "floci_override.tf"), armOverride(readFileSync10(template, "utf8"), LIVE_PORT));
6734
7177
  const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
6735
7178
  if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
6736
7179
  const apply = await step(
@@ -6780,7 +7223,7 @@ async function runCarveObserve(demo, select) {
6780
7223
  "run `behold demo carve --live` (needs docker + terraform)"
6781
7224
  );
6782
7225
  }
6783
- if (!existsSync8(join13(demo.out, "src"))) {
7226
+ if (!existsSync9(join15(demo.out, "src"))) {
6784
7227
  return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
6785
7228
  }
6786
7229
  const args = ["lifecycle", "diff", "prod", "--live", "--json"];
@@ -6850,8 +7293,8 @@ var refuse3 = (code, error, remedy) => ({
6850
7293
  });
6851
7294
  var BUILD_CAVEAT = "The gate shown is `chant lint`, not `chant build`. chant#1637's fold is applied now: the bucket's versioning and public-access-block sub-resources come out as native props, not just folded-in mentions. `chant build` still fails, but on one rule only \u2014 WAW042, a TLS-deny bucket policy the source Terraform never declared. That's real drift the estate inherited, not a gap in the carve. example-carve/README.md, \"Why build still fails\", has the full statement.";
6852
7295
  function insideDemo(root, p) {
6853
- const rel = relative2(resolve4(root), resolve4(p));
6854
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
7296
+ const rel = relative3(resolve7(root), resolve7(p));
7297
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
6855
7298
  }
6856
7299
  function selectFromReport(report, raw) {
6857
7300
  if (typeof raw !== "string" || !raw || raw.length > 512) return null;
@@ -6861,7 +7304,7 @@ function carveWriteBlock(demo) {
6861
7304
  if (!demo) {
6862
7305
  return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
6863
7306
  }
6864
- if (!existsSync8(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
7307
+ if (!existsSync9(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
6865
7308
  if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
6866
7309
  return unwritableReason(demo.root);
6867
7310
  }
@@ -6871,17 +7314,17 @@ function readArtifacts(demo, dir, filter) {
6871
7314
  if (out.length >= MAX_ARTIFACTS || depth > 2) return;
6872
7315
  let entries;
6873
7316
  try {
6874
- entries = readdirSync5(d).sort();
7317
+ entries = readdirSync6(d).sort();
6875
7318
  } catch {
6876
7319
  return;
6877
7320
  }
6878
7321
  for (const name of entries) {
6879
7322
  if (out.length >= MAX_ARTIFACTS) return;
6880
7323
  if (name === "node_modules" || name.startsWith(".")) continue;
6881
- const full = join13(d, name);
7324
+ const full = join15(d, name);
6882
7325
  let st;
6883
7326
  try {
6884
- st = statSync5(full);
7327
+ st = statSync6(full);
6885
7328
  } catch {
6886
7329
  continue;
6887
7330
  }
@@ -6889,11 +7332,11 @@ function readArtifacts(demo, dir, filter) {
6889
7332
  walk(full, depth + 1);
6890
7333
  continue;
6891
7334
  }
6892
- const rel = relative2(demo.root, full).split(sep2).join("/");
7335
+ const rel = relative3(demo.root, full).split(sep4).join("/");
6893
7336
  if (!filter(rel)) continue;
6894
7337
  let text = "";
6895
7338
  try {
6896
- text = readFileSync10(full, "utf8");
7339
+ text = readFileSync11(full, "utf8");
6897
7340
  } catch {
6898
7341
  continue;
6899
7342
  }
@@ -6915,17 +7358,17 @@ function carveSlug(select) {
6915
7358
  }
6916
7359
  var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
6917
7360
  function shortenIn(arg, root) {
6918
- return arg.startsWith(root + sep2) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
7361
+ return arg.startsWith(root + sep4) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
6919
7362
  }
6920
7363
  function merge(r, root) {
6921
7364
  const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
6922
- return clip(joined.split(root + sep2).join("").split(root).join("."));
7365
+ return clip(joined.split(root + sep4).join("").split(root).join("."));
6923
7366
  }
6924
7367
  async function runCarveEmit(demo, select) {
6925
7368
  const block = carveWriteBlock(demo);
6926
7369
  if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6927
- mkdirSync3(demo.out, { recursive: true });
6928
- const reportFile = join13(demo.out, `${carveSlug(select)}-boundary.json`);
7370
+ mkdirSync4(demo.out, { recursive: true });
7371
+ const reportFile = join15(demo.out, `${carveSlug(select)}-boundary.json`);
6929
7372
  const args = [
6930
7373
  "carve",
6931
7374
  "emit",
@@ -6954,10 +7397,10 @@ async function runCarveEmit(demo, select) {
6954
7397
  }
6955
7398
  let boundary = null;
6956
7399
  try {
6957
- boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
7400
+ boundary = JSON.parse(readFileSync11(reportFile, "utf8"));
6958
7401
  } catch {
6959
7402
  }
6960
- const lintPath = relative2(demo.project, join13(demo.out, "src")).split(sep2).join("/");
7403
+ const lintPath = relative3(demo.project, join15(demo.out, "src")).split(sep4).join("/");
6961
7404
  const lintArgs = ["lint", lintPath];
6962
7405
  const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
6963
7406
  code: 127,
@@ -6982,7 +7425,7 @@ async function runCarveEmit(demo, select) {
6982
7425
  async function runCarveBridge(demo, select) {
6983
7426
  const block = carveWriteBlock(demo);
6984
7427
  if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6985
- mkdirSync3(demo.out, { recursive: true });
7428
+ mkdirSync4(demo.out, { recursive: true });
6986
7429
  const args = [
6987
7430
  "carve",
6988
7431
  "bridge",
@@ -7007,21 +7450,21 @@ async function runCarveBridge(demo, select) {
7007
7450
  "Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
7008
7451
  );
7009
7452
  }
7010
- const slug3 = carveSlug(select);
7453
+ const slug4 = carveSlug(select);
7011
7454
  const all = readArtifacts(demo, demo.out, (rel) => /\.(tf|md|patch)$/.test(rel));
7012
7455
  return {
7013
7456
  ok: true,
7014
7457
  select,
7015
7458
  command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
7016
7459
  output,
7017
- runbook: all.find((a) => a.path.endsWith(`${slug3}-runbook.md`)) ?? null,
7018
- proposals: all.filter((a) => !a.path.endsWith(`${slug3}-runbook.md`))
7460
+ runbook: all.find((a) => a.path.endsWith(`${slug4}-runbook.md`)) ?? null,
7461
+ proposals: all.filter((a) => !a.path.endsWith(`${slug4}-runbook.md`))
7019
7462
  };
7020
7463
  }
7021
7464
 
7022
7465
  // src/ops.ts
7023
- import { readdirSync as readdirSync6, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
7024
- import { join as join14 } from "node:path";
7466
+ import { readdirSync as readdirSync7, readFileSync as readFileSync12, existsSync as existsSync10 } from "node:fs";
7467
+ import { join as join16 } from "node:path";
7025
7468
  var APPLY_TARGET_LEXICON = {
7026
7469
  cloudformation: "aws",
7027
7470
  kubectl: "k8s",
@@ -7043,11 +7486,11 @@ function discoverOps(projectDir) {
7043
7486
  const seen = /* @__PURE__ */ new Set();
7044
7487
  const out = [];
7045
7488
  for (const sub of ["ops", "src", "."]) {
7046
- const dir = join14(projectDir, sub);
7047
- if (!existsSync9(dir)) continue;
7048
- for (const f of readdirSync6(dir)) {
7489
+ const dir = join16(projectDir, sub);
7490
+ if (!existsSync10(dir)) continue;
7491
+ for (const f of readdirSync7(dir)) {
7049
7492
  if (!f.endsWith(".op.ts")) continue;
7050
- const content = readFileSync11(join14(dir, f), "utf8");
7493
+ const content = readFileSync12(join16(dir, f), "utf8");
7051
7494
  const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
7052
7495
  if (!name || seen.has(name)) continue;
7053
7496
  seen.add(name);
@@ -7063,25 +7506,25 @@ function discoverOps(projectDir) {
7063
7506
  ...gate ? { gate } : {},
7064
7507
  ...env ? { env } : {},
7065
7508
  ...substrate ? { substrate } : {},
7066
- ...existsSync9(irPath) ? { irPath } : {}
7509
+ ...existsSync10(irPath) ? { irPath } : {}
7067
7510
  });
7068
7511
  }
7069
7512
  }
7070
7513
  return out.sort((a, b) => a.name.localeCompare(b.name));
7071
7514
  }
7072
7515
  function opIrPath(projectDir, name) {
7073
- return join14(projectDir, "dist", "ops", name, "op.json");
7516
+ return join16(projectDir, "dist", "ops", name, "op.json");
7074
7517
  }
7075
7518
  function discoverOpIrs(projectDirs) {
7076
7519
  const seen = /* @__PURE__ */ new Set();
7077
7520
  const out = [];
7078
7521
  for (const dir of projectDirs) {
7079
- const root = join14(dir, "dist", "ops");
7080
- if (!existsSync9(root)) continue;
7081
- for (const name of readdirSync6(root)) {
7522
+ const root = join16(dir, "dist", "ops");
7523
+ if (!existsSync10(root)) continue;
7524
+ for (const name of readdirSync7(root)) {
7082
7525
  if (seen.has(name)) continue;
7083
7526
  const path = opIrPath(dir, name);
7084
- if (!existsSync9(path)) continue;
7527
+ if (!existsSync10(path)) continue;
7085
7528
  seen.add(name);
7086
7529
  out.push({ name, dir, path });
7087
7530
  }
@@ -7316,11 +7759,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
7316
7759
  function tookLabel(ms) {
7317
7760
  return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
7318
7761
  }
7319
- function paintPlayhead(ir, join21, gate) {
7320
- if (!join21.byNode.size && !join21.at) return ir;
7762
+ function paintPlayhead(ir, join23, gate) {
7763
+ if (!join23.byNode.size && !join23.at) return ir;
7321
7764
  const nodes = ir.nodes.map((node) => {
7322
- const run3 = join21.byNode.get(node.id);
7323
- const here = join21.at === node.id;
7765
+ const run3 = join23.byNode.get(node.id);
7766
+ const here = join23.at === node.id;
7324
7767
  if (!run3 && !here) return node;
7325
7768
  const isGate = node.attrs?._step === "gate";
7326
7769
  const attrs = { ...node.attrs };
@@ -7335,7 +7778,7 @@ function paintPlayhead(ir, join21, gate) {
7335
7778
  if (here) {
7336
7779
  attrs._playhead = true;
7337
7780
  attrs._status = isGate && gate ? "warn" : "accent";
7338
- attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join21.atReason ?? "here";
7781
+ attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join23.atReason ?? "here";
7339
7782
  if (isGate && gate?.description) attrs.gateDescription = gate.description;
7340
7783
  }
7341
7784
  return { ...node, attrs };
@@ -7357,11 +7800,11 @@ function pendingGateCard(op, gate, ir) {
7357
7800
  }
7358
7801
  };
7359
7802
  }
7360
- function playheadNote(state, join21) {
7803
+ function playheadNote(state, join23) {
7361
7804
  if (!state.op) {
7362
7805
  return "No run state: nothing has been run from this behold session, so every step paints as declared.";
7363
7806
  }
7364
- const settled = join21 ? join21.byNode.size : state.records.length;
7807
+ const settled = join23 ? join23.byNode.size : state.records.length;
7365
7808
  const bits = [];
7366
7809
  const head = state.status === "idle" ? `Run state for ${state.op}: no per-step records \u2014 this behold session didn't start the run, so only the gate query answers` : `Run: ${state.op} (${state.mode === "temporal" ? "durable" : "local executor"}) \u2014 ${settled} step${settled === 1 ? "" : "s"} settled`;
7367
7810
  if (state.status === "running") {
@@ -7377,9 +7820,9 @@ function playheadNote(state, join21) {
7377
7820
  } else if (state.gateNote) {
7378
7821
  bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
7379
7822
  }
7380
- if (join21?.unplaced.length) {
7823
+ if (join23?.unplaced.length) {
7381
7824
  bits.push(
7382
- `${join21.unplaced.length} record${join21.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join21.unplaced.length === 1 ? "is" : "are"} not painted`
7825
+ `${join23.unplaced.length} record${join23.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join23.unplaced.length === 1 ? "is" : "are"} not painted`
7383
7826
  );
7384
7827
  }
7385
7828
  if (!bits.length) return `${head}.`;
@@ -8045,24 +8488,24 @@ var OpRunner = class {
8045
8488
 
8046
8489
  // src/substrates.ts
8047
8490
  import { spawn as spawn4 } from "node:child_process";
8048
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
8049
- import { join as join15 } from "node:path";
8491
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
8492
+ import { join as join17 } from "node:path";
8050
8493
  import { platform } from "node:os";
8051
8494
  var DOCKER_DOWN_DETAIL = "docker is down";
8052
8495
  function probe(cmd, args) {
8053
- return new Promise((resolve9) => {
8496
+ return new Promise((resolve12) => {
8054
8497
  let out = "";
8055
8498
  let proc;
8056
8499
  try {
8057
8500
  proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
8058
8501
  } catch {
8059
- resolve9({ code: 127, out: "" });
8502
+ resolve12({ code: 127, out: "" });
8060
8503
  return;
8061
8504
  }
8062
8505
  proc.stdout.on("data", (d) => out += d);
8063
8506
  proc.stderr.on("data", (d) => out += d);
8064
- proc.on("error", () => resolve9({ code: 127, out }));
8065
- proc.on("close", (code) => resolve9({ code: code ?? 1, out }));
8507
+ proc.on("error", () => resolve12({ code: 127, out }));
8508
+ proc.on("close", (code) => resolve12({ code: code ?? 1, out }));
8066
8509
  });
8067
8510
  }
8068
8511
  async function dockerAvailable(run3) {
@@ -8075,11 +8518,11 @@ async function dockerRunning(run3, nameFilter) {
8075
8518
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
8076
8519
  }
8077
8520
  function scriptBringUp(projectDir, relPath, label) {
8078
- return existsSync10(join15(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
8521
+ return existsSync11(join17(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
8079
8522
  }
8080
8523
  function projectLexicons(projectDir) {
8081
8524
  try {
8082
- const src = readFileSync12(join15(projectDir, "chant.config.ts"), "utf-8");
8525
+ const src = readFileSync13(join17(projectDir, "chant.config.ts"), "utf-8");
8083
8526
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
8084
8527
  if (!m) return [];
8085
8528
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -8131,7 +8574,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
8131
8574
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
8132
8575
  ];
8133
8576
  for (const [name, label, marker, script] of forges) {
8134
- if (!existsSync10(join15(projectDir, marker))) continue;
8577
+ if (!existsSync11(join17(projectDir, marker))) continue;
8135
8578
  const c = docker ? await dockerRunning(run3, name) : [];
8136
8579
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
8137
8580
  subs.push({
@@ -8165,7 +8608,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
8165
8608
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
8166
8609
  });
8167
8610
  }
8168
- if (existsSync10(join15(projectDir, ".github", "workflows"))) {
8611
+ if (existsSync11(join17(projectDir, ".github", "workflows"))) {
8169
8612
  const gh = await run3("gh", ["auth", "status"]);
8170
8613
  const ready = gh.code === 0;
8171
8614
  subs.push({
@@ -8178,7 +8621,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
8178
8621
  if (lexicons.includes("temporal")) {
8179
8622
  let hasProfiles = false;
8180
8623
  try {
8181
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync12(join15(projectDir, "chant.config.ts"), "utf-8"));
8624
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync13(join17(projectDir, "chant.config.ts"), "utf-8"));
8182
8625
  } catch {
8183
8626
  }
8184
8627
  subs.push({
@@ -8282,12 +8725,12 @@ async function openRollbackBranches(projectDir, env) {
8282
8725
  }
8283
8726
 
8284
8727
  // src/estate-edges.ts
8285
- import { statSync as statSync6 } from "node:fs";
8728
+ import { statSync as statSync7 } from "node:fs";
8286
8729
  var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
8287
8730
  var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
8288
8731
  var realIsDir2 = (p) => {
8289
8732
  try {
8290
- return statSync6(p).isDirectory();
8733
+ return statSync7(p).isDirectory();
8291
8734
  } catch {
8292
8735
  return false;
8293
8736
  }
@@ -8386,8 +8829,8 @@ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
8386
8829
  }
8387
8830
 
8388
8831
  // src/events.ts
8389
- import { watch, existsSync as existsSync11 } from "node:fs";
8390
- import { join as join16 } from "node:path";
8832
+ import { watch, existsSync as existsSync12 } from "node:fs";
8833
+ import { join as join18 } from "node:path";
8391
8834
  var Broadcaster = class {
8392
8835
  listeners = /* @__PURE__ */ new Set();
8393
8836
  subscribe(fn) {
@@ -8405,7 +8848,7 @@ var Broadcaster = class {
8405
8848
  };
8406
8849
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
8407
8850
  function watchSource(projectDir, onChange, debounceMs = 200) {
8408
- const dir = existsSync11(join16(projectDir, "src")) ? join16(projectDir, "src") : projectDir;
8851
+ const dir = existsSync12(join18(projectDir, "src")) ? join18(projectDir, "src") : projectDir;
8409
8852
  let timer;
8410
8853
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
8411
8854
  const name = typeof file === "string" ? file : "";
@@ -8693,13 +9136,13 @@ async function emulatorDown(projectDir) {
8693
9136
  }
8694
9137
 
8695
9138
  // src/demos.ts
8696
- import { readFileSync as readFileSync13, existsSync as existsSync12, cpSync } from "node:fs";
8697
- import { join as join17, relative as relative3, resolve as resolve5, sep as sep3 } from "node:path";
9139
+ import { readFileSync as readFileSync14, existsSync as existsSync13, cpSync } from "node:fs";
9140
+ import { join as join19, relative as relative4, resolve as resolve8, sep as sep5 } from "node:path";
8698
9141
  import { spawn as spawn5, spawnSync } from "node:child_process";
8699
9142
  function loadDemoRegistry(pkgRoot2) {
8700
9143
  let raw;
8701
9144
  try {
8702
- raw = JSON.parse(readFileSync13(join17(pkgRoot2, "demos.json"), "utf8"));
9145
+ raw = JSON.parse(readFileSync14(join19(pkgRoot2, "demos.json"), "utf8"));
8703
9146
  } catch {
8704
9147
  return [];
8705
9148
  }
@@ -8739,22 +9182,22 @@ function fetchesFromNetwork(entry) {
8739
9182
  return entry.source === "git";
8740
9183
  }
8741
9184
  function demoTargetDir(entry, cwd = process.cwd()) {
8742
- const legacy = resolve5(cwd, "behold-demo");
8743
- if (entry.name === "writes" && existsSync12(legacy)) return legacy;
8744
- return resolve5(cwd, "behold-demos", entry.name);
9185
+ const legacy = resolve8(cwd, "behold-demo");
9186
+ if (entry.name === "writes" && existsSync13(legacy)) return legacy;
9187
+ return resolve8(cwd, "behold-demos", entry.name);
8745
9188
  }
8746
9189
  async function loadDemo(entry, opts) {
8747
9190
  const { pkgRoot: pkgRoot2, target } = opts;
8748
9191
  const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
8749
- if (!existsSync12(target)) {
9192
+ if (!existsSync13(target)) {
8750
9193
  if (entry.source === "bundled") {
8751
- const bundled = join17(pkgRoot2, entry.dir);
8752
- if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
9194
+ const bundled = join19(pkgRoot2, entry.dir);
9195
+ if (!existsSync13(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
8753
9196
  say(`copying to ${target} (it's yours \u2014 edit it)`);
8754
9197
  try {
8755
9198
  cpSync(bundled, target, {
8756
9199
  recursive: true,
8757
- filter: (src) => !relative3(bundled, src).split(sep3).includes("node_modules")
9200
+ filter: (src) => !relative4(bundled, src).split(sep5).includes("node_modules")
8758
9201
  });
8759
9202
  } catch (err) {
8760
9203
  return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -8768,7 +9211,7 @@ async function loadDemo(entry, opts) {
8768
9211
  } else {
8769
9212
  say(`reusing ${target}`);
8770
9213
  }
8771
- if (existsSync12(join17(target, "package.json")) && !existsSync12(join17(target, "node_modules"))) {
9214
+ if (existsSync13(join19(target, "package.json")) && !existsSync13(join19(target, "node_modules"))) {
8772
9215
  say("npm install\u2026");
8773
9216
  if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
8774
9217
  return { ok: false, error: `npm install failed in ${target}` };
@@ -8780,7 +9223,7 @@ async function loadDemo(entry, opts) {
8780
9223
  return { ok: false, error: `setup failed (${entry.setup})` };
8781
9224
  }
8782
9225
  }
8783
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join17(target, d)) : [target] };
9226
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join19(target, d)) : [target] };
8784
9227
  }
8785
9228
  function runStep(cmd, args, opts = {}) {
8786
9229
  return new Promise((res) => {
@@ -8791,8 +9234,8 @@ function runStep(cmd, args, opts = {}) {
8791
9234
  }
8792
9235
 
8793
9236
  // src/server.ts
8794
- var pkgRoot = join18(dirname5(fileURLToPath3(import.meta.url)), "..");
8795
- var webRoot = join18(pkgRoot, "web");
9237
+ var pkgRoot = join20(dirname5(fileURLToPath3(import.meta.url)), "..");
9238
+ var webRoot = join20(pkgRoot, "web");
8796
9239
  var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
8797
9240
  function optsFromQuery(url, tierEnvVar, projectDir) {
8798
9241
  const q = url.searchParams;
@@ -8842,7 +9285,7 @@ function tierFailure(tier, message) {
8842
9285
  function beholdVersion() {
8843
9286
  try {
8844
9287
  const here = dirname5(fileURLToPath3(import.meta.url));
8845
- return JSON.parse(readFileSync14(join18(here, "..", "package.json"), "utf8")).version ?? "unknown";
9288
+ return JSON.parse(readFileSync15(join20(here, "..", "package.json"), "utf8")).version ?? "unknown";
8846
9289
  } catch {
8847
9290
  return "unknown";
8848
9291
  }
@@ -8887,9 +9330,14 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
8887
9330
  }
8888
9331
  }
8889
9332
  function carveRoutes(app, reportPath, demo) {
8890
- const load = () => readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
9333
+ const load = () => readCarveReport(reportPath, (p) => readFileSync15(p, "utf8"));
8891
9334
  const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
8892
9335
  const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
9336
+ const emittedNodes = async (states) => {
9337
+ if (!demo || states.size === 0) return /* @__PURE__ */ new Map();
9338
+ const outIr = await memberIr(demo.out).catch(() => void 0);
9339
+ return emittedNodesFor(outIr, states.values(), demo.out);
9340
+ };
8893
9341
  const demoBlock = () => carveWriteBlock(demo);
8894
9342
  const demoInfo = () => {
8895
9343
  if (!demo) return null;
@@ -8901,8 +9349,8 @@ function carveRoutes(app, reportPath, demo) {
8901
9349
  project: demo.project,
8902
9350
  out: demo.out,
8903
9351
  // Paths the UI can print without leaking the operator's whole home dir.
8904
- outLabel: relative4(demo.root, demo.out).split(sep4).join("/"),
8905
- fromLabel: relative4(demo.root, demo.from).split(sep4).join("/"),
9352
+ outLabel: relative5(demo.root, demo.out).split(sep6).join("/"),
9353
+ fromLabel: relative5(demo.root, demo.from).split(sep6).join("/"),
8906
9354
  runnable: !block,
8907
9355
  ...block ? { reason: block } : {},
8908
9356
  // The live tier, when this boot armed it: the scratch endpoint the
@@ -8919,7 +9367,7 @@ function carveRoutes(app, reportPath, demo) {
8919
9367
  let appGraph;
8920
9368
  const appGraphOnce = () => {
8921
9369
  if (!demo) return Promise.resolve(null);
8922
- appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative4(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
9370
+ appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative5(demo.root, demo.project).split(sep6).join("/") } : null).catch(() => null);
8923
9371
  return appGraph;
8924
9372
  };
8925
9373
  app.get("/api/graph", async (c) => {
@@ -8928,16 +9376,18 @@ function carveRoutes(app, reportPath, demo) {
8928
9376
  const tfIr = carveReportToIr(parsed.report);
8929
9377
  const appSide = await appGraphOnce();
8930
9378
  const states = carveStates();
9379
+ const emitted = await emittedNodes(states);
8931
9380
  const shorten = demo ? (p) => shortenIn(p, demo.root) : (p) => p;
8932
9381
  const single = () => {
8933
- const split = splitCarveState(tfIr, states, { shorten });
9382
+ const split = splitCarveState(tfIr, states, { shorten, emitted });
8934
9383
  return bandGraduated(split.tf, split.graduated);
8935
9384
  };
8936
9385
  const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
8937
- tfTitle: `${relative4(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
9386
+ tfTitle: `${relative5(demo.root, demo.from).split(sep6).join("/")} \u2014 terraform`,
8938
9387
  appTitle: `${appSide.label} \u2014 chant`,
8939
9388
  carved: states,
8940
- shorten
9389
+ shorten,
9390
+ emitted
8941
9391
  }) : (() => {
8942
9392
  const banded = single();
8943
9393
  return { ...renderBanded(banded), ir: banded };
@@ -8953,7 +9403,9 @@ function carveRoutes(app, reportPath, demo) {
8953
9403
  carve: true,
8954
9404
  // A demo whose own advisor run failed says so on the statusbar, not
8955
9405
  // only in the terminal the viewer isn't looking at.
8956
- note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
9406
+ note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "") + // chant#2058: the app box shows only the app's own declarations;
9407
+ // what the git-root Op discovery added from elsewhere is named.
9408
+ (appSide && foreignNote(appSide.ir) ? ` ${foreignNote(appSide.ir)}.` : "")
8957
9409
  }
8958
9410
  });
8959
9411
  });
@@ -8985,7 +9437,7 @@ function carveRoutes(app, reportPath, demo) {
8985
9437
  }
8986
9438
  const tfIr = carveReportToIr(parsed.report);
8987
9439
  const html = renderCarveMorph(tfIr, appSide.ir, [select], {
8988
- tfTitle: `${relative4(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
9440
+ tfTitle: `${relative5(demo.root, demo.from).split(sep6).join("/")} \u2014 terraform`,
8989
9441
  appTitle: `${appSide.label} \u2014 chant`,
8990
9442
  title: `carve \u2014 ${select}`,
8991
9443
  carved: carveStates(),
@@ -8993,7 +9445,7 @@ function carveRoutes(app, reportPath, demo) {
8993
9445
  });
8994
9446
  return c.html(html);
8995
9447
  });
8996
- app.get("/api/project", (c) => {
9448
+ app.get("/api/project", async (c) => {
8997
9449
  const parsed = load();
8998
9450
  const total = parsed.ok ? parsed.report.count ?? parsed.report.resources.length : null;
8999
9451
  const state = carveStatePayload(
@@ -9079,6 +9531,36 @@ function carveRoutes(app, reportPath, demo) {
9079
9531
  app.get("/api/resources", (c) => c.json({ byComponent: {} }));
9080
9532
  app.get("/api/ci", (c) => c.json({ stages: [], jobs: [], forge: null }));
9081
9533
  }
9534
+ function followLimits(env = process.env) {
9535
+ const timeout = Number.parseInt(env.BEHOLD_CI_FOLLOW_TIMEOUT_MS ?? "", 10);
9536
+ const budget = Number.parseInt(env.BEHOLD_CI_POLL_FAIL_BUDGET ?? "", 10);
9537
+ return {
9538
+ ...Number.isInteger(timeout) && timeout >= 0 ? { followTimeoutMs: timeout } : {},
9539
+ ...Number.isInteger(budget) && budget > 0 ? { pollFailBudget: budget } : {}
9540
+ };
9541
+ }
9542
+ async function readoptDispatchedRun(cfg, runner) {
9543
+ const run3 = readDispatchedRun(cfg.projectDir);
9544
+ if (!run3 || run3.concluded && run3.concluded.verdict !== "lost") return { outcome: "none" };
9545
+ const ready = await ghReady();
9546
+ if (!ready.ok) return { outcome: "refused", reason: ready.reason ?? "gh is not ready", run: run3 };
9547
+ const pipeline = await ciPipeline(cfg.projectDir, run3.env ? { env: run3.env } : {}, "github").catch(() => void 0);
9548
+ if (!pipeline || pipeline.jobs.length === 0) {
9549
+ return { outcome: "refused", reason: "no generated GitHub pipeline to paint the run onto \u2014 `chant build --components --generate github` first", run: run3 };
9550
+ }
9551
+ const started = runner.track(`GitHub Actions ${run3.workflow} (re-adopted run ${run3.runId})`, (io) => {
9552
+ io.line(`\u25CF re-adopting run ${run3.runId} (${run3.workflow}, dispatched ${run3.dispatchedAt}) \u2014 \`gh run view\` is the durable read`);
9553
+ return followRun(run3.runId, pipeline, {
9554
+ ...followLimits(),
9555
+ onLine: io.line,
9556
+ onProgress: io.progress,
9557
+ onAdopted: ({ runId, url }) => saveDispatchedRun({ ...run3, runId, ...url ? { url } : {} }),
9558
+ onConcluded: ({ verdict }) => concludeDispatchedRun(cfg.projectDir, run3.runId, verdict)
9559
+ });
9560
+ });
9561
+ if (!started) return { outcome: "refused", reason: `busy \u2014 ${runner.running} is running`, run: run3 };
9562
+ return { outcome: "readopted", run: run3 };
9563
+ }
9082
9564
  function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
9083
9565
  projectDir: cfg.projectDir,
9084
9566
  broadcaster,
@@ -9088,6 +9570,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9088
9570
  if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
9089
9571
  let beholdConfig = loadBeholdConfig(cfg.projectDir);
9090
9572
  let tierEnvVar = beholdConfig.tiers?.envVar;
9573
+ const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
9574
+ const executorStatus = (env, d, pipeline) => {
9575
+ if ("invalid" in d) return { ok: false, reason: d.invalid };
9576
+ if (d.forge !== "github") {
9577
+ return { forge: d.forge, workflow: d.workflow, ok: false, reason: `env ${env} deploys through ${d.forge} per .behold.json, and behold has no ${d.forge} trigger (only GitHub Actions, via your gh) \u2014 Deploy stays disabled here; run the pipeline on the forge` };
9578
+ }
9579
+ const r = resolveWorkflow(cfg.projectDir, pipeline ?? { stages: [], jobs: [] }, d.workflow);
9580
+ return "workflow" in r ? { forge: d.forge, workflow: d.workflow, ok: true } : { forge: d.forge, workflow: d.workflow, ok: false, reason: r.reason };
9581
+ };
9582
+ const forgeRefusal = (env, d, what) => ({
9583
+ error: `${what} is refused: env ${env} deploys through ${"invalid" in d ? "a forge" : `${d.forge} (${d.workflow})`} per .behold.json's executor block \u2014 behold never runs it from this machine`,
9584
+ code: "executor-forge",
9585
+ remedy: "invalid" in d ? `Fix .behold.json: ${d.invalid}` : `Use Deploy (POST /api/ci/dispatch?env=${env}), which dispatches the designated workflow through your gh login.`
9586
+ });
9091
9587
  const boundK8sContext = async (env) => {
9092
9588
  try {
9093
9589
  const { lexicons, k8sProfiles } = await detectProject(cfg.projectDir);
@@ -9149,6 +9645,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9149
9645
  if (!info) {
9150
9646
  return c.json({ error: `no Op named "${name}" in the estate` }, 404);
9151
9647
  }
9648
+ const designatedEnv = info.kind === "apply" ? executorFor(info.env) : void 0;
9649
+ if (designatedEnv && info.env) return c.json(forgeRefusal(info.env, designatedEnv, `running ${name}`), 409);
9152
9650
  if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
9153
9651
  return c.json({ error: `an Op is already running (${runner.running})` }, 409);
9154
9652
  }
@@ -9180,9 +9678,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9180
9678
  return c.json({ started: true, name, ran: label });
9181
9679
  });
9182
9680
  app.post("/api/local/reset", (c) => {
9183
- const down = join18(cfg.projectDir, "scripts/local/local-down.sh");
9184
- const up = join18(cfg.projectDir, "scripts/local/local-up.sh");
9185
- if (!existsSync13(down) || !existsSync13(up)) {
9681
+ const down = join20(cfg.projectDir, "scripts/local/local-down.sh");
9682
+ const up = join20(cfg.projectDir, "scripts/local/local-up.sh");
9683
+ if (!existsSync14(down) || !existsSync14(up)) {
9186
9684
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
9187
9685
  }
9188
9686
  if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
@@ -9203,7 +9701,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9203
9701
  const dirs = [];
9204
9702
  const declared = [];
9205
9703
  for (const f of discoverOpIrs(estateDirs)) {
9206
- const parsed = readOpIr(f.path, (p) => readFileSync14(p, "utf8"));
9704
+ const parsed = readOpIr(f.path, (p) => readFileSync15(p, "utf8"));
9207
9705
  if (!parsed.ok) continue;
9208
9706
  const mine = declaredConvergeOps([parsed.ir]);
9209
9707
  if (!mine.length) continue;
@@ -9318,7 +9816,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9318
9816
  const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
9319
9817
  const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
9320
9818
  const emittedOps = discoverOpIrs(estateDirs).length;
9321
- const carveState = carveStatePayload(readCarveStates(estateDirs));
9819
+ const carveState = carveStatePayload(await carveStatesUnder(estateDirs));
9322
9820
  return c.json({
9323
9821
  projectDir: cfg.projectDir,
9324
9822
  ...carveState.manifests ? { carve: { state: carveState } } : {},
@@ -9345,6 +9843,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9345
9843
  // no `tiers` key) → no `tiers` field → the SPA's picker doesn't render
9346
9844
  // and the graph loads with no tier selected (web/app.js `initPickers`).
9347
9845
  ...beholdConfig.tiers ? { tiers: beholdConfig.tiers.values } : {},
9846
+ // The executor contract (#165): per designated env, whether Deploy can
9847
+ // dispatch right now and, if not, why — so the SPA disables the gesture
9848
+ // with the reason rather than offering a control that 4xxs.
9849
+ ...beholdConfig.executor ? { executor: Object.fromEntries(Object.entries(beholdConfig.executor).map(([env, d]) => [env, executorStatus(env, d)])) } : {},
9348
9850
  // The stack picker's options (#76, follow-up to #71): gated exactly like
9349
9851
  // `tiers` above — only present when `chant.config.ts` declares `stacks[]`
9350
9852
  // at all, so a single-stack/sourceDir-only/legacy project's SPA renders no
@@ -9369,9 +9871,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9369
9871
  app.post("/api/project/open", async (c) => {
9370
9872
  if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
9371
9873
  const body = await c.req.json().catch(() => ({}));
9372
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve6(body.dir.trim()) : "";
9373
- if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
9374
- if (!existsSync13(join18(dir, "chant.config.ts"))) {
9874
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve9(body.dir.trim()) : "";
9875
+ if (!dir || !existsSync14(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
9876
+ if (!existsSync14(join20(dir, "chant.config.ts"))) {
9375
9877
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
9376
9878
  }
9377
9879
  switchServedProject([dir]);
@@ -9391,7 +9893,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9391
9893
  };
9392
9894
  app.post("/api/project/reveal", async (c) => {
9393
9895
  const body = await c.req.json().catch(() => ({}));
9394
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve6(body.dir.trim()) : cfg.projectDir;
9896
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve9(body.dir.trim()) : cfg.projectDir;
9395
9897
  const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
9396
9898
  if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
9397
9899
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
@@ -9415,7 +9917,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9415
9917
  fetches: fetchesFromNetwork(e),
9416
9918
  ...e.repo ? { repo: e.repo } : {},
9417
9919
  target,
9418
- loaded: existsSync13(target),
9920
+ loaded: existsSync14(target),
9419
9921
  satisfiable: missing.length === 0,
9420
9922
  // #254: the carve walkthrough runs fine here — it just isn't a project
9421
9923
  // to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
@@ -9459,7 +9961,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9459
9961
  const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
9460
9962
  if (!loaded.ok) return c.json({ error: loaded.error }, 500);
9461
9963
  const dirs = loaded.serveDirs;
9462
- if (!existsSync13(join18(dirs[0], "chant.config.ts"))) {
9964
+ if (!existsSync14(join20(dirs[0], "chant.config.ts"))) {
9463
9965
  return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
9464
9966
  }
9465
9967
  if (entry.serve.local) {
@@ -9557,7 +10059,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9557
10059
  path: "/api/demos/open",
9558
10060
  desc: "load a demo and serve it: JSON body {name} (a catalog name, never a path) \u2014 copies/clones, installs, runs its setup, then switches (preview-locked)"
9559
10061
  },
9560
- { method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, ops=1, env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
10062
+ { method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, ops=1 (+entities=1 to link steps to the estate), env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
9561
10063
  ...cfg.carveReport ? [
9562
10064
  { method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
9563
10065
  {
@@ -9594,7 +10096,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9594
10096
  { method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
9595
10097
  { method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
9596
10098
  { method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
9597
- { method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline via the operator's gh" }
10099
+ { method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline for ?env= via the operator's gh \u2014 the designated workflow when .behold.json names one" },
10100
+ { method: "GET", path: "/api/ci/run", desc: "the persisted dispatched-run record (id, verdict) \u2014 what survives a behold restart" },
10101
+ { method: "POST", path: "/api/ci/readopt", desc: "re-adopt the persisted unconcluded run: follow it by its saved id (also attempted automatically at boot)" }
9598
10102
  ]
9599
10103
  })
9600
10104
  );
@@ -9616,15 +10120,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9616
10120
  }
9617
10121
  const parsedOps = [];
9618
10122
  for (const f of files) {
9619
- const parsed = readOpIr(f.path, (p) => readFileSync14(p, "utf8"));
10123
+ const parsed = readOpIr(f.path, (p) => readFileSync15(p, "utf8"));
9620
10124
  if (!parsed.ok) return c.json(parsed.refusal, 422);
9621
10125
  parsedOps.push(parsed.ir);
9622
10126
  }
9623
10127
  const declaredIr = opsToIr(parsedOps);
10128
+ if (url.searchParams.get("entities") === "1") {
10129
+ const memberDirs = [...new Set(files.map((f) => f.dir))];
10130
+ const memberReads = await Promise.allSettled(memberDirs.map((dir) => memberIr(dir)));
10131
+ const estateNodes = memberReads.flatMap((r) => r.status === "fulfilled" ? r.value.nodes : []);
10132
+ joinStepEntities(declaredIr, estateNodes.length ? { nodes: estateNodes, edges: [], groups: {} } : void 0);
10133
+ }
9624
10134
  const runState = runner.runState;
9625
10135
  const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
9626
- const join21 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
9627
- const opsIr = join21 ? paintPlayhead(declaredIr, join21, runState.gate) : declaredIr;
10136
+ const join23 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
10137
+ const opsIr = join23 ? paintPlayhead(declaredIr, join23, runState.gate) : declaredIr;
9628
10138
  const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
9629
10139
  const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
9630
10140
  return c.json({
@@ -9637,7 +10147,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9637
10147
  target: null,
9638
10148
  mode: "ops",
9639
10149
  ops: parsedOps.length,
9640
- note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join21)} ${operatorNote(operator)}`,
10150
+ note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join23)} ${operatorNote(operator)}`,
9641
10151
  // The operator strip's own model: the ConvergeOps this project
9642
10152
  // declares, plus whatever the last status read said. `declared`
9643
10153
  // without a `strip` is the honest starting state — a loop exists and
@@ -9709,6 +10219,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9709
10219
  return c.json({ ir: projected, svg: svg2, byContainer, meta: { projectDir: cfg.projectDir, env: metaEnv, tier: opts.tier ?? null, target: opts.target ?? null, mode: "logical", ...logicalNote ? { note: logicalNote } : {} } });
9710
10220
  } else {
9711
10221
  ir = mergeClusterRoot(await graphIr(cfg.projectDir, opts), await clusterRootGraphIr(cfg.projectDir, opts));
10222
+ ir = joinCarvedSources(ir, (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
9712
10223
  if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
9713
10224
  ir = addK8sDeclaredEdges(ir);
9714
10225
  ir = addValueMatchEdges(ir);
@@ -9726,7 +10237,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9726
10237
  srcCompositeEdgesAttached = 0;
9727
10238
  }
9728
10239
  }
9729
- if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join18(cfg.projectDir, "chant.config.ts"))) {
10240
+ if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync14(join20(cfg.projectDir, "chant.config.ts"))) {
9730
10241
  return c.json(noProjectError(cfg.projectDir), 404);
9731
10242
  }
9732
10243
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -9773,23 +10284,51 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9773
10284
  }
9774
10285
  });
9775
10286
  app.post("/api/ci/dispatch", async (c) => {
10287
+ const env = new URL(c.req.url).searchParams.get("env") ?? cfg.env;
10288
+ const designated = executorFor(env);
10289
+ if (designated && "invalid" in designated) return c.json({ error: designated.invalid, code: "executor-forge", remedy: "Fix .behold.json's executor block." }, 400);
10290
+ if (designated && designated.forge !== "github") {
10291
+ return c.json({ error: executorStatus(env, designated).reason, code: "executor-forge" }, 400);
10292
+ }
9776
10293
  const ready = await ghReady();
9777
10294
  if (!ready.ok) return c.json({ error: ready.reason }, 400);
9778
- const pipeline = await ciPipeline(cfg.projectDir, cfg.env ? { env: cfg.env } : {}, "github").catch(() => void 0);
10295
+ const pipeline = await ciPipeline(cfg.projectDir, env ? { env } : {}, "github").catch(() => void 0);
9779
10296
  if (!pipeline || pipeline.jobs.length === 0) {
9780
10297
  return c.json({ error: "no generated GitHub pipeline \u2014 `chant build --components --generate github` produced no jobs" }, 400);
9781
10298
  }
9782
- const workflow = pickWorkflow(cfg.projectDir, pipeline);
9783
- if (!workflow) {
9784
- return c.json({ error: "no committed workflow_dispatch workflow whose jobs match the generated pipeline \u2014 commit one (or add `workflow_dispatch:` to its `on:` block)" }, 400);
9785
- }
10299
+ const resolved = resolveWorkflow(cfg.projectDir, pipeline, designated?.workflow);
10300
+ if ("reason" in resolved) return c.json({ error: resolved.reason, ...designated ? { code: "executor-forge" } : {} }, 400);
10301
+ const workflow = resolved.workflow;
9786
10302
  const ref = await execFileP("git", ["-C", cfg.projectDir, "branch", "--show-current"]).then((out) => out.trim() || "main").catch(() => "main");
10303
+ const base = { projectDir: cfg.projectDir, ...env ? { env } : {}, workflow: workflow.file, ref, dispatchedAt: (/* @__PURE__ */ new Date()).toISOString() };
10304
+ let adoptedId;
9787
10305
  const started = runner.track(
9788
10306
  `GitHub Actions ${workflow.file}`,
9789
- (io) => dispatchAndFollow(workflow, pipeline, ref, { onLine: io.line, onProgress: io.progress })
10307
+ (io) => dispatchAndFollow(workflow, pipeline, ref, {
10308
+ ...followLimits(),
10309
+ onLine: io.line,
10310
+ onProgress: io.progress,
10311
+ onAdopted: ({ runId, url }) => {
10312
+ adoptedId = runId;
10313
+ saveDispatchedRun({ ...base, runId, ...url ? { url } : {} });
10314
+ },
10315
+ onConcluded: ({ verdict }) => {
10316
+ if (adoptedId !== void 0) concludeDispatchedRun(cfg.projectDir, adoptedId, verdict);
10317
+ }
10318
+ })
9790
10319
  );
9791
10320
  if (!started) return c.json({ error: `busy \u2014 ${runner.running} is running` }, 409);
9792
- return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length });
10321
+ return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length, ...env ? { env } : {}, designated: Boolean(designated) });
10322
+ });
10323
+ app.get("/api/ci/run", (c) => {
10324
+ const run3 = readDispatchedRun(cfg.projectDir);
10325
+ return c.json({ run: run3 ?? null });
10326
+ });
10327
+ app.post("/api/ci/readopt", async (c) => {
10328
+ const result = await readoptDispatchedRun(cfg, runner);
10329
+ if (result.outcome === "none") return c.json({ error: "no unconcluded dispatched run is persisted for this project" }, 404);
10330
+ if (result.outcome === "refused") return c.json({ error: result.reason, run: result.run }, 409);
10331
+ return c.json({ readopted: true, runId: result.run.runId, workflow: result.run.workflow });
9793
10332
  });
9794
10333
  app.get("/api/resources", async (c) => {
9795
10334
  const opts = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
@@ -9884,7 +10423,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9884
10423
  });
9885
10424
  }
9886
10425
  const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
9887
- let ir = reclassifyOverlay(await graphIr(cfg.projectDir, opts));
10426
+ let ir = joinCarvedSources(reclassifyOverlay(await graphIr(cfg.projectDir, opts)), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
9888
10427
  const boundContext = await boundK8sContext(env);
9889
10428
  ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
9890
10429
  const declaresHelm = await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("helm")).catch(() => false);
@@ -9892,7 +10431,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9892
10431
  const observed = await lifecycleDiffLive(cfg.projectDir, env, query).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
9893
10432
  applyHelmArtifacts(ir, observed);
9894
10433
  synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
9895
- applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query));
10434
+ applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query, observed));
9896
10435
  }
9897
10436
  ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
9898
10437
  if (logical) {
@@ -9943,7 +10482,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9943
10482
  const observed = await lifecycleDiffLive(cfg.projectDir, env).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
9944
10483
  applyHelmArtifacts(result.ir, observed);
9945
10484
  synthesizeHelmReleases(result.ir, observed, discoverReleaseUnits(cfg.projectDir));
9946
- applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env));
10485
+ applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env, {}, observed));
9947
10486
  }
9948
10487
  }
9949
10488
  if (env) {
@@ -10047,6 +10586,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
10047
10586
  }
10048
10587
  const component = url.searchParams.get("component") || "all";
10049
10588
  const force = url.searchParams.get("force") === "1";
10589
+ const designated = executorFor(env);
10590
+ if (designated) return c.json(forgeRefusal(env, designated, "apply"), 409);
10050
10591
  const flociTargeted = !!process.env.AWS_ENDPOINT_URL && await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("aws")).catch(() => false);
10051
10592
  if (!force && flociTargeted) {
10052
10593
  try {
@@ -10071,9 +10612,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
10071
10612
  }
10072
10613
  return c.json({ started: true, component, env });
10073
10614
  });
10074
- const rel = relative4(process.cwd(), webRoot) || ".";
10615
+ const rel = relative5(process.cwd(), webRoot) || ".";
10075
10616
  app.use("/*", serveStatic({ root: rel }));
10076
- app.get("/", serveStatic({ path: join18(rel, "index.html") }));
10617
+ app.get("/", serveStatic({ path: join20(rel, "index.html") }));
10077
10618
  return app;
10078
10619
  }
10079
10620
  async function bootLocalEmulators(dir, who) {
@@ -10102,7 +10643,7 @@ async function bootLocalEmulators(dir, who) {
10102
10643
  async function startServer(cfg) {
10103
10644
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
10104
10645
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
10105
- if (existsSync13(join18(dir, "chant.config.ts"))) addRecent(dir);
10646
+ if (existsSync14(join20(dir, "chant.config.ts"))) addRecent(dir);
10106
10647
  }
10107
10648
  const broadcaster = new Broadcaster();
10108
10649
  const frames = new FrameBuffer();
@@ -10113,6 +10654,17 @@ async function startServer(cfg) {
10113
10654
  });
10114
10655
  const app = createApp(cfg, broadcaster, frames, runner);
10115
10656
  const autoSync = cfg.autoSync ?? "off";
10657
+ if (!cfg.carveReport) {
10658
+ void readoptDispatchedRun(cfg, runner).then((result) => {
10659
+ if (result.outcome === "readopted") {
10660
+ process.stdout.write(` re-adopted dispatched run ${result.run.runId} (${result.run.workflow}) \u2014 following via gh
10661
+ `);
10662
+ } else if (result.outcome === "refused") {
10663
+ process.stdout.write(` a dispatched run (${result.run.runId}, ${result.run.workflow}) is persisted but not re-adopted: ${result.reason}
10664
+ `);
10665
+ }
10666
+ }).catch(() => void 0);
10667
+ }
10116
10668
  const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
10117
10669
  const onEstateChange = (memberDir) => {
10118
10670
  broadcaster.emit("changed", memberDir ?? "");
@@ -10128,11 +10680,19 @@ async function startServer(cfg) {
10128
10680
  if (autoSync === "off") return;
10129
10681
  void routeAutoSync(dir, movedLexicons);
10130
10682
  };
10683
+ const executorDesignations = loadBeholdConfig(cfg.projectDir).executor ?? {};
10684
+ const executorFor = (env) => env ? executorDesignations[env] : void 0;
10131
10685
  const routeAutoSync = async (dir, movedLexicons) => {
10132
10686
  const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
10687
+ const allOps = discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]);
10688
+ const routed = allOps.filter((o) => o.kind === "apply" && o.env && executorFor(o.env));
10689
+ for (const o of routed) {
10690
+ const d = executorFor(o.env);
10691
+ broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${o.name}: env ${o.env} deploys through ${"invalid" in d ? "a forge" : d.forge} per .behold.json \u2014 never applied from here`);
10692
+ }
10133
10693
  const { picks, declined } = pickAutoSyncOps(
10134
10694
  autoSync,
10135
- discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]),
10695
+ allOps.filter((o) => !routed.includes(o)),
10136
10696
  runner.running,
10137
10697
  movedLexicons,
10138
10698
  suspended
@@ -10241,8 +10801,8 @@ async function startServer(cfg) {
10241
10801
  }
10242
10802
 
10243
10803
  // src/export.ts
10244
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "node:fs";
10245
- import { join as join19, dirname as dirname6, basename as basename2 } from "node:path";
10804
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, copyFileSync, readFileSync as readFileSync16, readdirSync as readdirSync8 } from "node:fs";
10805
+ import { join as join21, dirname as dirname6, basename as basename2 } from "node:path";
10246
10806
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10247
10807
  var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
10248
10808
  function canonicalKey(path, params) {
@@ -10250,7 +10810,7 @@ function canonicalKey(path, params) {
10250
10810
  const q = LENS_PARAMS.filter((k) => params.has(k) && !(flat && (k === "detail" || k === "radial"))).map((k) => `${k}=${params.get(k)}`).join("&");
10251
10811
  return q ? `${path}?${q}` : path;
10252
10812
  }
10253
- function slug2(key) {
10813
+ function slug3(key) {
10254
10814
  const base = key.replace(/^\//, "").replace(/[^a-zA-Z0-9=_.-]+/g, "_").slice(0, 120);
10255
10815
  return `${base}.json`;
10256
10816
  }
@@ -10260,7 +10820,7 @@ function captureKeys(axes) {
10260
10820
  add("/api/project", {});
10261
10821
  add("/api/substrates", {});
10262
10822
  add("/api/ops", {});
10263
- if (axes.ops) add("/api/graph", { ops: "1" });
10823
+ if (axes.ops) add("/api/graph", { ops: "1", entities: "1" });
10264
10824
  const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
10265
10825
  const envs = ["", ...axes.environments];
10266
10826
  for (const env of envs) {
@@ -10291,7 +10851,7 @@ function captureKeys(axes) {
10291
10851
  return [...keys];
10292
10852
  }
10293
10853
  function webDir() {
10294
- return join19(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
10854
+ return join21(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
10295
10855
  }
10296
10856
  function workerName(project, override) {
10297
10857
  const raw = override ?? `behold-${basename2(project)}`;
@@ -10302,16 +10862,16 @@ async function runExport(cfg, outDir, opts = {}) {
10302
10862
  const app = createApp({ ...cfg, layoutWrites: false });
10303
10863
  const proj = await (await app.request("/api/project")).json();
10304
10864
  const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
10305
- const snapDir = join19(outDir, "snapshots");
10306
- mkdirSync4(snapDir, { recursive: true });
10865
+ const snapDir = join21(outDir, "snapshots");
10866
+ mkdirSync5(snapDir, { recursive: true });
10307
10867
  const keyToFile = {};
10308
10868
  let ok = 0;
10309
10869
  let failed = 0;
10310
10870
  for (const key of captureKeys(axes)) {
10311
10871
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
10312
10872
  const body = await res.text();
10313
- const file = slug2(key);
10314
- writeFileSync4(join19(snapDir, file), body);
10873
+ const file = slug3(key);
10874
+ writeFileSync5(join21(snapDir, file), body);
10315
10875
  keyToFile[key] = `snapshots/${file}`;
10316
10876
  if (res.ok) ok++;
10317
10877
  else failed++;
@@ -10323,21 +10883,21 @@ async function runExport(cfg, outDir, opts = {}) {
10323
10883
  axes,
10324
10884
  keyToFile
10325
10885
  };
10326
- writeFileSync4(join19(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
10327
- const html = readFileSync15(join19(webDir(), "index.html"), "utf8").replace(
10886
+ writeFileSync5(join21(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
10887
+ const html = readFileSync16(join21(webDir(), "index.html"), "utf8").replace(
10328
10888
  /<\/head>/i,
10329
10889
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
10330
10890
  </head>`
10331
10891
  );
10332
- writeFileSync4(join19(outDir, "index.html"), html);
10333
- for (const f of readdirSync7(webDir())) {
10892
+ writeFileSync5(join21(outDir, "index.html"), html);
10893
+ for (const f of readdirSync8(webDir())) {
10334
10894
  if (f === "index.html") continue;
10335
- copyFileSync(join19(webDir(), f), join19(outDir, f));
10895
+ copyFileSync(join21(webDir(), f), join21(outDir, f));
10336
10896
  }
10337
- writeFileSync4(join19(outDir, "README.md"), BUNDLE_README);
10897
+ writeFileSync5(join21(outDir, "README.md"), BUNDLE_README);
10338
10898
  const name = workerName(cfg.projectDir, opts.name);
10339
- writeFileSync4(
10340
- join19(outDir, "wrangler.jsonc"),
10899
+ writeFileSync5(
10900
+ join21(outDir, "wrangler.jsonc"),
10341
10901
  JSON.stringify(
10342
10902
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
10343
10903
  null,
@@ -10385,7 +10945,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
10385
10945
  `;
10386
10946
 
10387
10947
  // src/doctor.ts
10388
- import { relative as relative5, resolve as resolve7 } from "node:path";
10948
+ import { relative as relative6, resolve as resolve10 } from "node:path";
10389
10949
  var list2 = (xs) => xs.join(", ");
10390
10950
  var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
10391
10951
  function summarize(xs, max = 4) {
@@ -10393,7 +10953,7 @@ function summarize(xs, max = 4) {
10393
10953
  }
10394
10954
  function labelFor(root, target, estate) {
10395
10955
  if (!estate) return "";
10396
- const rel = relative5(root, target);
10956
+ const rel = relative6(root, target);
10397
10957
  return `${rel || "."}: `;
10398
10958
  }
10399
10959
  function chantCheck(root, targets, estate) {
@@ -10406,7 +10966,7 @@ function chantCheck(root, targets, estate) {
10406
10966
  name: "chant",
10407
10967
  status: "fail",
10408
10968
  detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
10409
- fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative5(root, target) || target))}`
10969
+ fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative6(root, target) || target))}`
10410
10970
  };
10411
10971
  }
10412
10972
  const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
@@ -10534,7 +11094,7 @@ function opsCheck(root, ops, chantSource, estate) {
10534
11094
  return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
10535
11095
  }
10536
11096
  async function diagnose(dir, probes = {}) {
10537
- const root = resolve7(dir);
11097
+ const root = resolve10(dir);
10538
11098
  const shape = detectProjectShape(root);
10539
11099
  const behold = beholdVersion();
10540
11100
  if (shape.kind === "none") {
@@ -10554,16 +11114,16 @@ async function diagnose(dir, probes = {}) {
10554
11114
  };
10555
11115
  }
10556
11116
  const estate = shape.kind === "estate";
10557
- const members = (shape.members ?? []).map((m) => resolve7(root, m));
11117
+ const members = (shape.members ?? []).map((m) => resolve10(root, m));
10558
11118
  const targets = estate ? members : [root];
10559
11119
  const primary = targets[0];
10560
11120
  const projectCheck = estate ? {
10561
11121
  name: "project",
10562
11122
  status: "pass",
10563
11123
  detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
10564
- members.map((m) => relative5(root, m))
11124
+ members.map((m) => relative6(root, m))
10565
11125
  )}`
10566
- } : { name: "project", status: "pass", detail: `chant project (${relative5(root, shape.configFile)})` };
11126
+ } : { name: "project", status: "pass", detail: `chant project (${relative6(root, shape.configFile)})` };
10567
11127
  const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
10568
11128
  const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
10569
11129
  const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
@@ -10788,7 +11348,7 @@ ${USAGE}`);
10788
11348
  process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
10789
11349
  process.exit(2);
10790
11350
  }
10791
- const dirs = projectDirs.map((d) => resolve8(d));
11351
+ const dirs = projectDirs.map((d) => resolve11(d));
10792
11352
  for (const d of dirs) warnIfNotChantProject(d);
10793
11353
  await startServer({
10794
11354
  projectDir: dirs[0],
@@ -10807,7 +11367,7 @@ function warnIfNotChantProject(dir) {
10807
11367
  if (shape.kind === "estate") {
10808
11368
  process.stderr.write(
10809
11369
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
10810
- Serve its members composed: behold serve ${shape.members.map((m) => join20(dir, m)).join(" ")}
11370
+ Serve its members composed: behold serve ${shape.members.map((m) => join22(dir, m)).join(" ")}
10811
11371
  `
10812
11372
  );
10813
11373
  return;
@@ -10842,8 +11402,8 @@ async function runCarve(rest) {
10842
11402
  );
10843
11403
  process.exit(2);
10844
11404
  }
10845
- const reportPath = resolve8(fileArg);
10846
- const parsed = readCarveReport(reportPath, (p) => readFileSync16(p, "utf8"));
11405
+ const reportPath = resolve11(fileArg);
11406
+ const parsed = readCarveReport(reportPath, (p) => readFileSync17(p, "utf8"));
10847
11407
  if (!parsed.ok) {
10848
11408
  process.stderr.write(`behold carve: ${parsed.refusal.error}
10849
11409
  ${parsed.refusal.remedy}
@@ -10870,8 +11430,8 @@ async function runDoctor(rest) {
10870
11430
  }
10871
11431
  }
10872
11432
  const dir = dirArg ?? ".";
10873
- if (!existsSync14(resolve8(dir))) {
10874
- process.stderr.write(`behold doctor: no such directory: ${resolve8(dir)}
11433
+ if (!existsSync15(resolve11(dir))) {
11434
+ process.stderr.write(`behold doctor: no such directory: ${resolve11(dir)}
10875
11435
  `);
10876
11436
  process.exit(2);
10877
11437
  }
@@ -10880,7 +11440,7 @@ async function runDoctor(rest) {
10880
11440
  if (!report.ok) process.exitCode = 1;
10881
11441
  }
10882
11442
  async function runDemo(rest) {
10883
- const pkgRoot2 = join20(dirname7(fileURLToPath5(import.meta.url)), "..");
11443
+ const pkgRoot2 = join22(dirname7(fileURLToPath5(import.meta.url)), "..");
10884
11444
  const registry = loadDemoRegistry(pkgRoot2);
10885
11445
  let port = 4600;
10886
11446
  let live = false;
@@ -10934,7 +11494,7 @@ async function runDemo(rest) {
10934
11494
  `);
10935
11495
  process.exit(2);
10936
11496
  }
10937
- const target = dirArg ? resolve8(dirArg) : demoTargetDir(entry);
11497
+ const target = dirArg ? resolve11(dirArg) : demoTargetDir(entry);
10938
11498
  const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
10939
11499
  if (!loaded.ok) {
10940
11500
  process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
@@ -10965,7 +11525,7 @@ function spawnStep(cmd, args, cwd) {
10965
11525
  });
10966
11526
  }
10967
11527
  async function serveCarveDemo(target, carve, port, live = false) {
10968
- const at = (rel) => resolve8(target, rel);
11528
+ const at = (rel) => resolve11(target, rel);
10969
11529
  const project = at(carve.project);
10970
11530
  const from = at(carve.from);
10971
11531
  const state = carve.state ? at(carve.state) : void 0;
@@ -10989,7 +11549,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
10989
11549
  process.exit(1);
10990
11550
  }
10991
11551
  process.stdout.write("behold demo carve --live \u2192 terraform init + apply (the starred resources, into the scratch Floci)\u2026\n");
10992
- if (state) rmSync2(state, { force: true });
11552
+ if (state) rmSync3(state, { force: true });
10993
11553
  const applyErr = await applyIntoFloci(from, spawnStep);
10994
11554
  if (applyErr) {
10995
11555
  await teardownScratchFloci();
@@ -11005,7 +11565,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
11005
11565
  };
11006
11566
  process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
11007
11567
  }
11008
- if (existsSync14(join20(project, "package.json")) && !existsSync14(join20(project, "node_modules"))) {
11568
+ if (existsSync15(join22(project, "package.json")) && !existsSync15(join22(project, "node_modules"))) {
11009
11569
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
11010
11570
  `);
11011
11571
  const code = await spawnStep("npm", ["install"], project);
@@ -11016,7 +11576,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
11016
11576
  }
11017
11577
  }
11018
11578
  let degraded;
11019
- if (!existsSync14(join20(target, "node_modules", "@cdktf", "hcl2json"))) {
11579
+ if (!existsSync15(join22(target, "node_modules", "@cdktf", "hcl2json"))) {
11020
11580
  process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
11021
11581
  const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
11022
11582
  if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
@@ -11032,7 +11592,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
11032
11592
  );
11033
11593
  if (code !== 0) degraded = `chant carve advise exited ${code}`;
11034
11594
  }
11035
- const serving = !degraded && existsSync14(report) ? report : committed;
11595
+ const serving = !degraded && existsSync15(report) ? report : committed;
11036
11596
  if (degraded) {
11037
11597
  process.stderr.write(
11038
11598
  `behold demo carve: ${degraded}
@@ -11040,7 +11600,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
11040
11600
  `
11041
11601
  );
11042
11602
  }
11043
- if (!existsSync14(serving)) {
11603
+ if (!existsSync15(serving)) {
11044
11604
  process.stderr.write(`behold demo carve: no carve report at ${serving}
11045
11605
  `);
11046
11606
  process.exit(2);
@@ -11085,8 +11645,8 @@ async function runPreview(rest) {
11085
11645
  process.stderr.write("behold preview: --port must be a number\n");
11086
11646
  process.exit(2);
11087
11647
  }
11088
- const projectDir = resolve8(dirArg ?? process.cwd());
11089
- if (!existsSync14(projectDir)) {
11648
+ const projectDir = resolve11(dirArg ?? process.cwd());
11649
+ if (!existsSync15(projectDir)) {
11090
11650
  process.stderr.write(`behold preview: project not found at ${projectDir}
11091
11651
  `);
11092
11652
  process.exit(2);
@@ -11106,26 +11666,26 @@ async function runPreview(rest) {
11106
11666
  await startServer({ projectDir, port, env: "local", previewMode: true });
11107
11667
  }
11108
11668
  async function runExportCmd(rest) {
11109
- let outDir = resolve8("behold-export");
11669
+ let outDir = resolve11("behold-export");
11110
11670
  let env;
11111
11671
  let name;
11112
11672
  let dirArg;
11113
11673
  let emulator = false;
11114
11674
  for (let i = 0; i < rest.length; i++) {
11115
11675
  const a = rest[i];
11116
- if (a === "--out") outDir = resolve8(rest[++i]);
11676
+ if (a === "--out") outDir = resolve11(rest[++i]);
11117
11677
  else if (a === "--env") env = rest[++i];
11118
11678
  else if (a === "--name") name = rest[++i];
11119
11679
  else if (a === "--emulator") emulator = true;
11120
11680
  else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
11121
11681
  else if (!a.startsWith("-")) dirArg = a;
11122
11682
  }
11123
- const projectDir = resolve8(dirArg ?? process.cwd());
11683
+ const projectDir = resolve11(dirArg ?? process.cwd());
11124
11684
  if (emulator) {
11125
11685
  injectEmulatorEnv(env);
11126
11686
  env ??= "local";
11127
11687
  }
11128
- if (!existsSync14(projectDir)) {
11688
+ if (!existsSync15(projectDir)) {
11129
11689
  process.stderr.write(`behold export: project not found at ${projectDir}
11130
11690
  `);
11131
11691
  process.exit(2);