@intentius/behold 0.14.0 → 0.16.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 resolve10(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 resolve10(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 resolve10({ 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: resolve10,
876
876
  read,
877
877
  pathFor,
878
878
  readIfPresent,
@@ -1065,7 +1065,7 @@ 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";
1068
+ import { resolve as resolve9, dirname as dirname7, join as join21 } from "node:path";
1069
1069
  import { realpathSync, existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync2 } 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";
@@ -1148,7 +1148,7 @@ 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
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";
1151
+ import { basename, dirname as dirname5, join as join19, relative as relative5, resolve as resolve7, sep as sep4 } 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 resolve2 } 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,309 @@ 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 } 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) 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 byFile = /* @__PURE__ */ new Map();
1839
+ for (const s of states) for (const f of s.sourceFiles) byFile.set(resolve(f), s);
1840
+ if (byFile.size === 0) return { ir, joined: 0 };
1841
+ let joined = 0;
1842
+ const nodes = ir.nodes.map((n) => {
1843
+ const file = n.sourceLoc?.file;
1844
+ if (!file) return n;
1845
+ const abs = resolve(memberDir, file);
1846
+ const state = byFile.get(abs);
1847
+ if (!state) return n;
1848
+ joined++;
1849
+ const carve = {
1850
+ target: state.target,
1851
+ ...state.tfType ? { tfType: state.tfType } : {},
1852
+ stage: state.stage,
1853
+ graduated: state.graduated,
1854
+ ...state.at ? { at: state.at } : {},
1855
+ ...state.from ? { from: state.from } : {},
1856
+ file: relative(memberDir, abs).split("\\").join("/")
1857
+ };
1858
+ return { ...n, attrs: { ...n.attrs, carved: state.target, _carve: carve } };
1859
+ });
1860
+ return { ir: joined ? { ...ir, nodes } : ir, joined };
1861
+ }
1862
+ function emittedNodesFor(ir, states, memberDir) {
1863
+ const out = /* @__PURE__ */ new Map();
1864
+ if (!ir) return out;
1865
+ for (const n of joinCarvedSources(ir, states, memberDir).ir.nodes) {
1866
+ const carve = n.attrs._carve;
1867
+ if (carve && !out.has(carve.target)) out.set(carve.target, n);
1868
+ }
1869
+ return out;
1870
+ }
1871
+ function carveProgress(states, total) {
1872
+ let applied = 0;
1873
+ let bridged = 0;
1874
+ let emitted = 0;
1875
+ for (const s of states) {
1876
+ if (s.stage === "applied") applied++;
1877
+ else if (s.stage === "bridged") bridged++;
1878
+ else emitted++;
1879
+ }
1880
+ const inFlight = bridged + emitted;
1881
+ const parts = [];
1882
+ if (emitted) parts.push(`${emitted} emitted`);
1883
+ if (bridged) parts.push(`${bridged} bridged`);
1884
+ return {
1885
+ applied,
1886
+ bridged,
1887
+ emitted,
1888
+ inFlight,
1889
+ total: typeof total === "number" ? total : null,
1890
+ label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
1891
+ detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
1892
+ };
1893
+ }
1894
+ function applyCommandFor(state, labels = {}) {
1895
+ const env = state.ownership?.env ?? "<env>";
1896
+ const stack = state.ownership?.stack ?? "<stack>";
1897
+ const from = labels.from ?? state.from ?? "<terraform-dir>";
1898
+ const out = labels.out ?? state.outDir;
1899
+ return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
1900
+ }
1901
+ function carveStatePayload(states, total, labels = () => ({})) {
1902
+ const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
1903
+ return {
1904
+ manifests: list3.length,
1905
+ progress: carveProgress(list3, total),
1906
+ states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
1907
+ apply: { human: true, note: APPLY_IS_HUMAN }
1908
+ };
1909
+ }
1910
+ function splitCarveState(tfIr, states, opts = {}) {
1911
+ if (states.size === 0) return { tf: tfIr, graduated: [] };
1912
+ const shorten = opts.shorten ?? ((p) => p);
1913
+ const emitted = opts.emitted ?? /* @__PURE__ */ new Map();
1914
+ const declaredAttrs = (n) => Object.fromEntries(Object.entries(n.attrs ?? {}).filter(([k]) => !k.startsWith("_") && k !== "carved"));
1915
+ const source = (files) => files.map(shorten).join(", ");
1916
+ const gone = /* @__PURE__ */ new Set();
1917
+ const graduated = [];
1918
+ const nodes = [];
1919
+ for (const node of tfIr.nodes) {
1920
+ const state = states.get(node.id);
1921
+ if (!state) {
1922
+ nodes.push(node);
1923
+ continue;
1924
+ }
1925
+ const became = emitted.get(node.id);
1926
+ if (state.graduated) {
1927
+ gone.add(node.id);
1928
+ graduated.push({
1929
+ ...node,
1930
+ ...became ? { kind: became.kind, lexicon: became.lexicon, ...became.sourceLoc ? { sourceLoc: became.sourceLoc } : {} } : {},
1931
+ attrs: {
1932
+ ...node.attrs,
1933
+ ...became ? { ...declaredAttrs(became), chantEntity: `${became.kind} ${became.id}` } : {},
1934
+ _status: "good",
1935
+ carve: carveWordFor(state.stage),
1936
+ carveState: state.stage,
1937
+ ...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
1938
+ ...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
1939
+ ...state.at ? { carvedAt: state.at } : {},
1940
+ ...state.files.length ? { chantSource: source(state.files) } : {}
1941
+ }
1942
+ });
1943
+ continue;
1944
+ }
1945
+ nodes.push({
1946
+ ...node,
1947
+ attrs: {
1948
+ ...node.attrs,
1949
+ _status: carveToneFor(state.stage),
1950
+ carve: carveWordFor(state.stage),
1951
+ carveState: state.stage,
1952
+ ...became ? { chantEntity: `${became.kind} ${became.id}` } : {},
1953
+ ...state.at ? { carvedAt: state.at } : {},
1954
+ ...state.files.length ? { chantSource: source(state.files) } : {},
1955
+ ...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
1956
+ ...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
1957
+ }
1958
+ });
1959
+ }
1960
+ const byStack = {};
1961
+ for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
1962
+ const left = members.filter((id) => !gone.has(id));
1963
+ if (left.length) byStack[band] = left;
1964
+ }
1965
+ return {
1966
+ tf: {
1967
+ nodes,
1968
+ edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
1969
+ groups: { ...tfIr.groups, byStack }
1970
+ },
1971
+ graduated
1972
+ };
1973
+ }
1974
+ var GRADUATED_BAND = "carved \u2192 chant";
1975
+ function bandGraduated(tf, graduated) {
1976
+ if (graduated.length === 0) return tf;
1977
+ return {
1978
+ nodes: [...graduated, ...tf.nodes],
1979
+ edges: tf.edges,
1980
+ groups: {
1981
+ ...tf.groups,
1982
+ byStack: {
1983
+ [GRADUATED_BAND]: graduated.map((n) => n.id),
1984
+ ...tf.groups?.byStack ?? {}
1985
+ }
1986
+ }
1987
+ };
1988
+ }
1989
+ function carveStateNote(progress, states) {
1990
+ if (states.size === 0) return "";
1991
+ const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
1992
+ return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
1993
+ }
1994
+
1667
1995
  // src/chant.ts
1668
1996
  function envOverridesFor(opts) {
1669
1997
  const overrides = {};
@@ -1694,14 +2022,14 @@ function packageManifestFrom(req, name) {
1694
2022
  } catch {
1695
2023
  return void 0;
1696
2024
  }
1697
- let dir = dirname2(entry);
2025
+ let dir = dirname3(entry);
1698
2026
  for (; ; ) {
1699
2027
  try {
1700
- const pkg = createRequire(import.meta.url)(join3(dir, "package.json"));
2028
+ const pkg = createRequire(import.meta.url)(join4(dir, "package.json"));
1701
2029
  if (pkg.name === name) return { root: dir, manifest: pkg };
1702
2030
  } catch {
1703
2031
  }
1704
- const parent = dirname2(dir);
2032
+ const parent = dirname3(dir);
1705
2033
  if (parent === dir) break;
1706
2034
  dir = parent;
1707
2035
  }
@@ -1711,10 +2039,10 @@ function chantPackageFrom(req) {
1711
2039
  const found = packageManifestFrom(req, "@intentius/chant");
1712
2040
  if (!found) return void 0;
1713
2041
  const { root, manifest } = found;
1714
- return { root, bin: join3(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
2042
+ return { root, bin: join4(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
1715
2043
  }
1716
2044
  function resolveLexicons(projectDir, lexicons) {
1717
- const req = createRequire(join3(resolve(projectDir), "noop.js"));
2045
+ const req = createRequire(join4(resolve2(projectDir), "noop.js"));
1718
2046
  return lexicons.map((lexicon) => {
1719
2047
  const pkg = `@intentius/chant-lexicon-${lexicon}`;
1720
2048
  const found = packageManifestFrom(req, pkg);
@@ -1724,7 +2052,7 @@ function resolveLexicons(projectDir, lexicons) {
1724
2052
  function resolveChant(projectDir) {
1725
2053
  const own = chantPackageFrom(createRequire(import.meta.url));
1726
2054
  if (projectDir) {
1727
- const fromProject = chantPackageFrom(createRequire(join3(resolve(projectDir), "noop.js")));
2055
+ const fromProject = chantPackageFrom(createRequire(join4(resolve2(projectDir), "noop.js")));
1728
2056
  if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
1729
2057
  }
1730
2058
  if (own) return { source: "behold", ...own };
@@ -1732,8 +2060,8 @@ function resolveChant(projectDir) {
1732
2060
  }
1733
2061
  function chantFloor() {
1734
2062
  try {
1735
- const here = dirname2(fileURLToPath(import.meta.url));
1736
- const pkg = JSON.parse(readFileSync3(join3(here, "..", "package.json"), "utf8"));
2063
+ const here = dirname3(fileURLToPath(import.meta.url));
2064
+ const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
1737
2065
  return pkg.dependencies?.["@intentius/chant"]?.replace(/^[^0-9]*/, "") || void 0;
1738
2066
  } catch {
1739
2067
  return void 0;
@@ -1839,7 +2167,7 @@ function runCommandStream(cmd, args, cwd, onLine) {
1839
2167
  return { pid: proc.pid ?? -1, kill: () => proc.kill(), done };
1840
2168
  }
1841
2169
  function legacyGraphPath(projectDir) {
1842
- const src = join3(projectDir, "src");
2170
+ const src = join4(projectDir, "src");
1843
2171
  return existsSync3(src) ? src : projectDir;
1844
2172
  }
1845
2173
  async function graphPath(projectDir, opts = {}) {
@@ -1851,9 +2179,9 @@ async function graphPath(projectDir, opts = {}) {
1851
2179
  }
1852
2180
  if (info.stacks?.length) {
1853
2181
  const picked = opts.stack ? info.stacks.find((s) => s.name === opts.stack) : void 0;
1854
- return resolve(projectDir, (picked ?? info.stacks[0]).src);
2182
+ return resolve2(projectDir, (picked ?? info.stacks[0]).src);
1855
2183
  }
1856
- if (info.sourceDir) return resolve(projectDir, info.sourceDir);
2184
+ if (info.sourceDir) return resolve2(projectDir, info.sourceDir);
1857
2185
  return legacyGraphPath(projectDir);
1858
2186
  }
1859
2187
  function graphArgs(src, format, opts, components) {
@@ -1864,7 +2192,7 @@ async function graphIr(projectDir, opts = {}) {
1864
2192
  return runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
1865
2193
  }
1866
2194
  async function clusterRootGraphIr(projectDir, opts = {}) {
1867
- if (!existsSync3(join3(projectDir, "cluster"))) return void 0;
2195
+ if (!existsSync3(join4(projectDir, "cluster"))) return void 0;
1868
2196
  const { live: _live, overlay: _overlay, env: _env, ...sourceOnly } = opts;
1869
2197
  try {
1870
2198
  return await runChantJson(graphArgs("cluster", "ir", sourceOnly, false), projectDir, envOverridesFor(sourceOnly));
@@ -1882,6 +2210,9 @@ function componentStatusArgs(env) {
1882
2210
  function componentStatus(projectDir, env, opts = {}) {
1883
2211
  return runChantJson(componentStatusArgs(env), projectDir, envOverridesFor(opts));
1884
2212
  }
2213
+ function ciWorkflowName(env) {
2214
+ return `chant-components-${env}`;
2215
+ }
1885
2216
  var CI_FORGES = ["gitlab", "github", "forgejo"];
1886
2217
  function ciForgeFor(lexicons) {
1887
2218
  return CI_FORGES.find((forge) => lexicons.includes(forge));
@@ -1899,7 +2230,7 @@ function parseCiPipeline(stdout) {
1899
2230
  const script = Array.isArray(props?.script) ? props.script.map(String) : [];
1900
2231
  return { ...j, script };
1901
2232
  });
1902
- return { stages: parsed.stages, jobs };
2233
+ return { stages: parsed.stages, jobs, ...typeof parsed.env === "string" && parsed.env ? { env: parsed.env } : {} };
1903
2234
  }
1904
2235
  function ciPipeline(projectDir, opts = {}, forge = "gitlab") {
1905
2236
  const args = ciPipelineArgs(opts, forge);
@@ -1920,6 +2251,9 @@ function lifecycleDiffArgs(env) {
1920
2251
  function lifecycleDiffLive(projectDir, env, opts = {}) {
1921
2252
  return runChantJson(lifecycleDiffArgs(env), projectDir, envOverridesFor(opts));
1922
2253
  }
2254
+ function carveStatus(from, projectDir) {
2255
+ return runChantJson(carveStatusArgs(from), projectDir);
2256
+ }
1923
2257
  function helmRendersArgs() {
1924
2258
  return ["helm", "renders", "--json"];
1925
2259
  }
@@ -1996,8 +2330,8 @@ function mergeClusterRoot(ir, clusterIr, running) {
1996
2330
  }
1997
2331
 
1998
2332
  // 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";
2333
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
2334
+ import { join as join5 } from "node:path";
2001
2335
  var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
2002
2336
  function declaredChartNames(ir) {
2003
2337
  const names = /* @__PURE__ */ new Set();
@@ -2017,15 +2351,15 @@ function matchesDeclaredChart(chart, declared) {
2017
2351
  }
2018
2352
  function discoverReleaseUnits(projectDir) {
2019
2353
  const owners = /* @__PURE__ */ new Map();
2020
- const roots = [join4(projectDir, "src"), join4(projectDir, "ops")];
2354
+ const roots = [join5(projectDir, "src"), join5(projectDir, "ops")];
2021
2355
  const files = [];
2022
2356
  const walk = (dir, depth) => {
2023
2357
  if (depth > 4 || !existsSync4(dir)) return;
2024
- for (const f of readdirSync(dir)) {
2025
- const p = join4(dir, f);
2358
+ for (const f of readdirSync2(dir)) {
2359
+ const p = join5(dir, f);
2026
2360
  let s;
2027
2361
  try {
2028
- s = statSync(p);
2362
+ s = statSync2(p);
2029
2363
  } catch {
2030
2364
  continue;
2031
2365
  }
@@ -2037,7 +2371,7 @@ function discoverReleaseUnits(projectDir) {
2037
2371
  for (const file of files) {
2038
2372
  let content;
2039
2373
  try {
2040
- content = readFileSync4(file, "utf8");
2374
+ content = readFileSync5(file, "utf8");
2041
2375
  } catch {
2042
2376
  continue;
2043
2377
  }
@@ -2090,8 +2424,8 @@ function synthesizeHelmReleases(ir, observed, owners) {
2090
2424
 
2091
2425
  // src/gh-run.ts
2092
2426
  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";
2427
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
2428
+ import { join as join6 } from "node:path";
2095
2429
 
2096
2430
  // src/ci-run.ts
2097
2431
  function pipelineProgress(pipeline) {
@@ -2145,19 +2479,19 @@ function finishPipelineProgress(state, exitCode) {
2145
2479
  }
2146
2480
 
2147
2481
  // src/gh-run.ts
2148
- var defaultGhExec = (args) => new Promise((resolve9) => {
2482
+ var defaultGhExec = (args) => new Promise((resolve10) => {
2149
2483
  let out = "";
2150
2484
  let proc;
2151
2485
  try {
2152
2486
  proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
2153
2487
  } catch {
2154
- resolve9({ code: 127, out: "" });
2488
+ resolve10({ code: 127, out: "" });
2155
2489
  return;
2156
2490
  }
2157
2491
  proc.stdout.on("data", (d) => out += d);
2158
2492
  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 }));
2493
+ proc.on("error", () => resolve10({ code: 127, out }));
2494
+ proc.on("close", (code) => resolve10({ code: code ?? 1, out }));
2161
2495
  });
2162
2496
  async function ghReady(exec = defaultGhExec) {
2163
2497
  const { code, out } = await exec(["auth", "status"]);
@@ -2176,31 +2510,49 @@ function parseWorkflow(file, text) {
2176
2510
  const jobs = d.jobs && typeof d.jobs === "object" && !Array.isArray(d.jobs) ? Object.keys(d.jobs) : [];
2177
2511
  const on = d.on ?? d[true];
2178
2512
  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 };
2513
+ return { file, jobIds: jobs, dispatchable, ...typeof d.name === "string" && d.name ? { name: d.name } : {} };
2180
2514
  }
2181
- function pickWorkflow(projectDir, pipeline) {
2182
- const dir = join5(projectDir, ".github", "workflows");
2515
+ function committedWorkflows(projectDir) {
2516
+ const dir = join6(projectDir, ".github", "workflows");
2183
2517
  let files;
2184
2518
  try {
2185
- files = readdirSync2(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
2519
+ files = readdirSync3(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
2186
2520
  } catch {
2187
- return void 0;
2521
+ return [];
2188
2522
  }
2189
- const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
2190
- let best;
2523
+ const out = [];
2191
2524
  for (const f of files) {
2192
- let text;
2193
2525
  try {
2194
- text = readFileSync5(join5(dir, f), "utf8");
2526
+ out.push(parseWorkflow(f, readFileSync6(join6(dir, f), "utf8")));
2195
2527
  } catch {
2196
- continue;
2197
2528
  }
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
2529
  }
2203
- return best?.info;
2530
+ return out;
2531
+ }
2532
+ function resolveWorkflow(projectDir, pipeline, designated) {
2533
+ const all = committedWorkflows(projectDir);
2534
+ if (designated) {
2535
+ const hit = all.find((w) => w.file === designated);
2536
+ 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)` };
2537
+ 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` };
2538
+ return { workflow: hit };
2539
+ }
2540
+ const dispatchable = all.filter((w) => w.dispatchable);
2541
+ if (pipeline.env) {
2542
+ const named = dispatchable.filter((w) => w.name === ciWorkflowName(pipeline.env));
2543
+ if (named.length === 1) return { workflow: named[0] };
2544
+ 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` };
2545
+ }
2546
+ const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
2547
+ const scored = dispatchable.map((w) => ({ w, overlap: w.jobIds.filter((id) => wanted.has(id)).length })).filter((x) => x.overlap > 0).sort((a, b) => b.overlap - a.overlap);
2548
+ if (scored.length === 0) {
2549
+ return { reason: "no committed workflow_dispatch workflow whose jobs match the generated pipeline \u2014 commit one (or add `workflow_dispatch:` to its `on:` block)" };
2550
+ }
2551
+ if (scored.length > 1 && scored[1].overlap === scored[0].overlap) {
2552
+ const tied = scored.filter((x) => x.overlap === scored[0].overlap).map((x) => x.w.file);
2553
+ 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` };
2554
+ }
2555
+ return { workflow: scored[0].w };
2204
2556
  }
2205
2557
  async function latestRunId(workflowFile, exec = defaultGhExec) {
2206
2558
  const { code, out } = await exec(["run", "list", "--workflow", workflowFile, "--limit", "1", "--json", "databaseId"]);
@@ -2234,6 +2586,7 @@ function runViewToProgress(pipeline, view) {
2234
2586
  return { ...w, status };
2235
2587
  })
2236
2588
  };
2589
+ if (view.url) state = { ...state, url: view.url };
2237
2590
  if (view.status === "completed") {
2238
2591
  return { ...state, status: view.conclusion === "success" ? "ok" : "failed" };
2239
2592
  }
@@ -2267,8 +2620,9 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
2267
2620
  return 1;
2268
2621
  }
2269
2622
  deps.onLine(`\u25CF run ${runId} started \u2014 following`);
2623
+ let urlSaid = false;
2270
2624
  for (; ; ) {
2271
- const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs"]);
2625
+ const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs,url"]);
2272
2626
  if (code === 0) {
2273
2627
  let view;
2274
2628
  try {
@@ -2277,6 +2631,10 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
2277
2631
  view = void 0;
2278
2632
  }
2279
2633
  if (view) {
2634
+ if (view.url && !urlSaid) {
2635
+ urlSaid = true;
2636
+ 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`);
2637
+ }
2280
2638
  deps.onProgress(runViewToProgress(pipeline, view));
2281
2639
  if (view.status === "completed") {
2282
2640
  deps.onLine(`\u25A0 run ${runId} ${view.conclusion ?? "completed"}`);
@@ -2313,15 +2671,15 @@ function joinCiProgress(ir, state) {
2313
2671
  // src/operator.ts
2314
2672
  var ANSI = /\x1b\[[0-9;]*m/g;
2315
2673
  var strip = (s) => s.replace(ANSI, "");
2316
- var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2674
+ var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2317
2675
  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
2676
  function readRow(value) {
2319
- if (!isRecord(value)) return null;
2677
+ if (!isRecord2(value)) return null;
2320
2678
  if (typeof value.op !== "string" || typeof value.env !== "string") return null;
2321
2679
  if (!Array.isArray(value.pendingGates)) return null;
2322
2680
  const gates = [];
2323
2681
  for (const g of value.pendingGates) {
2324
- if (!isRecord(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
2682
+ if (!isRecord2(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
2325
2683
  gates.push({
2326
2684
  rule: g.rule,
2327
2685
  gate: g.gate,
@@ -2333,9 +2691,9 @@ function readRow(value) {
2333
2691
  ...typeof g.url === "string" && g.url ? { url: g.url } : {}
2334
2692
  });
2335
2693
  }
2336
- const tick = isRecord(value.lastTick) ? value.lastTick : void 0;
2694
+ const tick = isRecord2(value.lastTick) ? value.lastTick : void 0;
2337
2695
  const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
2338
- const lease = isRecord(value.lease) ? value.lease : void 0;
2696
+ const lease = isRecord2(value.lease) ? value.lease : void 0;
2339
2697
  return {
2340
2698
  op: value.op,
2341
2699
  env: value.env,
@@ -2433,7 +2791,7 @@ var CHANT = "chant";
2433
2791
  var CONVERGE_TICK = "converge-tick";
2434
2792
  var CRONJOB_KIND = "K8s::Batch::CronJob";
2435
2793
  var NAMESPACE_KIND = "K8s::Core::Namespace";
2436
- var rec = (v) => isRecord(v) ? v : void 0;
2794
+ var rec = (v) => isRecord2(v) ? v : void 0;
2437
2795
  var str = (v) => typeof v === "string" && v ? v : void 0;
2438
2796
  var labelsOf = (n) => rec(rec(n.attrs?.metadata)?.labels) ?? {};
2439
2797
  function operatorHomes(ir) {
@@ -2541,10 +2899,10 @@ function tickFreshness(at, now = Date.now()) {
2541
2899
  }
2542
2900
  var RECONCILIATIONS = /* @__PURE__ */ new Set(["reconciled", "unrecorded", "stale", "drifted", "unknown"]);
2543
2901
  function readVerdict(value) {
2544
- if (!isRecord(value)) return null;
2902
+ if (!isRecord2(value)) return null;
2545
2903
  if (typeof value.component !== "string" || !value.component) return null;
2546
2904
  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;
2905
+ const unobserved = isRecord2(value.unobserved) && typeof value.unobserved.reason === "string" ? value.unobserved : void 0;
2548
2906
  return {
2549
2907
  component: value.component,
2550
2908
  reconciliation: value.reconciliation,
@@ -2655,12 +3013,12 @@ function operatorLogArgs(window) {
2655
3013
  ];
2656
3014
  }
2657
3015
  function readTickRecord(value) {
2658
- if (!isRecord(value)) return null;
3016
+ if (!isRecord2(value)) return null;
2659
3017
  if (typeof value.op !== "string" || typeof value.timestamp !== "string" || typeof value.log !== "string") return null;
2660
3018
  return value;
2661
3019
  }
2662
3020
  function readResolutionRecord(value) {
2663
- if (!isRecord(value)) return null;
3021
+ if (!isRecord2(value)) return null;
2664
3022
  const needed = ["op", "gate", "resolvedBy", "timestamp"];
2665
3023
  if (needed.some((k) => typeof value[k] !== "string")) return null;
2666
3024
  return value;
@@ -2721,10 +3079,10 @@ function readOperatorLog(run3) {
2721
3079
  remedy: "behold reads `{entries: [{kind, timestamp, record}], malformed: {converge, gates}}`."
2722
3080
  }
2723
3081
  });
2724
- if (!isRecord(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
3082
+ if (!isRecord2(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
2725
3083
  const entries = [];
2726
3084
  for (const value of parsed.entries) {
2727
- if (!isRecord(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
3085
+ if (!isRecord2(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
2728
3086
  if (value.kind === "tick") {
2729
3087
  const record = readTickRecord(value.record);
2730
3088
  if (!record) return unreadable("a tick entry with no `op`/`timestamp`/`log`");
@@ -2737,7 +3095,7 @@ function readOperatorLog(run3) {
2737
3095
  return unreadable(`an entry of an unknown kind "${String(value.kind)}"`);
2738
3096
  }
2739
3097
  }
2740
- const counts = isRecord(parsed.malformed) ? parsed.malformed : {};
3098
+ const counts = isRecord2(parsed.malformed) ? parsed.malformed : {};
2741
3099
  const count = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
2742
3100
  return { ok: true, entries, malformed: { converge: count(counts.converge), gates: count(counts.gates) } };
2743
3101
  }
@@ -2871,6 +3229,50 @@ function componentStatusColor(row) {
2871
3229
  return "neutral";
2872
3230
  }
2873
3231
  }
3232
+ function httpUrl(url) {
3233
+ if (typeof url !== "string" || !url) return void 0;
3234
+ try {
3235
+ const p = new URL(url);
3236
+ return p.protocol === "http:" || p.protocol === "https:" ? url : void 0;
3237
+ } catch {
3238
+ return void 0;
3239
+ }
3240
+ }
3241
+ function releaseOrigin(rec6) {
3242
+ const origin = rec6.runOrigin;
3243
+ if (origin && typeof origin.forge === "string") {
3244
+ const url = httpUrl(origin.url);
3245
+ return {
3246
+ forge: origin.forge,
3247
+ originSource: "record",
3248
+ ...origin.repo ? { repo: origin.repo } : {},
3249
+ ...url ? { url } : {}
3250
+ };
3251
+ }
3252
+ return { forge: rec6.runId.startsWith("local-") ? "local" : "unknown", originSource: "inferred" };
3253
+ }
3254
+ var FORGE_WORD = {
3255
+ github: "github",
3256
+ gitlab: "gitlab",
3257
+ op: "op",
3258
+ local: "laptop",
3259
+ unknown: "?"
3260
+ };
3261
+ function releaseChip(status) {
3262
+ return `${status.gitSha.slice(0, 7)} via ${FORGE_WORD[status.forge]}`;
3263
+ }
3264
+ function releaseStatus(rec6) {
3265
+ if (!rec6) return void 0;
3266
+ return {
3267
+ runId: rec6.runId,
3268
+ ...releaseOrigin(rec6),
3269
+ gitSha: rec6.gitSha,
3270
+ digest: rec6.digest,
3271
+ timestamp: rec6.timestamp,
3272
+ actor: rec6.actor,
3273
+ ...rec6.approver ? { approver: rec6.approver } : {}
3274
+ };
3275
+ }
2874
3276
  function joinComponentStatus(ir, rows) {
2875
3277
  const byComponent = new Map(rows.map((r) => [r.component, r]));
2876
3278
  return {
@@ -2878,10 +3280,12 @@ function joinComponentStatus(ir, rows) {
2878
3280
  nodes: ir.nodes.map((n) => {
2879
3281
  const row = byComponent.get(n.id);
2880
3282
  if (!row) return n;
3283
+ const release = releaseStatus(row.recorded);
2881
3284
  return {
2882
3285
  ...n,
2883
3286
  attrs: {
2884
3287
  ...n.attrs,
3288
+ ...release ? { release: releaseChip(release), _release: release } : {},
2885
3289
  _status: componentStatusColor(row),
2886
3290
  _liveStatus: {
2887
3291
  reconciliation: row.reconciliation,
@@ -3323,7 +3727,9 @@ function applyHelmArtifacts(ir, observed) {
3323
3727
  release: key.replace(/^release\//, ""),
3324
3728
  ...o.status ? { status: o.status } : {},
3325
3729
  ...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
3326
- ...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {}
3730
+ ...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {},
3731
+ ...typeof o.attributes?.inputDigest === "string" ? { inputDigest: o.attributes.inputDigest } : {},
3732
+ ...typeof o.attributes?.contentDigest === "string" ? { contentDigest: o.attributes.contentDigest } : {}
3327
3733
  };
3328
3734
  attrs._artifact = match;
3329
3735
  attrs._status = o.status === "deployed" ? "good" : "warn";
@@ -3332,37 +3738,35 @@ function applyHelmArtifacts(ir, observed) {
3332
3738
  return { charts, installed };
3333
3739
  }
3334
3740
 
3335
- // src/estate.ts
3336
- import { statSync as statSync3 } from "node:fs";
3337
- import { availableParallelism } from "node:os";
3338
- import { join as join7, resolve as resolve3, sep } from "node:path";
3339
- import { composeStacks, shortStackNames } from "@intentius/pinhole";
3741
+ // src/carve-discovery.ts
3742
+ import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
3743
+ import { join as join8 } from "node:path";
3340
3744
 
3341
3745
  // src/member-ir.ts
3342
3746
  import { createHash } from "node:crypto";
3343
- import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
3344
- import { join as join6, relative, resolve as resolve2 } from "node:path";
3747
+ import { readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
3748
+ import { join as join7, relative as relative2, resolve as resolve3 } from "node:path";
3345
3749
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
3346
3750
  function memberIrCacheSize(env = process.env) {
3347
3751
  const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
3348
3752
  return Number.isInteger(override) && override >= 0 ? override : 64;
3349
3753
  }
3350
3754
  function memberSourceStamp(dir) {
3351
- const root = resolve2(dir);
3755
+ const root = resolve3(dir);
3352
3756
  const h = createHash("sha1");
3353
3757
  let any = false;
3354
3758
  const walk = (at) => {
3355
- const entries = readdirSync3(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
3759
+ const entries = readdirSync4(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
3356
3760
  for (const e of entries) {
3357
3761
  if (SKIP.has(e.name)) continue;
3358
- const path = join6(at, e.name);
3762
+ const path = join7(at, e.name);
3359
3763
  if (e.isDirectory()) {
3360
3764
  walk(path);
3361
3765
  continue;
3362
3766
  }
3363
3767
  if (!e.isFile()) continue;
3364
- const st = statSync2(path);
3365
- h.update(`${relative(root, path)}\0${st.mtimeMs}\0${st.size}
3768
+ const st = statSync3(path);
3769
+ h.update(`${relative2(root, path)}\0${st.mtimeMs}\0${st.size}
3366
3770
  `);
3367
3771
  any = true;
3368
3772
  }
@@ -3377,7 +3781,7 @@ function memberSourceStamp(dir) {
3377
3781
  function cacheKey(dir, opts) {
3378
3782
  const chant = resolveChant(dir);
3379
3783
  const canonical = JSON.stringify(opts, Object.keys(opts).sort());
3380
- return `${resolve2(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
3784
+ return `${resolve3(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
3381
3785
  }
3382
3786
  var cache = /* @__PURE__ */ new Map();
3383
3787
  var hits = 0;
@@ -3387,7 +3791,7 @@ function invalidateMember(dir) {
3387
3791
  cache.clear();
3388
3792
  return;
3389
3793
  }
3390
- const root = resolve2(dir);
3794
+ const root = resolve3(dir);
3391
3795
  for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
3392
3796
  }
3393
3797
  async function memberIr(dir, opts = {}) {
@@ -3402,18 +3806,82 @@ async function memberIr(dir, opts = {}) {
3402
3806
  cache.set(key, hit);
3403
3807
  return structuredClone(hit.ir);
3404
3808
  }
3405
- misses++;
3406
- const ir = await graphIr(dir, opts);
3407
- if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
3408
- cache.delete(key);
3409
- cache.set(key, { dir: resolve2(dir), stamp, ir });
3410
- for (const oldest of cache.keys()) {
3411
- if (cache.size <= size) break;
3412
- cache.delete(oldest);
3809
+ misses++;
3810
+ const ir = await graphIr(dir, opts);
3811
+ if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
3812
+ cache.delete(key);
3813
+ cache.set(key, { dir: resolve3(dir), stamp, ir });
3814
+ for (const oldest of cache.keys()) {
3815
+ if (cache.size <= size) break;
3816
+ cache.delete(oldest);
3817
+ }
3818
+ return structuredClone(ir);
3819
+ }
3820
+
3821
+ // src/carve-discovery.ts
3822
+ function carveStatusReader(projectDir) {
3823
+ return async (dir) => {
3824
+ const via = projectDir ?? dir;
3825
+ if (!meetsFloor(resolveChant(via).version, CARVE_STATUS_FLOOR)) return void 0;
3826
+ return carveStatus(dir, via).catch(() => void 0);
3827
+ };
3828
+ }
3829
+ var SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".chant", ".terraform", "cdk.out"]);
3830
+ function hasAnyCarveManifest(dir, io = fsIo) {
3831
+ const walk = (d) => {
3832
+ let entries;
3833
+ try {
3834
+ entries = io.readdir(d);
3835
+ } catch {
3836
+ return false;
3837
+ }
3838
+ for (const name of entries) {
3839
+ if (name.endsWith(CARVE_MANIFEST_SUFFIX)) return true;
3840
+ if (name.startsWith(".") || SKIP2.has(name)) continue;
3841
+ const full = join8(d, name);
3842
+ if (io.isDirectory(full) && walk(full)) return true;
3843
+ }
3844
+ return false;
3845
+ };
3846
+ return walk(dir);
3847
+ }
3848
+ var fsIo = {
3849
+ readdir: (d) => readdirSync5(d),
3850
+ isDirectory: (p) => {
3851
+ try {
3852
+ return statSync4(p).isDirectory();
3853
+ } catch {
3854
+ return false;
3855
+ }
3856
+ }
3857
+ };
3858
+ var cache2 = /* @__PURE__ */ new Map();
3859
+ async function carveStatesFor(dir) {
3860
+ const stamp = memberSourceStamp(dir);
3861
+ const held = cache2.get(dir);
3862
+ if (held && stamp !== void 0 && held.stamp === stamp) return held.states;
3863
+ const states = hasAnyCarveManifest(dir) ? await discoverCarveStates([dir], carveStatusReader(), nodeCarveIo, MANIFEST_SCAN_DEPTH) : /* @__PURE__ */ new Map();
3864
+ if (stamp !== void 0) cache2.set(dir, { stamp, states });
3865
+ return states;
3866
+ }
3867
+ async function carveStatesUnder(dirs) {
3868
+ const rank = { emitted: 0, bridged: 1, applied: 2 };
3869
+ const out = /* @__PURE__ */ new Map();
3870
+ for (const dir of dirs) {
3871
+ for (const s of (await carveStatesFor(dir)).values()) {
3872
+ const held = out.get(s.target);
3873
+ if (!held || rank[s.stage] > rank[held.stage]) out.set(s.target, s);
3874
+ }
3413
3875
  }
3414
- return structuredClone(ir);
3876
+ return out;
3415
3877
  }
3416
3878
 
3879
+ // src/estate.ts
3880
+ import { statSync as statSync5 } from "node:fs";
3881
+ import { availableParallelism } from "node:os";
3882
+ import { join as join9, resolve as resolve4, sep } from "node:path";
3883
+ import { composeStacks, shortStackNames } from "@intentius/pinhole";
3884
+
3417
3885
  // src/zoom-notes.ts
3418
3886
  function zoomNote(zoom, ir, compositeEdgesAttached) {
3419
3887
  const empty = ir.nodes.length === 0;
@@ -3516,7 +3984,10 @@ async function composeEstate(projectDirs, opts = {}) {
3516
3984
  const names = shortStackNames(projectDirs);
3517
3985
  const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
3518
3986
  name: names[i],
3519
- ir: await memberIr(dir, opts)
3987
+ // chant#2040: the manifests a member holds name the files its carved
3988
+ // entities were emitted into — joined here, where the member's root is
3989
+ // known, onto a COPY (the member IR is the #312 cache's own object).
3990
+ ir: joinCarvedSources(await memberIr(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
3520
3991
  }));
3521
3992
  return composeStacks(stacks);
3522
3993
  }
@@ -3537,7 +4008,7 @@ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
3537
4008
  var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
3538
4009
  var realIsDir = (p) => {
3539
4010
  try {
3540
- return statSync3(p).isDirectory();
4011
+ return statSync5(p).isDirectory();
3541
4012
  } catch {
3542
4013
  return false;
3543
4014
  }
@@ -3567,11 +4038,11 @@ function declaredSegments(path) {
3567
4038
  function pathAlignment(declared, dir, isDir = realIsDir) {
3568
4039
  const want = declaredSegments(declared);
3569
4040
  if (!want) return 0;
3570
- const root = resolve3(dir);
4041
+ const root = resolve4(dir);
3571
4042
  const have = root.split(sep).filter(Boolean);
3572
4043
  for (let k = Math.min(want.length, have.length); k >= 1; k--) {
3573
4044
  if (!have.slice(-k).every((s, i) => s === want[i])) continue;
3574
- if (!isDir(join7(root, ...want.slice(k)))) continue;
4045
+ if (!isDir(join9(root, ...want.slice(k)))) continue;
3575
4046
  return k;
3576
4047
  }
3577
4048
  return 0;
@@ -3601,10 +4072,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
3601
4072
  if (!winner || tied) continue;
3602
4073
  const prev = best.get(winner.dir);
3603
4074
  if (prev === "ambiguous") continue;
3604
- const join21 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
3605
- if (!prev) best.set(winner.dir, { score: winner.score, join: join21 });
4075
+ const join22 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
4076
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join22 });
3606
4077
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
3607
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join21 });
4078
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join22 });
3608
4079
  }
3609
4080
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
3610
4081
  }
@@ -3645,7 +4116,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
3645
4116
  const reason = firstLine2(err);
3646
4117
  try {
3647
4118
  const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
3648
- const src = await memberIr(dir, srcOpts);
4119
+ const src = joinCarvedSources(await memberIr(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
3649
4120
  for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
3650
4121
  stacks[i] = { name, ir: src };
3651
4122
  unobserved.push({ name, reason });
@@ -3668,6 +4139,36 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
3668
4139
  }
3669
4140
 
3670
4141
  // src/helm-drift.ts
4142
+ function renderIdentity(record, observed) {
4143
+ const declared = {
4144
+ ...record.inputDigest ? { inputDigest: record.inputDigest } : {},
4145
+ ...record.contentDigest ? { contentDigest: record.contentDigest } : {}
4146
+ };
4147
+ const a = observed?.observation.attributes ?? {};
4148
+ const obs = {
4149
+ ...observed ? { release: observed.key.replace(/^release\//, "") } : {},
4150
+ ...typeof a.inputDigest === "string" ? { inputDigest: a.inputDigest } : {},
4151
+ ...typeof a.contentDigest === "string" ? { contentDigest: a.contentDigest } : {}
4152
+ };
4153
+ const base = { declared, observed: obs };
4154
+ if (!declared.inputDigest && !declared.contentDigest) return { match: "unpinned", ...base };
4155
+ if (!obs.inputDigest && !obs.contentDigest) return { match: "unrecorded", ...base };
4156
+ if (declared.contentDigest && obs.contentDigest) {
4157
+ return { match: declared.contentDigest === obs.contentDigest ? "content" : "mismatch", ...base };
4158
+ }
4159
+ if (declared.inputDigest && obs.inputDigest) {
4160
+ return { match: declared.inputDigest === obs.inputDigest ? "input" : "mismatch", ...base };
4161
+ }
4162
+ return { match: "unrecorded", ...base };
4163
+ }
4164
+ function observedReleaseFor(record, observed, namespace) {
4165
+ if (!observed) return void 0;
4166
+ const candidates = Object.entries(observed).filter(([key2]) => key2.startsWith("release/") && key2.split("/").pop() === record.name);
4167
+ if (candidates.length === 0) return void 0;
4168
+ const scoped = namespace ? candidates.find(([key2]) => key2 === `release/${namespace}/${record.name}`) : void 0;
4169
+ const [key, observation] = scoped ?? candidates[0];
4170
+ return { key, observation };
4171
+ }
3671
4172
  function countChanges(docs) {
3672
4173
  return docs.reduce((n, d) => n + d.changes.length, 0);
3673
4174
  }
@@ -3690,8 +4191,9 @@ function provenanceOf(live) {
3690
4191
  sourceRef: m.sourceRef
3691
4192
  };
3692
4193
  }
3693
- function helmRenderReport(record, live) {
3694
- const base = { render: record.name, chart: record.chart };
4194
+ function helmRenderReport(record, live, observed) {
4195
+ const identity = observed ? { identity: renderIdentity(record, observedReleaseFor(record, observed, live?.manifest.namespace)) } : {};
4196
+ const base = { render: record.name, chart: record.chart, ...identity };
3695
4197
  if (!live) {
3696
4198
  return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
3697
4199
  }
@@ -3715,21 +4217,24 @@ function helmRenderReport(record, live) {
3715
4217
  }
3716
4218
  return { ...base, verdict: "in-sync", counts, provenance };
3717
4219
  }
3718
- async function readHelmRenderDrift(projectDir, env, opts = {}) {
4220
+ async function readHelmRenderDrift(projectDir, env, opts = {}, observed) {
3719
4221
  const report = await helmRenders(projectDir, opts).catch(() => void 0);
3720
4222
  const records = report?.records ?? [];
3721
4223
  if (records.length === 0) return [];
3722
4224
  return mapPool(records, estateReadPool(records.length), async (record) => {
3723
- if (!record.contentDigest) return helmRenderReport(record, void 0);
4225
+ if (!record.contentDigest) return helmRenderReport(record, void 0, observed);
3724
4226
  const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
3725
- return helmRenderReport(record, live);
4227
+ return helmRenderReport(record, live, observed);
3726
4228
  });
3727
4229
  }
3728
4230
  function chartNameOf2(node) {
3729
4231
  const name = node.attrs?.name;
3730
4232
  return typeof name === "string" && name.length > 0 ? name : void 0;
3731
4233
  }
3732
- var SEVERITY = { drifted: 2, unobserved: 1, "in-sync": 0 };
4234
+ var SEVERITY = { drifted: 3, unobserved: 1, "in-sync": 0 };
4235
+ function severityOf(r) {
4236
+ return Math.max(SEVERITY[r.verdict], r.identity?.match === "mismatch" ? 2 : 0);
4237
+ }
3733
4238
  function applyHelmRenderDrift(ir, reports) {
3734
4239
  if (reports.length === 0) return { charts: 0, drifted: 0 };
3735
4240
  const byChart = /* @__PURE__ */ new Map();
@@ -3745,11 +4250,11 @@ function applyHelmRenderDrift(ir, reports) {
3745
4250
  const chartName = chartNameOf2(node);
3746
4251
  const matched = chartName ? byChart.get(chartName) : void 0;
3747
4252
  if (!matched || matched.length === 0) continue;
3748
- const worst = matched.reduce((a, b) => SEVERITY[b.verdict] > SEVERITY[a.verdict] ? b : a);
4253
+ const worst = matched.reduce((a, b) => severityOf(b) > severityOf(a) ? b : a);
3749
4254
  const attrs = node.attrs ??= {};
3750
4255
  attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
3751
4256
  charts++;
3752
- if (worst.verdict === "drifted") {
4257
+ if (worst.verdict === "drifted" || matched.some((r) => r.identity?.match === "mismatch")) {
3753
4258
  attrs._status = "warn";
3754
4259
  drifted++;
3755
4260
  }
@@ -4289,7 +4794,7 @@ function projectHelmLogical(ir) {
4289
4794
 
4290
4795
  // src/logical-kustomize.ts
4291
4796
  import { existsSync as existsSync5 } from "node:fs";
4292
- import { join as join8 } from "node:path";
4797
+ import { join as join10 } from "node:path";
4293
4798
  function overlayBoxTitle(name) {
4294
4799
  return `overlay ${name}`;
4295
4800
  }
@@ -4309,7 +4814,7 @@ function kustomizationRoot(bases, dir, exists) {
4309
4814
  let current = dir;
4310
4815
  for (let i = 0; i < 4 && current; i++) {
4311
4816
  for (const base of bases) {
4312
- if (exists(join8(base, current, "kustomization.yaml")) || exists(join8(base, current, "kustomization.yml"))) {
4817
+ if (exists(join10(base, current, "kustomization.yaml")) || exists(join10(base, current, "kustomization.yml"))) {
4313
4818
  return current;
4314
4819
  }
4315
4820
  }
@@ -5040,11 +5545,11 @@ import {
5040
5545
  } from "@intentius/pinhole";
5041
5546
 
5042
5547
  // src/icon-packs.ts
5043
- import { readFileSync as readFileSync6 } from "node:fs";
5044
- import { dirname as dirname3, join as join9 } from "node:path";
5548
+ import { readFileSync as readFileSync7 } from "node:fs";
5549
+ import { dirname as dirname4, join as join11 } from "node:path";
5045
5550
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5046
- var ICON_ROOT = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "web", "icons");
5047
- var cache2 = /* @__PURE__ */ new Map();
5551
+ var ICON_ROOT = join11(dirname4(fileURLToPath2(import.meta.url)), "..", "web", "icons");
5552
+ var cache3 = /* @__PURE__ */ new Map();
5048
5553
  function unprefix(body) {
5049
5554
  return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
5050
5555
  }
@@ -5063,9 +5568,9 @@ function plate(glyph) {
5063
5568
  return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
5064
5569
  }
5065
5570
  function loadIcon(rel) {
5066
- const hit = cache2.get(rel);
5571
+ const hit = cache3.get(rel);
5067
5572
  if (hit) return hit;
5068
- const raw = readFileSync6(join9(ICON_ROOT, `${rel}.svg`), "utf8");
5573
+ const raw = readFileSync7(join11(ICON_ROOT, `${rel}.svg`), "utf8");
5069
5574
  const open = raw.indexOf("<svg");
5070
5575
  const openEnd = raw.indexOf(">", open);
5071
5576
  const close = raw.lastIndexOf("</svg");
@@ -5074,7 +5579,7 @@ function loadIcon(rel) {
5074
5579
  if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
5075
5580
  const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
5076
5581
  const glyph = PLATED.has(rel) ? plate(bare) : bare;
5077
- cache2.set(rel, glyph);
5582
+ cache3.set(rel, glyph);
5078
5583
  return glyph;
5079
5584
  }
5080
5585
  var K8S_ICON_BY_KIND = {
@@ -5130,9 +5635,9 @@ function helmIconFor(kind) {
5130
5635
  var SUPPORTED_MAJOR = 1;
5131
5636
  var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
5132
5637
  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`.";
5133
- var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5638
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5134
5639
  function parseCarveReport(value) {
5135
- if (!isRecord2(value)) {
5640
+ if (!isRecord3(value)) {
5136
5641
  return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
5137
5642
  }
5138
5643
  if (!Array.isArray(value.resources)) {
@@ -5142,7 +5647,7 @@ function parseCarveReport(value) {
5142
5647
  );
5143
5648
  }
5144
5649
  const bad = value.resources.findIndex(
5145
- (r) => !isRecord2(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
5650
+ (r) => !isRecord3(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
5146
5651
  );
5147
5652
  if (bad >= 0) {
5148
5653
  return refuse(
@@ -5203,368 +5708,120 @@ function tfTypeOf(address, kind) {
5203
5708
  const dot = address.indexOf(".");
5204
5709
  return dot > 0 ? address.slice(0, dot) : address;
5205
5710
  }
5206
- var directionOf = (e) => e.direction ?? "inbound";
5207
- function boundaryEdgesOf(r) {
5208
- const b = r.boundary;
5209
- if (!b) return [];
5210
- const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
5211
- return list3.filter((e) => isRecord2(e) && typeof e.survivor === "string" && typeof e.carved === "string");
5212
- }
5213
- function scoreArithmetic(r) {
5214
- const b = r.breakdown;
5215
- if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
5216
- const p = b.penalties ?? {};
5217
- const terms = [];
5218
- if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
5219
- if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
5220
- if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
5221
- if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
5222
- if (p.dynamic) terms.push(`- 10 dynamic`);
5223
- if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
5224
- if (!terms.length) return `100 (no penalties) = ${r.score}`;
5225
- const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
5226
- const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
5227
- return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
5228
- }
5229
- function boundaryWorkOf(r) {
5230
- const b = r.breakdown;
5231
- if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
5232
- const parts = [];
5233
- if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
5234
- if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
5235
- if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
5236
- if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
5237
- if (b.hasDynamic) parts.push("count/for_each/data present");
5238
- if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
5239
- return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
5240
- }
5241
- function carveCardFields(node) {
5242
- const score = node.attrs.score;
5243
- const carve = node.attrs.carve;
5244
- if (typeof score !== "number") return void 0;
5245
- return [
5246
- { label: "score", value: String(score) },
5247
- ...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
5248
- ];
5249
- }
5250
- function carveReportToIr(report) {
5251
- const resources = report.resources ?? [];
5252
- const known = new Set(resources.map((r) => r.address));
5253
- const nodes = resources.map((r) => {
5254
- const status = statusForBand(r.band, r.score);
5255
- const b = r.breakdown ?? {};
5256
- const edges2 = boundaryEdgesOf(r);
5257
- const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
5258
- const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
5259
- return {
5260
- id: r.address,
5261
- kind: tfTypeOf(r.address, r.kind),
5262
- lexicon: "terraform",
5263
- attrs: {
5264
- // The drift palette's channel — `_`-prefixed, so it paints the card and
5265
- // stays out of both the card's fields and the inspect pane's list.
5266
- _status: status,
5267
- score: r.score,
5268
- carve: verdictForStatus(status),
5269
- band: r.band,
5270
- arithmetic: scoreArithmetic(r),
5271
- boundaryWork: boundaryWorkOf(r),
5272
- ...r.mapsTo ? { mapsTo: r.mapsTo } : {},
5273
- tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
5274
- inbound: b.inbound ?? 0,
5275
- outbound: b.outbound ?? 0,
5276
- outputs: b.outputs ?? 0,
5277
- instances: b.instances ?? 1,
5278
- dynamic: b.hasDynamic ?? false,
5279
- // The predicted diff, when the report carries the edge lists: who needs
5280
- // a `data` source the moment this is carved, and what becomes a
5281
- // deploy-time input. Absent (not empty) when the report has no lists —
5282
- // "none" and "not reported" are different claims.
5283
- ...patches.length ? { patchOnCarve: patches.join(", ") } : {},
5284
- ...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
5285
- }
5286
- };
5287
- });
5288
- const seen = /* @__PURE__ */ new Set();
5289
- const edges = [];
5290
- for (const r of resources) {
5291
- for (const e of boundaryEdgesOf(r)) {
5292
- const direction = directionOf(e);
5293
- const from = direction === "inbound" ? e.survivor : e.carved;
5294
- const to = direction === "inbound" ? e.carved : e.survivor;
5295
- if (!known.has(from) || !known.has(to)) continue;
5296
- const key = `${from}|${to}`;
5297
- if (seen.has(key)) continue;
5298
- seen.add(key);
5299
- edges.push({
5300
- from,
5301
- to,
5302
- kind: "ref",
5303
- viaAttr: direction,
5304
- ...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
5305
- });
5306
- }
5307
- }
5308
- const byStack = {};
5309
- for (const order of ["good", "warn", "neutral"]) {
5310
- const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
5311
- if (members.length) byStack[verdictForStatus(order)] = members;
5312
- }
5313
- return { nodes, edges, groups: { byStack } };
5314
- }
5315
- function carveNote(report, ir) {
5316
- const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
5317
- const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
5318
- if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
5319
- 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.`;
5320
- }
5321
-
5322
- // src/carve-manifest.ts
5323
- import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
5324
- import { dirname as dirname4, join as join10 } from "node:path";
5325
- var CARVE_MANIFEST_SUFFIX = ".carve.json";
5326
- 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.";
5327
- var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5328
- var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
5329
- function parseCarveManifest(value) {
5330
- if (!isRecord3(value)) return null;
5331
- if (value.version !== 1) return null;
5332
- if (typeof value.target !== "string" || !value.target) return null;
5333
- return value;
5334
- }
5335
- function readCarveManifest(path, readFile = (p) => readFileSync7(p, "utf8")) {
5336
- let text;
5337
- try {
5338
- text = readFile(path);
5339
- } catch {
5340
- return null;
5341
- }
5342
- try {
5343
- return parseCarveManifest(JSON.parse(text));
5344
- } catch {
5345
- return null;
5346
- }
5347
- }
5348
- function carveStageOf(m) {
5349
- if (m.apply) return "applied";
5350
- if (m.bridge) return "bridged";
5351
- return "emitted";
5352
- }
5353
- function carveWordFor(stage) {
5354
- switch (stage) {
5355
- case "applied":
5356
- return "carved \u2192 chant";
5357
- case "bridged":
5358
- return "bridged \u2014 apply is yours";
5359
- default:
5360
- return "emitted \u2014 not bridged";
5361
- }
5362
- }
5363
- function carveToneFor(stage) {
5364
- return stage === "applied" ? "good" : "accent";
5365
- }
5366
- function carveStageNote(state) {
5367
- switch (state.stage) {
5368
- case "applied":
5369
- 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.";
5370
- case "bridged":
5371
- return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
5372
- default:
5373
- return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
5374
- }
5375
- }
5376
- function carveStateOf(m, path) {
5377
- const stage = carveStageOf(m);
5378
- const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
5379
- return {
5380
- target: m.target,
5381
- ...m.tfType ? { tfType: m.tfType } : {},
5382
- stage,
5383
- graduated: stage === "applied",
5384
- // The last stamped step wins — that is the age of the state, not the age
5385
- // of the carve.
5386
- ...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
5387
- files: strings(m.emit?.files),
5388
- excised: strings(m.bridge?.excised),
5389
- deferredInputs: deferred,
5390
- ...m.apply ? {
5391
- ownership: {
5392
- ...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
5393
- ...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
5394
- ...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
5395
- }
5396
- } : {},
5397
- ...m.from ? { from: m.from } : {},
5398
- outDir: dirname4(path),
5399
- path
5400
- };
5401
- }
5402
- var nodeCarveIo = {
5403
- readdir: (dir) => readdirSync4(dir),
5404
- isDirectory: (p) => {
5405
- try {
5406
- return statSync4(p).isDirectory();
5407
- } catch {
5408
- return false;
5409
- }
5410
- },
5411
- readFile: (p) => readFileSync7(p, "utf8")
5412
- };
5413
- var MANIFEST_SCAN_DEPTH = 2;
5414
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
5415
- function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
5416
- const out = [];
5417
- const walk = (d, level) => {
5418
- let entries;
5419
- try {
5420
- entries = io.readdir(d).sort();
5421
- } catch {
5422
- return;
5423
- }
5424
- for (const name of entries) {
5425
- const full = join10(d, name);
5426
- if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
5427
- out.push(full);
5428
- continue;
5429
- }
5430
- if (level >= depth) continue;
5431
- if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
5432
- if (io.isDirectory(full)) walk(full, level + 1);
5433
- }
5434
- };
5435
- walk(dir, 0);
5436
- return out;
5437
- }
5438
- function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
5439
- const rank = { emitted: 0, bridged: 1, applied: 2 };
5440
- const byTarget = /* @__PURE__ */ new Map();
5441
- for (const dir of dirs) {
5442
- for (const path of listCarveManifests(dir, io, depth)) {
5443
- const manifest = readCarveManifest(path, io.readFile);
5444
- if (!manifest) continue;
5445
- const state = carveStateOf(manifest, path);
5446
- const held = byTarget.get(state.target);
5447
- if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
5448
- }
5449
- }
5450
- return byTarget;
5711
+ var directionOf = (e) => e.direction ?? "inbound";
5712
+ function boundaryEdgesOf(r) {
5713
+ const b = r.boundary;
5714
+ if (!b) return [];
5715
+ const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
5716
+ return list3.filter((e) => isRecord3(e) && typeof e.survivor === "string" && typeof e.carved === "string");
5451
5717
  }
5452
- function carveProgress(states, total) {
5453
- let applied = 0;
5454
- let bridged = 0;
5455
- let emitted = 0;
5456
- for (const s of states) {
5457
- if (s.stage === "applied") applied++;
5458
- else if (s.stage === "bridged") bridged++;
5459
- else emitted++;
5460
- }
5461
- const inFlight = bridged + emitted;
5462
- const parts = [];
5463
- if (emitted) parts.push(`${emitted} emitted`);
5464
- if (bridged) parts.push(`${bridged} bridged`);
5465
- return {
5466
- applied,
5467
- bridged,
5468
- emitted,
5469
- inFlight,
5470
- total: typeof total === "number" ? total : null,
5471
- label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
5472
- detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
5473
- };
5718
+ function scoreArithmetic(r) {
5719
+ const b = r.breakdown;
5720
+ if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
5721
+ const p = b.penalties ?? {};
5722
+ const terms = [];
5723
+ if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
5724
+ if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
5725
+ if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
5726
+ if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
5727
+ if (p.dynamic) terms.push(`- 10 dynamic`);
5728
+ if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
5729
+ if (!terms.length) return `100 (no penalties) = ${r.score}`;
5730
+ const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
5731
+ const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
5732
+ return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
5474
5733
  }
5475
- function applyCommandFor(state, labels = {}) {
5476
- const env = state.ownership?.env ?? "<env>";
5477
- const stack = state.ownership?.stack ?? "<stack>";
5478
- const from = labels.from ?? state.from ?? "<terraform-dir>";
5479
- const out = labels.out ?? state.outDir;
5480
- return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
5734
+ function boundaryWorkOf(r) {
5735
+ const b = r.breakdown;
5736
+ if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
5737
+ const parts = [];
5738
+ if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
5739
+ if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
5740
+ if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
5741
+ if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
5742
+ if (b.hasDynamic) parts.push("count/for_each/data present");
5743
+ if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
5744
+ return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
5481
5745
  }
5482
- function carveStatePayload(states, total, labels = () => ({})) {
5483
- const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
5484
- return {
5485
- manifests: list3.length,
5486
- progress: carveProgress(list3, total),
5487
- states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
5488
- apply: { human: true, note: APPLY_IS_HUMAN }
5489
- };
5746
+ function carveCardFields(node) {
5747
+ const score = node.attrs.score;
5748
+ const carve = node.attrs.carve;
5749
+ if (typeof score !== "number") return void 0;
5750
+ return [
5751
+ { label: "score", value: String(score) },
5752
+ ...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
5753
+ ];
5490
5754
  }
5491
- function splitCarveState(tfIr, states, opts = {}) {
5492
- if (states.size === 0) return { tf: tfIr, graduated: [] };
5493
- const shorten = opts.shorten ?? ((p) => p);
5494
- const source = (files) => files.map(shorten).join(", ");
5495
- const gone = /* @__PURE__ */ new Set();
5496
- const graduated = [];
5497
- const nodes = [];
5498
- for (const node of tfIr.nodes) {
5499
- const state = states.get(node.id);
5500
- if (!state) {
5501
- nodes.push(node);
5502
- continue;
5503
- }
5504
- if (state.graduated) {
5505
- gone.add(node.id);
5506
- graduated.push({
5507
- ...node,
5508
- attrs: {
5509
- ...node.attrs,
5510
- _status: "good",
5511
- carve: carveWordFor(state.stage),
5512
- carveState: state.stage,
5513
- ...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
5514
- ...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
5515
- ...state.at ? { carvedAt: state.at } : {},
5516
- ...state.files.length ? { chantSource: source(state.files) } : {}
5517
- }
5518
- });
5519
- continue;
5520
- }
5521
- nodes.push({
5522
- ...node,
5755
+ function carveReportToIr(report) {
5756
+ const resources = report.resources ?? [];
5757
+ const known = new Set(resources.map((r) => r.address));
5758
+ const nodes = resources.map((r) => {
5759
+ const status = statusForBand(r.band, r.score);
5760
+ const b = r.breakdown ?? {};
5761
+ const edges2 = boundaryEdgesOf(r);
5762
+ const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
5763
+ const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
5764
+ return {
5765
+ id: r.address,
5766
+ kind: tfTypeOf(r.address, r.kind),
5767
+ lexicon: "terraform",
5523
5768
  attrs: {
5524
- ...node.attrs,
5525
- _status: carveToneFor(state.stage),
5526
- carve: carveWordFor(state.stage),
5527
- carveState: state.stage,
5528
- ...state.at ? { carvedAt: state.at } : {},
5529
- ...state.files.length ? { chantSource: source(state.files) } : {},
5530
- ...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
5531
- ...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
5769
+ // The drift palette's channel — `_`-prefixed, so it paints the card and
5770
+ // stays out of both the card's fields and the inspect pane's list.
5771
+ _status: status,
5772
+ score: r.score,
5773
+ carve: verdictForStatus(status),
5774
+ band: r.band,
5775
+ arithmetic: scoreArithmetic(r),
5776
+ boundaryWork: boundaryWorkOf(r),
5777
+ ...r.mapsTo ? { mapsTo: r.mapsTo } : {},
5778
+ tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
5779
+ inbound: b.inbound ?? 0,
5780
+ outbound: b.outbound ?? 0,
5781
+ outputs: b.outputs ?? 0,
5782
+ instances: b.instances ?? 1,
5783
+ dynamic: b.hasDynamic ?? false,
5784
+ // The predicted diff, when the report carries the edge lists: who needs
5785
+ // a `data` source the moment this is carved, and what becomes a
5786
+ // deploy-time input. Absent (not empty) when the report has no lists —
5787
+ // "none" and "not reported" are different claims.
5788
+ ...patches.length ? { patchOnCarve: patches.join(", ") } : {},
5789
+ ...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
5532
5790
  }
5533
- });
5791
+ };
5792
+ });
5793
+ const seen = /* @__PURE__ */ new Set();
5794
+ const edges = [];
5795
+ for (const r of resources) {
5796
+ for (const e of boundaryEdgesOf(r)) {
5797
+ const direction = directionOf(e);
5798
+ const from = direction === "inbound" ? e.survivor : e.carved;
5799
+ const to = direction === "inbound" ? e.carved : e.survivor;
5800
+ if (!known.has(from) || !known.has(to)) continue;
5801
+ const key = `${from}|${to}`;
5802
+ if (seen.has(key)) continue;
5803
+ seen.add(key);
5804
+ edges.push({
5805
+ from,
5806
+ to,
5807
+ kind: "ref",
5808
+ viaAttr: direction,
5809
+ ...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
5810
+ });
5811
+ }
5534
5812
  }
5535
5813
  const byStack = {};
5536
- for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
5537
- const left = members.filter((id) => !gone.has(id));
5538
- if (left.length) byStack[band] = left;
5814
+ for (const order of ["good", "warn", "neutral"]) {
5815
+ const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
5816
+ if (members.length) byStack[verdictForStatus(order)] = members;
5539
5817
  }
5540
- return {
5541
- tf: {
5542
- nodes,
5543
- edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
5544
- groups: { ...tfIr.groups, byStack }
5545
- },
5546
- graduated
5547
- };
5548
- }
5549
- var GRADUATED_BAND = "carved \u2192 chant";
5550
- function bandGraduated(tf, graduated) {
5551
- if (graduated.length === 0) return tf;
5552
- return {
5553
- nodes: [...graduated, ...tf.nodes],
5554
- edges: tf.edges,
5555
- groups: {
5556
- ...tf.groups,
5557
- byStack: {
5558
- [GRADUATED_BAND]: graduated.map((n) => n.id),
5559
- ...tf.groups?.byStack ?? {}
5560
- }
5561
- }
5562
- };
5818
+ return { nodes, edges, groups: { byStack } };
5563
5819
  }
5564
- function carveStateNote(progress, states) {
5565
- if (states.size === 0) return "";
5566
- const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
5567
- return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
5820
+ function carveNote(report, ir) {
5821
+ const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
5822
+ const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
5823
+ if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
5824
+ 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.`;
5568
5825
  }
5569
5826
 
5570
5827
  // src/ops-lens.ts
@@ -5838,9 +6095,12 @@ function nodeFor(op, placed, guards) {
5838
6095
  ...profile ? { profile } : {},
5839
6096
  ...policy ? { policy } : {},
5840
6097
  // The resolved args, verbatim from the IR. They carry the Op's SCOPE
5841
- // (env, stack, template path) and never an estate entity id — see the
5842
- // module doc on why no edge is drawn from them.
6098
+ // (env, stack, template path); which of them identify an ENTITY is the
6099
+ // contract's to say, and 0.54's op.json says it in `entities` below.
5843
6100
  ...args ? { args } : {},
6101
+ // chant#2022: what the step touches, verbatim. `entities` prints on the
6102
+ // card; `_entities` is what `joinStepEntities` resolves.
6103
+ ...step.entities?.length ? { entities: step.entities.join(", "), _entities: step.entities } : {},
5844
6104
  ...step.outcomeAttribute ? { outcome: step.outcomeAttribute.from ? `${step.outcomeAttribute.name} \u2190 ${step.outcomeAttribute.from}` : step.outcomeAttribute.name } : {},
5845
6105
  ...op.activityContracts?.[step.fn] ? { contract: "declared (args/returns JSON Schema in op.json)" } : {}
5846
6106
  }
@@ -5942,17 +6202,83 @@ function opCardFields(node) {
5942
6202
  function opsNote(ops, ir) {
5943
6203
  const gates = ir.nodes.filter((n) => n.attrs._step === "gate").length;
5944
6204
  const rules = ir.nodes.filter((n) => n.attrs._step === "rule");
5945
- const steps = ir.nodes.length - rules.length;
6205
+ const estate = ir.nodes.filter((n) => n.lexicon !== "op").length;
6206
+ const steps = ir.nodes.length - rules.length - estate;
5946
6207
  const dangling = rules.filter((n) => n.attrs.dangling !== void 0).length;
5947
6208
  const counts = [
5948
6209
  `${ops.length} declared Op${ops.length === 1 ? "" : "s"}`,
5949
6210
  `${steps} step${steps === 1 ? "" : "s"}`,
5950
6211
  ...gates ? [`${gates} gate${gates === 1 ? "" : "s"}`] : [],
5951
- ...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : []
6212
+ ...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : [],
6213
+ ...entityCountClause(ir)
5952
6214
  ].join(" \xB7 ");
5953
6215
  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.` : "") : "";
5954
6216
  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.`;
5955
6217
  }
6218
+ var ESTATE_BOX = "estate";
6219
+ function joinStepEntities(ir, estate) {
6220
+ const steps = ir.nodes.filter((n) => Array.isArray(n.attrs._entities));
6221
+ if (steps.length === 0) return { refs: 0, resolved: 0 };
6222
+ const byId = /* @__PURE__ */ new Map();
6223
+ const byLeaf = /* @__PURE__ */ new Map();
6224
+ for (const n of estate?.nodes ?? []) {
6225
+ byId.set(n.id, n);
6226
+ for (const leaf of stringLeaves(n.attrs)) {
6227
+ const list3 = byLeaf.get(leaf.value);
6228
+ if (list3) {
6229
+ if (!list3.includes(n)) list3.push(n);
6230
+ } else byLeaf.set(leaf.value, [n]);
6231
+ }
6232
+ }
6233
+ const present = new Set(ir.nodes.map((n) => n.id));
6234
+ let refs = 0;
6235
+ let resolvedCount = 0;
6236
+ for (const step of steps) {
6237
+ const links = { resolved: [], unresolved: [] };
6238
+ for (const value of step.attrs._entities) {
6239
+ refs++;
6240
+ const targets = [];
6241
+ const idHit = byId.get(value);
6242
+ if (idHit) targets.push({ node: idHit, via: "id" });
6243
+ for (const n of byLeaf.get(value) ?? []) if (n !== idHit) targets.push({ node: n, via: "attr" });
6244
+ if (targets.length === 0) {
6245
+ links.unresolved.push(value);
6246
+ continue;
6247
+ }
6248
+ resolvedCount++;
6249
+ for (const { node, via } of targets) {
6250
+ links.resolved.push({ value, node: node.id, via });
6251
+ if (!present.has(node.id)) {
6252
+ present.add(node.id);
6253
+ ir.nodes.push({ ...node, attrs: { ...node.attrs, _touchedBy: [step.id] } });
6254
+ (ir.groups.byStack ??= {})[ESTATE_BOX] = [...ir.groups.byStack[ESTATE_BOX] ?? [], node.id];
6255
+ } else {
6256
+ const held = ir.nodes.find((n) => n.id === node.id);
6257
+ const touched = held.attrs._touchedBy ?? [];
6258
+ if (!touched.includes(step.id)) held.attrs._touchedBy = [...touched, step.id];
6259
+ }
6260
+ ir.edges.push({ from: step.id, to: node.id, kind: "ref", viaAttr: "entities" });
6261
+ }
6262
+ }
6263
+ step.attrs._entityLinks = links;
6264
+ }
6265
+ return { refs, resolved: resolvedCount };
6266
+ }
6267
+ function entityCountClause(ir) {
6268
+ let refs = 0;
6269
+ let resolved = 0;
6270
+ for (const n of ir.nodes) {
6271
+ const links = n.attrs._entityLinks;
6272
+ if (links) {
6273
+ refs += links.resolved.length + links.unresolved.length;
6274
+ resolved += links.resolved.length;
6275
+ } else if (Array.isArray(n.attrs._entities)) {
6276
+ refs += n.attrs._entities.length;
6277
+ }
6278
+ }
6279
+ if (refs === 0) return [];
6280
+ return [`${refs} entity ref${refs === 1 ? "" : "s"}, ${resolved} linked`];
6281
+ }
5956
6282
 
5957
6283
  // src/render.ts
5958
6284
  registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
@@ -6066,7 +6392,10 @@ function renderCarveEstate(tfIr, appIr, opts) {
6066
6392
  }
6067
6393
  function withCarveState(tfIr, appIr, opts) {
6068
6394
  const states = opts.carved ?? /* @__PURE__ */ new Map();
6069
- const { tf, graduated } = splitCarveState(tfIr, states, { ...opts.shorten ? { shorten: opts.shorten } : {} });
6395
+ const { tf, graduated } = splitCarveState(tfIr, states, {
6396
+ ...opts.shorten ? { shorten: opts.shorten } : {},
6397
+ ...opts.emitted ? { emitted: opts.emitted } : {}
6398
+ });
6070
6399
  const ns = namespaceAppIr(appIr, opts.appTitle);
6071
6400
  const progress = carveProgress(states.values(), tfIr.nodes.length);
6072
6401
  return {
@@ -6406,12 +6735,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
6406
6735
  }
6407
6736
 
6408
6737
  // src/carve-actions.ts
6409
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
6410
- import { isAbsolute, join as join13, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
6738
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync6 } from "node:fs";
6739
+ import { isAbsolute as isAbsolute2, join as join14, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
6411
6740
 
6412
6741
  // src/layout.ts
6413
6742
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
6414
- import { join as join11 } from "node:path";
6743
+ import { join as join12 } from "node:path";
6415
6744
  var LAYOUT_DIR = ".behold";
6416
6745
  var LAYOUT_FILE = "layout.json";
6417
6746
  var MAX_BODY_BYTES = 64 * 1024;
@@ -6452,7 +6781,7 @@ function lensFromQuery(params) {
6452
6781
  return [slug(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug(`stack-${stack}`) : null].filter(Boolean).join("+");
6453
6782
  }
6454
6783
  function layoutPath(projectDir) {
6455
- return join11(projectDir, LAYOUT_DIR, LAYOUT_FILE);
6784
+ return join12(projectDir, LAYOUT_DIR, LAYOUT_FILE);
6456
6785
  }
6457
6786
  function unwritableReason(projectDir) {
6458
6787
  if (!projectDir || !existsSync6(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
@@ -6508,7 +6837,7 @@ function writeLens(projectDir, lens, deltas) {
6508
6837
  }
6509
6838
  const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
6510
6839
  if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
6511
- mkdirSync2(join11(projectDir, LAYOUT_DIR), { recursive: true });
6840
+ mkdirSync2(join12(projectDir, LAYOUT_DIR), { recursive: true });
6512
6841
  const tmp = `${file}.tmp`;
6513
6842
  writeFileSync2(tmp, body, "utf8");
6514
6843
  renameSync(tmp, file);
@@ -6594,7 +6923,35 @@ function reanchorEdges(svg, moved) {
6594
6923
  // src/carve-live.ts
6595
6924
  import { spawn as spawn3 } from "node:child_process";
6596
6925
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
6597
- import { join as join12 } from "node:path";
6926
+ import { join as join13 } from "node:path";
6927
+
6928
+ // src/scratch.ts
6929
+ var PROTECTED_INFRA = [
6930
+ /^floci(-|$)/,
6931
+ /^chant-floci(-|$)/,
6932
+ /^k3d-kubemicrovm-local$/,
6933
+ /^k3d-fountain-local$/,
6934
+ /^kubemicrovm-local$/,
6935
+ /^fountain-local$/
6936
+ ];
6937
+ var PROTECTED_PORTS = [4566, 4577, 4588];
6938
+ var SCRATCH_PREFIX = "behold-";
6939
+ function scratchNameProblem(name) {
6940
+ if (!name.startsWith(SCRATCH_PREFIX)) return `scratch infra must be named ${SCRATCH_PREFIX}*, got ${JSON.stringify(name)}`;
6941
+ const hit = PROTECTED_INFRA.find((re) => re.test(name));
6942
+ if (hit) return `${JSON.stringify(name)} is protected infrastructure (${hit}) \u2014 behold never touches it`;
6943
+ return void 0;
6944
+ }
6945
+ function scratchPortProblem(port) {
6946
+ if (PROTECTED_PORTS.includes(port)) return `port ${port} belongs to a shared emulator \u2014 a scratch substrate gets its own`;
6947
+ return void 0;
6948
+ }
6949
+ function assertScratch(name, port) {
6950
+ const p = scratchNameProblem(name) ?? (port === void 0 ? void 0 : scratchPortProblem(port));
6951
+ if (p) throw new Error(`scratch discipline: ${p}`);
6952
+ }
6953
+
6954
+ // src/carve-live.ts
6598
6955
  var LIVE_CONTAINER = "behold-carve-floci";
6599
6956
  var LIVE_PORT = 4602;
6600
6957
  var LIVE_IMAGE = "floci/floci:1.5.34";
@@ -6627,6 +6984,7 @@ function capture(cmd, args, cwd) {
6627
6984
  });
6628
6985
  }
6629
6986
  async function bootScratchFloci() {
6987
+ assertScratch(LIVE_CONTAINER, LIVE_PORT);
6630
6988
  const ps = await capture("docker", ["ps", "-a", "--format", "{{.Names}}"], process.cwd());
6631
6989
  if (ps.code !== 0) return "docker isn't answering \u2014 is the daemon up?";
6632
6990
  if (ps.stdout.split("\n").includes(LIVE_CONTAINER)) {
@@ -6653,9 +7011,9 @@ async function teardownScratchFloci() {
6653
7011
  await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
6654
7012
  }
6655
7013
  async function applyIntoFloci(fromDir, step) {
6656
- const template = join12(fromDir, "floci-override.tf.disabled");
7014
+ const template = join13(fromDir, "floci-override.tf.disabled");
6657
7015
  if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
6658
- writeFileSync3(join12(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
7016
+ writeFileSync3(join13(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
6659
7017
  const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
6660
7018
  if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
6661
7019
  const apply = await step(
@@ -6705,7 +7063,7 @@ async function runCarveObserve(demo, select) {
6705
7063
  "run `behold demo carve --live` (needs docker + terraform)"
6706
7064
  );
6707
7065
  }
6708
- if (!existsSync8(join13(demo.out, "src"))) {
7066
+ if (!existsSync8(join14(demo.out, "src"))) {
6709
7067
  return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
6710
7068
  }
6711
7069
  const args = ["lifecycle", "diff", "prod", "--live", "--json"];
@@ -6775,8 +7133,8 @@ var refuse3 = (code, error, remedy) => ({
6775
7133
  });
6776
7134
  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.";
6777
7135
  function insideDemo(root, p) {
6778
- const rel = relative2(resolve4(root), resolve4(p));
6779
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
7136
+ const rel = relative3(resolve5(root), resolve5(p));
7137
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
6780
7138
  }
6781
7139
  function selectFromReport(report, raw) {
6782
7140
  if (typeof raw !== "string" || !raw || raw.length > 512) return null;
@@ -6796,17 +7154,17 @@ function readArtifacts(demo, dir, filter) {
6796
7154
  if (out.length >= MAX_ARTIFACTS || depth > 2) return;
6797
7155
  let entries;
6798
7156
  try {
6799
- entries = readdirSync5(d).sort();
7157
+ entries = readdirSync6(d).sort();
6800
7158
  } catch {
6801
7159
  return;
6802
7160
  }
6803
7161
  for (const name of entries) {
6804
7162
  if (out.length >= MAX_ARTIFACTS) return;
6805
7163
  if (name === "node_modules" || name.startsWith(".")) continue;
6806
- const full = join13(d, name);
7164
+ const full = join14(d, name);
6807
7165
  let st;
6808
7166
  try {
6809
- st = statSync5(full);
7167
+ st = statSync6(full);
6810
7168
  } catch {
6811
7169
  continue;
6812
7170
  }
@@ -6814,7 +7172,7 @@ function readArtifacts(demo, dir, filter) {
6814
7172
  walk(full, depth + 1);
6815
7173
  continue;
6816
7174
  }
6817
- const rel = relative2(demo.root, full).split(sep2).join("/");
7175
+ const rel = relative3(demo.root, full).split(sep2).join("/");
6818
7176
  if (!filter(rel)) continue;
6819
7177
  let text = "";
6820
7178
  try {
@@ -6850,7 +7208,7 @@ async function runCarveEmit(demo, select) {
6850
7208
  const block = carveWriteBlock(demo);
6851
7209
  if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6852
7210
  mkdirSync3(demo.out, { recursive: true });
6853
- const reportFile = join13(demo.out, `${carveSlug(select)}-boundary.json`);
7211
+ const reportFile = join14(demo.out, `${carveSlug(select)}-boundary.json`);
6854
7212
  const args = [
6855
7213
  "carve",
6856
7214
  "emit",
@@ -6882,7 +7240,7 @@ async function runCarveEmit(demo, select) {
6882
7240
  boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
6883
7241
  } catch {
6884
7242
  }
6885
- const lintPath = relative2(demo.project, join13(demo.out, "src")).split(sep2).join("/");
7243
+ const lintPath = relative3(demo.project, join14(demo.out, "src")).split(sep2).join("/");
6886
7244
  const lintArgs = ["lint", lintPath];
6887
7245
  const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
6888
7246
  code: 127,
@@ -6945,8 +7303,8 @@ async function runCarveBridge(demo, select) {
6945
7303
  }
6946
7304
 
6947
7305
  // src/ops.ts
6948
- import { readdirSync as readdirSync6, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
6949
- import { join as join14 } from "node:path";
7306
+ import { readdirSync as readdirSync7, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
7307
+ import { join as join15 } from "node:path";
6950
7308
  var APPLY_TARGET_LEXICON = {
6951
7309
  cloudformation: "aws",
6952
7310
  kubectl: "k8s",
@@ -6968,11 +7326,11 @@ function discoverOps(projectDir) {
6968
7326
  const seen = /* @__PURE__ */ new Set();
6969
7327
  const out = [];
6970
7328
  for (const sub of ["ops", "src", "."]) {
6971
- const dir = join14(projectDir, sub);
7329
+ const dir = join15(projectDir, sub);
6972
7330
  if (!existsSync9(dir)) continue;
6973
- for (const f of readdirSync6(dir)) {
7331
+ for (const f of readdirSync7(dir)) {
6974
7332
  if (!f.endsWith(".op.ts")) continue;
6975
- const content = readFileSync11(join14(dir, f), "utf8");
7333
+ const content = readFileSync11(join15(dir, f), "utf8");
6976
7334
  const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
6977
7335
  if (!name || seen.has(name)) continue;
6978
7336
  seen.add(name);
@@ -6995,15 +7353,15 @@ function discoverOps(projectDir) {
6995
7353
  return out.sort((a, b) => a.name.localeCompare(b.name));
6996
7354
  }
6997
7355
  function opIrPath(projectDir, name) {
6998
- return join14(projectDir, "dist", "ops", name, "op.json");
7356
+ return join15(projectDir, "dist", "ops", name, "op.json");
6999
7357
  }
7000
7358
  function discoverOpIrs(projectDirs) {
7001
7359
  const seen = /* @__PURE__ */ new Set();
7002
7360
  const out = [];
7003
7361
  for (const dir of projectDirs) {
7004
- const root = join14(dir, "dist", "ops");
7362
+ const root = join15(dir, "dist", "ops");
7005
7363
  if (!existsSync9(root)) continue;
7006
- for (const name of readdirSync6(root)) {
7364
+ for (const name of readdirSync7(root)) {
7007
7365
  if (seen.has(name)) continue;
7008
7366
  const path = opIrPath(dir, name);
7009
7367
  if (!existsSync9(path)) continue;
@@ -7241,11 +7599,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
7241
7599
  function tookLabel(ms) {
7242
7600
  return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
7243
7601
  }
7244
- function paintPlayhead(ir, join21, gate) {
7245
- if (!join21.byNode.size && !join21.at) return ir;
7602
+ function paintPlayhead(ir, join22, gate) {
7603
+ if (!join22.byNode.size && !join22.at) return ir;
7246
7604
  const nodes = ir.nodes.map((node) => {
7247
- const run3 = join21.byNode.get(node.id);
7248
- const here = join21.at === node.id;
7605
+ const run3 = join22.byNode.get(node.id);
7606
+ const here = join22.at === node.id;
7249
7607
  if (!run3 && !here) return node;
7250
7608
  const isGate = node.attrs?._step === "gate";
7251
7609
  const attrs = { ...node.attrs };
@@ -7260,7 +7618,7 @@ function paintPlayhead(ir, join21, gate) {
7260
7618
  if (here) {
7261
7619
  attrs._playhead = true;
7262
7620
  attrs._status = isGate && gate ? "warn" : "accent";
7263
- attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join21.atReason ?? "here";
7621
+ attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join22.atReason ?? "here";
7264
7622
  if (isGate && gate?.description) attrs.gateDescription = gate.description;
7265
7623
  }
7266
7624
  return { ...node, attrs };
@@ -7282,11 +7640,11 @@ function pendingGateCard(op, gate, ir) {
7282
7640
  }
7283
7641
  };
7284
7642
  }
7285
- function playheadNote(state, join21) {
7643
+ function playheadNote(state, join22) {
7286
7644
  if (!state.op) {
7287
7645
  return "No run state: nothing has been run from this behold session, so every step paints as declared.";
7288
7646
  }
7289
- const settled = join21 ? join21.byNode.size : state.records.length;
7647
+ const settled = join22 ? join22.byNode.size : state.records.length;
7290
7648
  const bits = [];
7291
7649
  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`;
7292
7650
  if (state.status === "running") {
@@ -7302,9 +7660,9 @@ function playheadNote(state, join21) {
7302
7660
  } else if (state.gateNote) {
7303
7661
  bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
7304
7662
  }
7305
- if (join21?.unplaced.length) {
7663
+ if (join22?.unplaced.length) {
7306
7664
  bits.push(
7307
- `${join21.unplaced.length} record${join21.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join21.unplaced.length === 1 ? "is" : "are"} not painted`
7665
+ `${join22.unplaced.length} record${join22.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join22.unplaced.length === 1 ? "is" : "are"} not painted`
7308
7666
  );
7309
7667
  }
7310
7668
  if (!bits.length) return `${head}.`;
@@ -7971,23 +8329,23 @@ var OpRunner = class {
7971
8329
  // src/substrates.ts
7972
8330
  import { spawn as spawn4 } from "node:child_process";
7973
8331
  import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
7974
- import { join as join15 } from "node:path";
8332
+ import { join as join16 } from "node:path";
7975
8333
  import { platform } from "node:os";
7976
8334
  var DOCKER_DOWN_DETAIL = "docker is down";
7977
8335
  function probe(cmd, args) {
7978
- return new Promise((resolve9) => {
8336
+ return new Promise((resolve10) => {
7979
8337
  let out = "";
7980
8338
  let proc;
7981
8339
  try {
7982
8340
  proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
7983
8341
  } catch {
7984
- resolve9({ code: 127, out: "" });
8342
+ resolve10({ code: 127, out: "" });
7985
8343
  return;
7986
8344
  }
7987
8345
  proc.stdout.on("data", (d) => out += d);
7988
8346
  proc.stderr.on("data", (d) => out += d);
7989
- proc.on("error", () => resolve9({ code: 127, out }));
7990
- proc.on("close", (code) => resolve9({ code: code ?? 1, out }));
8347
+ proc.on("error", () => resolve10({ code: 127, out }));
8348
+ proc.on("close", (code) => resolve10({ code: code ?? 1, out }));
7991
8349
  });
7992
8350
  }
7993
8351
  async function dockerAvailable(run3) {
@@ -8000,11 +8358,11 @@ async function dockerRunning(run3, nameFilter) {
8000
8358
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
8001
8359
  }
8002
8360
  function scriptBringUp(projectDir, relPath, label) {
8003
- return existsSync10(join15(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
8361
+ return existsSync10(join16(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
8004
8362
  }
8005
8363
  function projectLexicons(projectDir) {
8006
8364
  try {
8007
- const src = readFileSync12(join15(projectDir, "chant.config.ts"), "utf-8");
8365
+ const src = readFileSync12(join16(projectDir, "chant.config.ts"), "utf-8");
8008
8366
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
8009
8367
  if (!m) return [];
8010
8368
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -8056,7 +8414,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
8056
8414
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
8057
8415
  ];
8058
8416
  for (const [name, label, marker, script] of forges) {
8059
- if (!existsSync10(join15(projectDir, marker))) continue;
8417
+ if (!existsSync10(join16(projectDir, marker))) continue;
8060
8418
  const c = docker ? await dockerRunning(run3, name) : [];
8061
8419
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
8062
8420
  subs.push({
@@ -8090,7 +8448,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
8090
8448
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
8091
8449
  });
8092
8450
  }
8093
- if (existsSync10(join15(projectDir, ".github", "workflows"))) {
8451
+ if (existsSync10(join16(projectDir, ".github", "workflows"))) {
8094
8452
  const gh = await run3("gh", ["auth", "status"]);
8095
8453
  const ready = gh.code === 0;
8096
8454
  subs.push({
@@ -8103,7 +8461,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
8103
8461
  if (lexicons.includes("temporal")) {
8104
8462
  let hasProfiles = false;
8105
8463
  try {
8106
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync12(join15(projectDir, "chant.config.ts"), "utf-8"));
8464
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync12(join16(projectDir, "chant.config.ts"), "utf-8"));
8107
8465
  } catch {
8108
8466
  }
8109
8467
  subs.push({
@@ -8207,12 +8565,12 @@ async function openRollbackBranches(projectDir, env) {
8207
8565
  }
8208
8566
 
8209
8567
  // src/estate-edges.ts
8210
- import { statSync as statSync6 } from "node:fs";
8568
+ import { statSync as statSync7 } from "node:fs";
8211
8569
  var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
8212
8570
  var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
8213
8571
  var realIsDir2 = (p) => {
8214
8572
  try {
8215
- return statSync6(p).isDirectory();
8573
+ return statSync7(p).isDirectory();
8216
8574
  } catch {
8217
8575
  return false;
8218
8576
  }
@@ -8312,7 +8670,7 @@ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
8312
8670
 
8313
8671
  // src/events.ts
8314
8672
  import { watch, existsSync as existsSync11 } from "node:fs";
8315
- import { join as join16 } from "node:path";
8673
+ import { join as join17 } from "node:path";
8316
8674
  var Broadcaster = class {
8317
8675
  listeners = /* @__PURE__ */ new Set();
8318
8676
  subscribe(fn) {
@@ -8330,7 +8688,7 @@ var Broadcaster = class {
8330
8688
  };
8331
8689
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
8332
8690
  function watchSource(projectDir, onChange, debounceMs = 200) {
8333
- const dir = existsSync11(join16(projectDir, "src")) ? join16(projectDir, "src") : projectDir;
8691
+ const dir = existsSync11(join17(projectDir, "src")) ? join17(projectDir, "src") : projectDir;
8334
8692
  let timer;
8335
8693
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
8336
8694
  const name = typeof file === "string" ? file : "";
@@ -8619,12 +8977,12 @@ async function emulatorDown(projectDir) {
8619
8977
 
8620
8978
  // src/demos.ts
8621
8979
  import { readFileSync as readFileSync13, existsSync as existsSync12, cpSync } from "node:fs";
8622
- import { join as join17, relative as relative3, resolve as resolve5, sep as sep3 } from "node:path";
8980
+ import { join as join18, relative as relative4, resolve as resolve6, sep as sep3 } from "node:path";
8623
8981
  import { spawn as spawn5, spawnSync } from "node:child_process";
8624
8982
  function loadDemoRegistry(pkgRoot2) {
8625
8983
  let raw;
8626
8984
  try {
8627
- raw = JSON.parse(readFileSync13(join17(pkgRoot2, "demos.json"), "utf8"));
8985
+ raw = JSON.parse(readFileSync13(join18(pkgRoot2, "demos.json"), "utf8"));
8628
8986
  } catch {
8629
8987
  return [];
8630
8988
  }
@@ -8664,22 +9022,22 @@ function fetchesFromNetwork(entry) {
8664
9022
  return entry.source === "git";
8665
9023
  }
8666
9024
  function demoTargetDir(entry, cwd = process.cwd()) {
8667
- const legacy = resolve5(cwd, "behold-demo");
9025
+ const legacy = resolve6(cwd, "behold-demo");
8668
9026
  if (entry.name === "writes" && existsSync12(legacy)) return legacy;
8669
- return resolve5(cwd, "behold-demos", entry.name);
9027
+ return resolve6(cwd, "behold-demos", entry.name);
8670
9028
  }
8671
9029
  async function loadDemo(entry, opts) {
8672
9030
  const { pkgRoot: pkgRoot2, target } = opts;
8673
9031
  const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
8674
9032
  if (!existsSync12(target)) {
8675
9033
  if (entry.source === "bundled") {
8676
- const bundled = join17(pkgRoot2, entry.dir);
9034
+ const bundled = join18(pkgRoot2, entry.dir);
8677
9035
  if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
8678
9036
  say(`copying to ${target} (it's yours \u2014 edit it)`);
8679
9037
  try {
8680
9038
  cpSync(bundled, target, {
8681
9039
  recursive: true,
8682
- filter: (src) => !relative3(bundled, src).split(sep3).includes("node_modules")
9040
+ filter: (src) => !relative4(bundled, src).split(sep3).includes("node_modules")
8683
9041
  });
8684
9042
  } catch (err) {
8685
9043
  return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -8693,7 +9051,7 @@ async function loadDemo(entry, opts) {
8693
9051
  } else {
8694
9052
  say(`reusing ${target}`);
8695
9053
  }
8696
- if (existsSync12(join17(target, "package.json")) && !existsSync12(join17(target, "node_modules"))) {
9054
+ if (existsSync12(join18(target, "package.json")) && !existsSync12(join18(target, "node_modules"))) {
8697
9055
  say("npm install\u2026");
8698
9056
  if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
8699
9057
  return { ok: false, error: `npm install failed in ${target}` };
@@ -8705,7 +9063,7 @@ async function loadDemo(entry, opts) {
8705
9063
  return { ok: false, error: `setup failed (${entry.setup})` };
8706
9064
  }
8707
9065
  }
8708
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join17(target, d)) : [target] };
9066
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join18(target, d)) : [target] };
8709
9067
  }
8710
9068
  function runStep(cmd, args, opts = {}) {
8711
9069
  return new Promise((res) => {
@@ -8716,8 +9074,8 @@ function runStep(cmd, args, opts = {}) {
8716
9074
  }
8717
9075
 
8718
9076
  // src/server.ts
8719
- var pkgRoot = join18(dirname5(fileURLToPath3(import.meta.url)), "..");
8720
- var webRoot = join18(pkgRoot, "web");
9077
+ var pkgRoot = join19(dirname5(fileURLToPath3(import.meta.url)), "..");
9078
+ var webRoot = join19(pkgRoot, "web");
8721
9079
  var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
8722
9080
  function optsFromQuery(url, tierEnvVar, projectDir) {
8723
9081
  const q = url.searchParams;
@@ -8767,7 +9125,7 @@ function tierFailure(tier, message) {
8767
9125
  function beholdVersion() {
8768
9126
  try {
8769
9127
  const here = dirname5(fileURLToPath3(import.meta.url));
8770
- return JSON.parse(readFileSync14(join18(here, "..", "package.json"), "utf8")).version ?? "unknown";
9128
+ return JSON.parse(readFileSync14(join19(here, "..", "package.json"), "utf8")).version ?? "unknown";
8771
9129
  } catch {
8772
9130
  return "unknown";
8773
9131
  }
@@ -8815,6 +9173,11 @@ function carveRoutes(app, reportPath, demo) {
8815
9173
  const load = () => readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
8816
9174
  const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
8817
9175
  const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
9176
+ const emittedNodes = async (states) => {
9177
+ if (!demo || states.size === 0) return /* @__PURE__ */ new Map();
9178
+ const outIr = await memberIr(demo.out).catch(() => void 0);
9179
+ return emittedNodesFor(outIr, states.values(), demo.out);
9180
+ };
8818
9181
  const demoBlock = () => carveWriteBlock(demo);
8819
9182
  const demoInfo = () => {
8820
9183
  if (!demo) return null;
@@ -8826,8 +9189,8 @@ function carveRoutes(app, reportPath, demo) {
8826
9189
  project: demo.project,
8827
9190
  out: demo.out,
8828
9191
  // Paths the UI can print without leaking the operator's whole home dir.
8829
- outLabel: relative4(demo.root, demo.out).split(sep4).join("/"),
8830
- fromLabel: relative4(demo.root, demo.from).split(sep4).join("/"),
9192
+ outLabel: relative5(demo.root, demo.out).split(sep4).join("/"),
9193
+ fromLabel: relative5(demo.root, demo.from).split(sep4).join("/"),
8831
9194
  runnable: !block,
8832
9195
  ...block ? { reason: block } : {},
8833
9196
  // The live tier, when this boot armed it: the scratch endpoint the
@@ -8844,7 +9207,7 @@ function carveRoutes(app, reportPath, demo) {
8844
9207
  let appGraph;
8845
9208
  const appGraphOnce = () => {
8846
9209
  if (!demo) return Promise.resolve(null);
8847
- appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative4(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
9210
+ appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative5(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
8848
9211
  return appGraph;
8849
9212
  };
8850
9213
  app.get("/api/graph", async (c) => {
@@ -8853,16 +9216,18 @@ function carveRoutes(app, reportPath, demo) {
8853
9216
  const tfIr = carveReportToIr(parsed.report);
8854
9217
  const appSide = await appGraphOnce();
8855
9218
  const states = carveStates();
9219
+ const emitted = await emittedNodes(states);
8856
9220
  const shorten = demo ? (p) => shortenIn(p, demo.root) : (p) => p;
8857
9221
  const single = () => {
8858
- const split = splitCarveState(tfIr, states, { shorten });
9222
+ const split = splitCarveState(tfIr, states, { shorten, emitted });
8859
9223
  return bandGraduated(split.tf, split.graduated);
8860
9224
  };
8861
9225
  const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
8862
- tfTitle: `${relative4(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
9226
+ tfTitle: `${relative5(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
8863
9227
  appTitle: `${appSide.label} \u2014 chant`,
8864
9228
  carved: states,
8865
- shorten
9229
+ shorten,
9230
+ emitted
8866
9231
  }) : (() => {
8867
9232
  const banded = single();
8868
9233
  return { ...renderBanded(banded), ir: banded };
@@ -8910,7 +9275,7 @@ function carveRoutes(app, reportPath, demo) {
8910
9275
  }
8911
9276
  const tfIr = carveReportToIr(parsed.report);
8912
9277
  const html = renderCarveMorph(tfIr, appSide.ir, [select], {
8913
- tfTitle: `${relative4(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
9278
+ tfTitle: `${relative5(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
8914
9279
  appTitle: `${appSide.label} \u2014 chant`,
8915
9280
  title: `carve \u2014 ${select}`,
8916
9281
  carved: carveStates(),
@@ -8918,7 +9283,7 @@ function carveRoutes(app, reportPath, demo) {
8918
9283
  });
8919
9284
  return c.html(html);
8920
9285
  });
8921
- app.get("/api/project", (c) => {
9286
+ app.get("/api/project", async (c) => {
8922
9287
  const parsed = load();
8923
9288
  const total = parsed.ok ? parsed.report.count ?? parsed.report.resources.length : null;
8924
9289
  const state = carveStatePayload(
@@ -9013,6 +9378,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9013
9378
  if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
9014
9379
  let beholdConfig = loadBeholdConfig(cfg.projectDir);
9015
9380
  let tierEnvVar = beholdConfig.tiers?.envVar;
9381
+ const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
9382
+ const executorStatus = (env, d, pipeline) => {
9383
+ if ("invalid" in d) return { ok: false, reason: d.invalid };
9384
+ if (d.forge !== "github") {
9385
+ 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` };
9386
+ }
9387
+ const r = resolveWorkflow(cfg.projectDir, pipeline ?? { stages: [], jobs: [] }, d.workflow);
9388
+ return "workflow" in r ? { forge: d.forge, workflow: d.workflow, ok: true } : { forge: d.forge, workflow: d.workflow, ok: false, reason: r.reason };
9389
+ };
9390
+ const forgeRefusal = (env, d, what) => ({
9391
+ 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`,
9392
+ code: "executor-forge",
9393
+ 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.`
9394
+ });
9016
9395
  const boundK8sContext = async (env) => {
9017
9396
  try {
9018
9397
  const { lexicons, k8sProfiles } = await detectProject(cfg.projectDir);
@@ -9074,6 +9453,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9074
9453
  if (!info) {
9075
9454
  return c.json({ error: `no Op named "${name}" in the estate` }, 404);
9076
9455
  }
9456
+ const designatedEnv = info.kind === "apply" ? executorFor(info.env) : void 0;
9457
+ if (designatedEnv && info.env) return c.json(forgeRefusal(info.env, designatedEnv, `running ${name}`), 409);
9077
9458
  if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
9078
9459
  return c.json({ error: `an Op is already running (${runner.running})` }, 409);
9079
9460
  }
@@ -9105,8 +9486,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9105
9486
  return c.json({ started: true, name, ran: label });
9106
9487
  });
9107
9488
  app.post("/api/local/reset", (c) => {
9108
- const down = join18(cfg.projectDir, "scripts/local/local-down.sh");
9109
- const up = join18(cfg.projectDir, "scripts/local/local-up.sh");
9489
+ const down = join19(cfg.projectDir, "scripts/local/local-down.sh");
9490
+ const up = join19(cfg.projectDir, "scripts/local/local-up.sh");
9110
9491
  if (!existsSync13(down) || !existsSync13(up)) {
9111
9492
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
9112
9493
  }
@@ -9243,7 +9624,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9243
9624
  const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
9244
9625
  const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
9245
9626
  const emittedOps = discoverOpIrs(estateDirs).length;
9246
- const carveState = carveStatePayload(readCarveStates(estateDirs));
9627
+ const carveState = carveStatePayload(await carveStatesUnder(estateDirs));
9247
9628
  return c.json({
9248
9629
  projectDir: cfg.projectDir,
9249
9630
  ...carveState.manifests ? { carve: { state: carveState } } : {},
@@ -9270,6 +9651,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9270
9651
  // no `tiers` key) → no `tiers` field → the SPA's picker doesn't render
9271
9652
  // and the graph loads with no tier selected (web/app.js `initPickers`).
9272
9653
  ...beholdConfig.tiers ? { tiers: beholdConfig.tiers.values } : {},
9654
+ // The executor contract (#165): per designated env, whether Deploy can
9655
+ // dispatch right now and, if not, why — so the SPA disables the gesture
9656
+ // with the reason rather than offering a control that 4xxs.
9657
+ ...beholdConfig.executor ? { executor: Object.fromEntries(Object.entries(beholdConfig.executor).map(([env, d]) => [env, executorStatus(env, d)])) } : {},
9273
9658
  // The stack picker's options (#76, follow-up to #71): gated exactly like
9274
9659
  // `tiers` above — only present when `chant.config.ts` declares `stacks[]`
9275
9660
  // at all, so a single-stack/sourceDir-only/legacy project's SPA renders no
@@ -9294,9 +9679,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9294
9679
  app.post("/api/project/open", async (c) => {
9295
9680
  if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
9296
9681
  const body = await c.req.json().catch(() => ({}));
9297
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve6(body.dir.trim()) : "";
9682
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve7(body.dir.trim()) : "";
9298
9683
  if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
9299
- if (!existsSync13(join18(dir, "chant.config.ts"))) {
9684
+ if (!existsSync13(join19(dir, "chant.config.ts"))) {
9300
9685
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
9301
9686
  }
9302
9687
  switchServedProject([dir]);
@@ -9316,7 +9701,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9316
9701
  };
9317
9702
  app.post("/api/project/reveal", async (c) => {
9318
9703
  const body = await c.req.json().catch(() => ({}));
9319
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve6(body.dir.trim()) : cfg.projectDir;
9704
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve7(body.dir.trim()) : cfg.projectDir;
9320
9705
  const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
9321
9706
  if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
9322
9707
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
@@ -9384,7 +9769,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9384
9769
  const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
9385
9770
  if (!loaded.ok) return c.json({ error: loaded.error }, 500);
9386
9771
  const dirs = loaded.serveDirs;
9387
- if (!existsSync13(join18(dirs[0], "chant.config.ts"))) {
9772
+ if (!existsSync13(join19(dirs[0], "chant.config.ts"))) {
9388
9773
  return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
9389
9774
  }
9390
9775
  if (entry.serve.local) {
@@ -9482,7 +9867,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9482
9867
  path: "/api/demos/open",
9483
9868
  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)"
9484
9869
  },
9485
- { 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" },
9870
+ { 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" },
9486
9871
  ...cfg.carveReport ? [
9487
9872
  { method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
9488
9873
  {
@@ -9519,7 +9904,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9519
9904
  { method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
9520
9905
  { method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
9521
9906
  { method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
9522
- { method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline via the operator's gh" }
9907
+ { 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" }
9523
9908
  ]
9524
9909
  })
9525
9910
  );
@@ -9546,10 +9931,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9546
9931
  parsedOps.push(parsed.ir);
9547
9932
  }
9548
9933
  const declaredIr = opsToIr(parsedOps);
9934
+ if (url.searchParams.get("entities") === "1") {
9935
+ const memberDirs = [...new Set(files.map((f) => f.dir))];
9936
+ const memberReads = await Promise.allSettled(memberDirs.map((dir) => memberIr(dir)));
9937
+ const estateNodes = memberReads.flatMap((r) => r.status === "fulfilled" ? r.value.nodes : []);
9938
+ joinStepEntities(declaredIr, estateNodes.length ? { nodes: estateNodes, edges: [], groups: {} } : void 0);
9939
+ }
9549
9940
  const runState = runner.runState;
9550
9941
  const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
9551
- const join21 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
9552
- const opsIr = join21 ? paintPlayhead(declaredIr, join21, runState.gate) : declaredIr;
9942
+ const join22 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
9943
+ const opsIr = join22 ? paintPlayhead(declaredIr, join22, runState.gate) : declaredIr;
9553
9944
  const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
9554
9945
  const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
9555
9946
  return c.json({
@@ -9562,7 +9953,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9562
9953
  target: null,
9563
9954
  mode: "ops",
9564
9955
  ops: parsedOps.length,
9565
- note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join21)} ${operatorNote(operator)}`,
9956
+ note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join22)} ${operatorNote(operator)}`,
9566
9957
  // The operator strip's own model: the ConvergeOps this project
9567
9958
  // declares, plus whatever the last status read said. `declared`
9568
9959
  // without a `strip` is the honest starting state — a loop exists and
@@ -9634,6 +10025,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9634
10025
  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 } : {} } });
9635
10026
  } else {
9636
10027
  ir = mergeClusterRoot(await graphIr(cfg.projectDir, opts), await clusterRootGraphIr(cfg.projectDir, opts));
10028
+ ir = joinCarvedSources(ir, (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
9637
10029
  if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
9638
10030
  ir = addK8sDeclaredEdges(ir);
9639
10031
  ir = addValueMatchEdges(ir);
@@ -9651,7 +10043,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9651
10043
  srcCompositeEdgesAttached = 0;
9652
10044
  }
9653
10045
  }
9654
- if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join18(cfg.projectDir, "chant.config.ts"))) {
10046
+ if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join19(cfg.projectDir, "chant.config.ts"))) {
9655
10047
  return c.json(noProjectError(cfg.projectDir), 404);
9656
10048
  }
9657
10049
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -9698,23 +10090,28 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9698
10090
  }
9699
10091
  });
9700
10092
  app.post("/api/ci/dispatch", async (c) => {
10093
+ const env = new URL(c.req.url).searchParams.get("env") ?? cfg.env;
10094
+ const designated = executorFor(env);
10095
+ if (designated && "invalid" in designated) return c.json({ error: designated.invalid, code: "executor-forge", remedy: "Fix .behold.json's executor block." }, 400);
10096
+ if (designated && designated.forge !== "github") {
10097
+ return c.json({ error: executorStatus(env, designated).reason, code: "executor-forge" }, 400);
10098
+ }
9701
10099
  const ready = await ghReady();
9702
10100
  if (!ready.ok) return c.json({ error: ready.reason }, 400);
9703
- const pipeline = await ciPipeline(cfg.projectDir, cfg.env ? { env: cfg.env } : {}, "github").catch(() => void 0);
10101
+ const pipeline = await ciPipeline(cfg.projectDir, env ? { env } : {}, "github").catch(() => void 0);
9704
10102
  if (!pipeline || pipeline.jobs.length === 0) {
9705
10103
  return c.json({ error: "no generated GitHub pipeline \u2014 `chant build --components --generate github` produced no jobs" }, 400);
9706
10104
  }
9707
- const workflow = pickWorkflow(cfg.projectDir, pipeline);
9708
- if (!workflow) {
9709
- 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);
9710
- }
10105
+ const resolved = resolveWorkflow(cfg.projectDir, pipeline, designated?.workflow);
10106
+ if ("reason" in resolved) return c.json({ error: resolved.reason, ...designated ? { code: "executor-forge" } : {} }, 400);
10107
+ const workflow = resolved.workflow;
9711
10108
  const ref = await execFileP("git", ["-C", cfg.projectDir, "branch", "--show-current"]).then((out) => out.trim() || "main").catch(() => "main");
9712
10109
  const started = runner.track(
9713
10110
  `GitHub Actions ${workflow.file}`,
9714
10111
  (io) => dispatchAndFollow(workflow, pipeline, ref, { onLine: io.line, onProgress: io.progress })
9715
10112
  );
9716
10113
  if (!started) return c.json({ error: `busy \u2014 ${runner.running} is running` }, 409);
9717
- return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length });
10114
+ return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length, ...env ? { env } : {}, designated: Boolean(designated) });
9718
10115
  });
9719
10116
  app.get("/api/resources", async (c) => {
9720
10117
  const opts = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
@@ -9809,7 +10206,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9809
10206
  });
9810
10207
  }
9811
10208
  const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
9812
- let ir = reclassifyOverlay(await graphIr(cfg.projectDir, opts));
10209
+ let ir = joinCarvedSources(reclassifyOverlay(await graphIr(cfg.projectDir, opts)), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
9813
10210
  const boundContext = await boundK8sContext(env);
9814
10211
  ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
9815
10212
  const declaresHelm = await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("helm")).catch(() => false);
@@ -9817,7 +10214,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9817
10214
  const observed = await lifecycleDiffLive(cfg.projectDir, env, query).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
9818
10215
  applyHelmArtifacts(ir, observed);
9819
10216
  synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
9820
- applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query));
10217
+ applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query, observed));
9821
10218
  }
9822
10219
  ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
9823
10220
  if (logical) {
@@ -9868,7 +10265,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9868
10265
  const observed = await lifecycleDiffLive(cfg.projectDir, env).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
9869
10266
  applyHelmArtifacts(result.ir, observed);
9870
10267
  synthesizeHelmReleases(result.ir, observed, discoverReleaseUnits(cfg.projectDir));
9871
- applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env));
10268
+ applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env, {}, observed));
9872
10269
  }
9873
10270
  }
9874
10271
  if (env) {
@@ -9972,6 +10369,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9972
10369
  }
9973
10370
  const component = url.searchParams.get("component") || "all";
9974
10371
  const force = url.searchParams.get("force") === "1";
10372
+ const designated = executorFor(env);
10373
+ if (designated) return c.json(forgeRefusal(env, designated, "apply"), 409);
9975
10374
  const flociTargeted = !!process.env.AWS_ENDPOINT_URL && await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("aws")).catch(() => false);
9976
10375
  if (!force && flociTargeted) {
9977
10376
  try {
@@ -9996,9 +10395,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9996
10395
  }
9997
10396
  return c.json({ started: true, component, env });
9998
10397
  });
9999
- const rel = relative4(process.cwd(), webRoot) || ".";
10398
+ const rel = relative5(process.cwd(), webRoot) || ".";
10000
10399
  app.use("/*", serveStatic({ root: rel }));
10001
- app.get("/", serveStatic({ path: join18(rel, "index.html") }));
10400
+ app.get("/", serveStatic({ path: join19(rel, "index.html") }));
10002
10401
  return app;
10003
10402
  }
10004
10403
  async function bootLocalEmulators(dir, who) {
@@ -10027,7 +10426,7 @@ async function bootLocalEmulators(dir, who) {
10027
10426
  async function startServer(cfg) {
10028
10427
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
10029
10428
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
10030
- if (existsSync13(join18(dir, "chant.config.ts"))) addRecent(dir);
10429
+ if (existsSync13(join19(dir, "chant.config.ts"))) addRecent(dir);
10031
10430
  }
10032
10431
  const broadcaster = new Broadcaster();
10033
10432
  const frames = new FrameBuffer();
@@ -10053,11 +10452,19 @@ async function startServer(cfg) {
10053
10452
  if (autoSync === "off") return;
10054
10453
  void routeAutoSync(dir, movedLexicons);
10055
10454
  };
10455
+ const executorDesignations = loadBeholdConfig(cfg.projectDir).executor ?? {};
10456
+ const executorFor = (env) => env ? executorDesignations[env] : void 0;
10056
10457
  const routeAutoSync = async (dir, movedLexicons) => {
10057
10458
  const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
10459
+ const allOps = discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]);
10460
+ const routed = allOps.filter((o) => o.kind === "apply" && o.env && executorFor(o.env));
10461
+ for (const o of routed) {
10462
+ const d = executorFor(o.env);
10463
+ 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`);
10464
+ }
10058
10465
  const { picks, declined } = pickAutoSyncOps(
10059
10466
  autoSync,
10060
- discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]),
10467
+ allOps.filter((o) => !routed.includes(o)),
10061
10468
  runner.running,
10062
10469
  movedLexicons,
10063
10470
  suspended
@@ -10166,8 +10573,8 @@ async function startServer(cfg) {
10166
10573
  }
10167
10574
 
10168
10575
  // src/export.ts
10169
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "node:fs";
10170
- import { join as join19, dirname as dirname6, basename as basename2 } from "node:path";
10576
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as readdirSync8 } from "node:fs";
10577
+ import { join as join20, dirname as dirname6, basename as basename2 } from "node:path";
10171
10578
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10172
10579
  var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
10173
10580
  function canonicalKey(path, params) {
@@ -10185,7 +10592,7 @@ function captureKeys(axes) {
10185
10592
  add("/api/project", {});
10186
10593
  add("/api/substrates", {});
10187
10594
  add("/api/ops", {});
10188
- if (axes.ops) add("/api/graph", { ops: "1" });
10595
+ if (axes.ops) add("/api/graph", { ops: "1", entities: "1" });
10189
10596
  const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
10190
10597
  const envs = ["", ...axes.environments];
10191
10598
  for (const env of envs) {
@@ -10216,7 +10623,7 @@ function captureKeys(axes) {
10216
10623
  return [...keys];
10217
10624
  }
10218
10625
  function webDir() {
10219
- return join19(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
10626
+ return join20(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
10220
10627
  }
10221
10628
  function workerName(project, override) {
10222
10629
  const raw = override ?? `behold-${basename2(project)}`;
@@ -10227,7 +10634,7 @@ async function runExport(cfg, outDir, opts = {}) {
10227
10634
  const app = createApp({ ...cfg, layoutWrites: false });
10228
10635
  const proj = await (await app.request("/api/project")).json();
10229
10636
  const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
10230
- const snapDir = join19(outDir, "snapshots");
10637
+ const snapDir = join20(outDir, "snapshots");
10231
10638
  mkdirSync4(snapDir, { recursive: true });
10232
10639
  const keyToFile = {};
10233
10640
  let ok = 0;
@@ -10236,7 +10643,7 @@ async function runExport(cfg, outDir, opts = {}) {
10236
10643
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
10237
10644
  const body = await res.text();
10238
10645
  const file = slug2(key);
10239
- writeFileSync4(join19(snapDir, file), body);
10646
+ writeFileSync4(join20(snapDir, file), body);
10240
10647
  keyToFile[key] = `snapshots/${file}`;
10241
10648
  if (res.ok) ok++;
10242
10649
  else failed++;
@@ -10248,21 +10655,21 @@ async function runExport(cfg, outDir, opts = {}) {
10248
10655
  axes,
10249
10656
  keyToFile
10250
10657
  };
10251
- writeFileSync4(join19(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
10252
- const html = readFileSync15(join19(webDir(), "index.html"), "utf8").replace(
10658
+ writeFileSync4(join20(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
10659
+ const html = readFileSync15(join20(webDir(), "index.html"), "utf8").replace(
10253
10660
  /<\/head>/i,
10254
10661
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
10255
10662
  </head>`
10256
10663
  );
10257
- writeFileSync4(join19(outDir, "index.html"), html);
10258
- for (const f of readdirSync7(webDir())) {
10664
+ writeFileSync4(join20(outDir, "index.html"), html);
10665
+ for (const f of readdirSync8(webDir())) {
10259
10666
  if (f === "index.html") continue;
10260
- copyFileSync(join19(webDir(), f), join19(outDir, f));
10667
+ copyFileSync(join20(webDir(), f), join20(outDir, f));
10261
10668
  }
10262
- writeFileSync4(join19(outDir, "README.md"), BUNDLE_README);
10669
+ writeFileSync4(join20(outDir, "README.md"), BUNDLE_README);
10263
10670
  const name = workerName(cfg.projectDir, opts.name);
10264
10671
  writeFileSync4(
10265
- join19(outDir, "wrangler.jsonc"),
10672
+ join20(outDir, "wrangler.jsonc"),
10266
10673
  JSON.stringify(
10267
10674
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
10268
10675
  null,
@@ -10310,7 +10717,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
10310
10717
  `;
10311
10718
 
10312
10719
  // src/doctor.ts
10313
- import { relative as relative5, resolve as resolve7 } from "node:path";
10720
+ import { relative as relative6, resolve as resolve8 } from "node:path";
10314
10721
  var list2 = (xs) => xs.join(", ");
10315
10722
  var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
10316
10723
  function summarize(xs, max = 4) {
@@ -10318,7 +10725,7 @@ function summarize(xs, max = 4) {
10318
10725
  }
10319
10726
  function labelFor(root, target, estate) {
10320
10727
  if (!estate) return "";
10321
- const rel = relative5(root, target);
10728
+ const rel = relative6(root, target);
10322
10729
  return `${rel || "."}: `;
10323
10730
  }
10324
10731
  function chantCheck(root, targets, estate) {
@@ -10331,7 +10738,7 @@ function chantCheck(root, targets, estate) {
10331
10738
  name: "chant",
10332
10739
  status: "fail",
10333
10740
  detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
10334
- fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative5(root, target) || target))}`
10741
+ fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative6(root, target) || target))}`
10335
10742
  };
10336
10743
  }
10337
10744
  const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
@@ -10459,7 +10866,7 @@ function opsCheck(root, ops, chantSource, estate) {
10459
10866
  return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
10460
10867
  }
10461
10868
  async function diagnose(dir, probes = {}) {
10462
- const root = resolve7(dir);
10869
+ const root = resolve8(dir);
10463
10870
  const shape = detectProjectShape(root);
10464
10871
  const behold = beholdVersion();
10465
10872
  if (shape.kind === "none") {
@@ -10479,16 +10886,16 @@ async function diagnose(dir, probes = {}) {
10479
10886
  };
10480
10887
  }
10481
10888
  const estate = shape.kind === "estate";
10482
- const members = (shape.members ?? []).map((m) => resolve7(root, m));
10889
+ const members = (shape.members ?? []).map((m) => resolve8(root, m));
10483
10890
  const targets = estate ? members : [root];
10484
10891
  const primary = targets[0];
10485
10892
  const projectCheck = estate ? {
10486
10893
  name: "project",
10487
10894
  status: "pass",
10488
10895
  detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
10489
- members.map((m) => relative5(root, m))
10896
+ members.map((m) => relative6(root, m))
10490
10897
  )}`
10491
- } : { name: "project", status: "pass", detail: `chant project (${relative5(root, shape.configFile)})` };
10898
+ } : { name: "project", status: "pass", detail: `chant project (${relative6(root, shape.configFile)})` };
10492
10899
  const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
10493
10900
  const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
10494
10901
  const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
@@ -10713,7 +11120,7 @@ ${USAGE}`);
10713
11120
  process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
10714
11121
  process.exit(2);
10715
11122
  }
10716
- const dirs = projectDirs.map((d) => resolve8(d));
11123
+ const dirs = projectDirs.map((d) => resolve9(d));
10717
11124
  for (const d of dirs) warnIfNotChantProject(d);
10718
11125
  await startServer({
10719
11126
  projectDir: dirs[0],
@@ -10732,7 +11139,7 @@ function warnIfNotChantProject(dir) {
10732
11139
  if (shape.kind === "estate") {
10733
11140
  process.stderr.write(
10734
11141
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
10735
- Serve its members composed: behold serve ${shape.members.map((m) => join20(dir, m)).join(" ")}
11142
+ Serve its members composed: behold serve ${shape.members.map((m) => join21(dir, m)).join(" ")}
10736
11143
  `
10737
11144
  );
10738
11145
  return;
@@ -10767,7 +11174,7 @@ async function runCarve(rest) {
10767
11174
  );
10768
11175
  process.exit(2);
10769
11176
  }
10770
- const reportPath = resolve8(fileArg);
11177
+ const reportPath = resolve9(fileArg);
10771
11178
  const parsed = readCarveReport(reportPath, (p) => readFileSync16(p, "utf8"));
10772
11179
  if (!parsed.ok) {
10773
11180
  process.stderr.write(`behold carve: ${parsed.refusal.error}
@@ -10795,8 +11202,8 @@ async function runDoctor(rest) {
10795
11202
  }
10796
11203
  }
10797
11204
  const dir = dirArg ?? ".";
10798
- if (!existsSync14(resolve8(dir))) {
10799
- process.stderr.write(`behold doctor: no such directory: ${resolve8(dir)}
11205
+ if (!existsSync14(resolve9(dir))) {
11206
+ process.stderr.write(`behold doctor: no such directory: ${resolve9(dir)}
10800
11207
  `);
10801
11208
  process.exit(2);
10802
11209
  }
@@ -10805,7 +11212,7 @@ async function runDoctor(rest) {
10805
11212
  if (!report.ok) process.exitCode = 1;
10806
11213
  }
10807
11214
  async function runDemo(rest) {
10808
- const pkgRoot2 = join20(dirname7(fileURLToPath5(import.meta.url)), "..");
11215
+ const pkgRoot2 = join21(dirname7(fileURLToPath5(import.meta.url)), "..");
10809
11216
  const registry = loadDemoRegistry(pkgRoot2);
10810
11217
  let port = 4600;
10811
11218
  let live = false;
@@ -10859,7 +11266,7 @@ async function runDemo(rest) {
10859
11266
  `);
10860
11267
  process.exit(2);
10861
11268
  }
10862
- const target = dirArg ? resolve8(dirArg) : demoTargetDir(entry);
11269
+ const target = dirArg ? resolve9(dirArg) : demoTargetDir(entry);
10863
11270
  const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
10864
11271
  if (!loaded.ok) {
10865
11272
  process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
@@ -10890,7 +11297,7 @@ function spawnStep(cmd, args, cwd) {
10890
11297
  });
10891
11298
  }
10892
11299
  async function serveCarveDemo(target, carve, port, live = false) {
10893
- const at = (rel) => resolve8(target, rel);
11300
+ const at = (rel) => resolve9(target, rel);
10894
11301
  const project = at(carve.project);
10895
11302
  const from = at(carve.from);
10896
11303
  const state = carve.state ? at(carve.state) : void 0;
@@ -10930,7 +11337,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
10930
11337
  };
10931
11338
  process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
10932
11339
  }
10933
- if (existsSync14(join20(project, "package.json")) && !existsSync14(join20(project, "node_modules"))) {
11340
+ if (existsSync14(join21(project, "package.json")) && !existsSync14(join21(project, "node_modules"))) {
10934
11341
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
10935
11342
  `);
10936
11343
  const code = await spawnStep("npm", ["install"], project);
@@ -10941,7 +11348,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
10941
11348
  }
10942
11349
  }
10943
11350
  let degraded;
10944
- if (!existsSync14(join20(target, "node_modules", "@cdktf", "hcl2json"))) {
11351
+ if (!existsSync14(join21(target, "node_modules", "@cdktf", "hcl2json"))) {
10945
11352
  process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
10946
11353
  const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
10947
11354
  if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
@@ -11010,7 +11417,7 @@ async function runPreview(rest) {
11010
11417
  process.stderr.write("behold preview: --port must be a number\n");
11011
11418
  process.exit(2);
11012
11419
  }
11013
- const projectDir = resolve8(dirArg ?? process.cwd());
11420
+ const projectDir = resolve9(dirArg ?? process.cwd());
11014
11421
  if (!existsSync14(projectDir)) {
11015
11422
  process.stderr.write(`behold preview: project not found at ${projectDir}
11016
11423
  `);
@@ -11031,21 +11438,21 @@ async function runPreview(rest) {
11031
11438
  await startServer({ projectDir, port, env: "local", previewMode: true });
11032
11439
  }
11033
11440
  async function runExportCmd(rest) {
11034
- let outDir = resolve8("behold-export");
11441
+ let outDir = resolve9("behold-export");
11035
11442
  let env;
11036
11443
  let name;
11037
11444
  let dirArg;
11038
11445
  let emulator = false;
11039
11446
  for (let i = 0; i < rest.length; i++) {
11040
11447
  const a = rest[i];
11041
- if (a === "--out") outDir = resolve8(rest[++i]);
11448
+ if (a === "--out") outDir = resolve9(rest[++i]);
11042
11449
  else if (a === "--env") env = rest[++i];
11043
11450
  else if (a === "--name") name = rest[++i];
11044
11451
  else if (a === "--emulator") emulator = true;
11045
11452
  else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
11046
11453
  else if (!a.startsWith("-")) dirArg = a;
11047
11454
  }
11048
- const projectDir = resolve8(dirArg ?? process.cwd());
11455
+ const projectDir = resolve9(dirArg ?? process.cwd());
11049
11456
  if (emulator) {
11050
11457
  injectEmulatorEnv(env);
11051
11458
  env ??= "local";