@intentius/behold 0.19.0 → 0.20.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.
Files changed (42) hide show
  1. package/AGENTS.md +90 -5
  2. package/README.md +5 -1
  3. package/demos.json +3 -2
  4. package/dist/cli.js +1106 -298
  5. package/example-argo-estate/README.md +8 -8
  6. package/example-argo-estate/app-a/package.json +2 -2
  7. package/example-argo-estate/app-b/package.json +2 -2
  8. package/example-argo-estate/chant.workspace.json +27 -0
  9. package/example-argo-estate/control-plane/package.json +2 -2
  10. package/example-argo-estate/package-lock.json +133 -56
  11. package/example-carve/README.md +14 -14
  12. package/example-carve/app/package-lock.json +72 -9
  13. package/example-carve/app/package.json +2 -2
  14. package/example-carve/legacy-tf/versions.tf +1 -1
  15. package/example-choudoufu-estate/README.md +4 -4
  16. package/example-choudoufu-estate/chant.workspace.json +32 -0
  17. package/example-choudoufu-estate/scripts/choudoufu-up.sh +21 -0
  18. package/example-flux-estate/README.md +5 -5
  19. package/example-flux-estate/app-a/package.json +2 -2
  20. package/example-flux-estate/app-b/package.json +2 -2
  21. package/example-flux-estate/chant.workspace.json +27 -0
  22. package/example-flux-estate/control-plane/package.json +2 -2
  23. package/example-flux-estate/package-lock.json +133 -56
  24. package/example-k8s/README.md +20 -20
  25. package/example-k8s/chant.config.ts +1 -1
  26. package/example-k8s/ops/k3d-apply.op.ts +1 -1
  27. package/example-k8s/package-lock.json +129 -211
  28. package/example-k8s/package.json +2 -3
  29. package/example-terraform-estate/README.md +5 -5
  30. package/example-terraform-estate/chant.workspace.json +22 -0
  31. package/example-writes/README.md +5 -5
  32. package/example-writes/chant.config.ts +1 -1
  33. package/example-writes/ops/apply.op.ts +1 -1
  34. package/example-writes/ops/floci.op.ts +2 -2
  35. package/example-writes/ops/reconcile.op.ts +1 -1
  36. package/example-writes/package-lock.json +62 -175
  37. package/example-writes/package.json +2 -3
  38. package/package.json +6 -6
  39. package/web/app.js +54 -8
  40. package/web/behaviour-scale.js +32 -0
  41. package/web/behaviour-scale.test.js +39 -0
  42. package/example-choudoufu-estate/.behold.json +0 -8
package/dist/cli.js CHANGED
@@ -118,17 +118,17 @@ var init_read_scheduler = __esm({
118
118
  recordRefused();
119
119
  return Promise.reject(new Error("Chant read queue is full; retry after current reads finish"));
120
120
  }
121
- let resolve15;
121
+ let resolve18;
122
122
  let reject;
123
123
  const promise = new Promise((yes, no) => {
124
- resolve15 = yes;
124
+ resolve18 = yes;
125
125
  reject = no;
126
126
  });
127
127
  job = {
128
128
  key,
129
129
  run: run3,
130
130
  promise,
131
- resolve: resolve15,
131
+ resolve: resolve18,
132
132
  reject,
133
133
  controller: new AbortController(),
134
134
  users: 0,
@@ -142,7 +142,7 @@ var init_read_scheduler = __esm({
142
142
  }
143
143
  const shared2 = job;
144
144
  shared2.users++;
145
- const result = new Promise((resolve15, reject) => {
145
+ const result = new Promise((resolve18, reject) => {
146
146
  let done = false;
147
147
  const finish = (error, value, failed = false) => {
148
148
  if (done) return;
@@ -150,7 +150,7 @@ var init_read_scheduler = __esm({
150
150
  signal?.removeEventListener("abort", abort);
151
151
  shared2.users--;
152
152
  if (failed) reject(error);
153
- else resolve15(value);
153
+ else resolve18(value);
154
154
  };
155
155
  const abort = () => {
156
156
  finish(signal?.reason, void 0, true);
@@ -1038,11 +1038,11 @@ async function createK8sClient(options = {}) {
1038
1038
  }
1039
1039
  return void 0;
1040
1040
  }
1041
- async function resolve15(selector, signal) {
1041
+ async function resolve18(selector, signal) {
1042
1042
  return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
1043
1043
  }
1044
1044
  async function resolveOrThrow(selector, signal) {
1045
- const info = await resolve15(selector, signal);
1045
+ const info = await resolve18(selector, signal);
1046
1046
  if (!info) throw new UnknownResourceError(selectorText(selector));
1047
1047
  return info;
1048
1048
  }
@@ -1054,7 +1054,7 @@ async function createK8sClient(options = {}) {
1054
1054
  return parts.join("/");
1055
1055
  }
1056
1056
  async function pathFor(ref, signal) {
1057
- const info = await resolve15({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
1057
+ const info = await resolve18({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
1058
1058
  if (!info) return void 0;
1059
1059
  return objectPath(info, ref.name, ref.namespace);
1060
1060
  }
@@ -1168,8 +1168,8 @@ async function createK8sClient(options = {}) {
1168
1168
  const { lines } = await openStream(listPath, query, controller.signal, target);
1169
1169
  let expired = false;
1170
1170
  const iterator = lines[Symbol.asyncIterator]();
1171
- const stopped = new Promise((resolve16) => {
1172
- const finish = () => resolve16({ done: true, value: void 0 });
1171
+ const stopped = new Promise((resolve19) => {
1172
+ const finish = () => resolve19({ done: true, value: void 0 });
1173
1173
  if (controller.signal.aborted) finish();
1174
1174
  else controller.signal.addEventListener("abort", finish, { once: true });
1175
1175
  });
@@ -1196,11 +1196,11 @@ async function createK8sClient(options = {}) {
1196
1196
  resourceVersion = await listResourceVersion();
1197
1197
  continue;
1198
1198
  }
1199
- await new Promise((resolve16) => {
1200
- const timer = setTimeout(resolve16, reopenDelayMs);
1199
+ await new Promise((resolve19) => {
1200
+ const timer = setTimeout(resolve19, reopenDelayMs);
1201
1201
  controller.signal.addEventListener("abort", () => {
1202
1202
  clearTimeout(timer);
1203
- resolve16(void 0);
1203
+ resolve19(void 0);
1204
1204
  }, { once: true });
1205
1205
  });
1206
1206
  }
@@ -1318,7 +1318,7 @@ async function createK8sClient(options = {}) {
1318
1318
  return {
1319
1319
  provenance,
1320
1320
  defaultNamespace,
1321
- resolve: resolve15,
1321
+ resolve: resolve18,
1322
1322
  read,
1323
1323
  pathFor,
1324
1324
  readIfPresent,
@@ -1576,8 +1576,8 @@ function parseLiveCheckOutput(run3, dir) {
1576
1576
  if (run3.code === 127) return refuse(`choudoufu is not on PATH (${stripAnsi(run3.stderr).trim() || "spawn failed"}).`, INSTALL);
1577
1577
  const text = run3.stdout.trim();
1578
1578
  if (!text) {
1579
- const said3 = stripAnsiLines(run3.stderr);
1580
- const why = said3.find((l) => /^Error:/.test(l)) ?? said3[0] ?? `exit ${run3.code}`;
1579
+ const said4 = stripAnsiLines(run3.stderr);
1580
+ const why = said4.find((l) => /^Error:/.test(l)) ?? said4[0] ?? `exit ${run3.code}`;
1581
1581
  return refuse(`choudoufu could not read ${dir}: ${why}`, "Fix the configuration until `choudoufu live-check` prints a document, then reload.");
1582
1582
  }
1583
1583
  let json;
@@ -1734,6 +1734,9 @@ function choudoufuVersion(bin = choudoufuBinary()) {
1734
1734
  function setChoudoufuSpawnEnv(env) {
1735
1735
  spawnEnvOverride = env && Object.keys(env).length ? { ...env } : void 0;
1736
1736
  }
1737
+ function choudoufuSpawnOverride() {
1738
+ return spawnEnvOverride ? { ...spawnEnvOverride } : void 0;
1739
+ }
1737
1740
  function choudoufuSpawnEnv(base = process.env) {
1738
1741
  return { ...base, ...spawnEnvOverride ?? {}, NO_COLOR: "1" };
1739
1742
  }
@@ -2312,9 +2315,9 @@ function beholdNodeModules(from = beholdDir()) {
2312
2315
  dir = parent;
2313
2316
  }
2314
2317
  }
2315
- function terraformScratchDir(estate, tmp = tmpdir()) {
2318
+ function terraformScratchDir(estate, tmp = tmpdir(), flavour = "tf") {
2316
2319
  const target = resolve(estate);
2317
- const name = `${SCRATCH_PREFIX}tf-${createHash("sha1").update(target).digest("hex").slice(0, 12)}`;
2320
+ const name = `${SCRATCH_PREFIX}${flavour}-${createHash("sha1").update(target).digest("hex").slice(0, 12)}`;
2318
2321
  assertScratch(name);
2319
2322
  let root = resolve(tmp);
2320
2323
  try {
@@ -2327,7 +2330,7 @@ function terraformScratchDir(estate, tmp = tmpdir()) {
2327
2330
  }
2328
2331
  return dir;
2329
2332
  }
2330
- function terraformScratchConfig(scan, estateLink) {
2333
+ function terraformScratchConfig(scan, estateLink, flavour = "tf") {
2331
2334
  const roots = [...scan.roots].sort((a, b) => a.name < b.name ? -1 : 1);
2332
2335
  const entries = roots.map((r) => ` ${JSON.stringify(r.name)}: { dir: ${JSON.stringify(r.dir === "." ? estateLink : `${estateLink}/${r.dir}`)} },`);
2333
2336
  return [
@@ -2341,6 +2344,7 @@ function terraformScratchConfig(scan, estateLink) {
2341
2344
  "export default {",
2342
2345
  ' lexicons: ["terraform"],',
2343
2346
  " terraform: {",
2347
+ ...flavour === "chdf" ? [' binary: "choudoufu",'] : [],
2344
2348
  " roots: {",
2345
2349
  ...entries,
2346
2350
  " },",
@@ -2357,14 +2361,14 @@ function relink(path, target) {
2357
2361
  }
2358
2362
  symlinkSync(target, path);
2359
2363
  }
2360
- function writeTerraformScratchProject(estate, scan, tmp = tmpdir()) {
2364
+ function writeTerraformScratchProject(estate, scan, tmp = tmpdir(), flavour = "tf") {
2361
2365
  const target = resolve(estate);
2362
- const dir = terraformScratchDir(target, tmp);
2366
+ const dir = terraformScratchDir(target, tmp, flavour);
2363
2367
  mkdirSync2(dir, { recursive: true });
2364
2368
  relink(join3(dir, ESTATE_LINK), target);
2365
2369
  const modules = beholdNodeModules();
2366
2370
  if (modules) relink(join3(dir, "node_modules"), modules);
2367
- const config = terraformScratchConfig(scan, ESTATE_LINK);
2371
+ const config = terraformScratchConfig(scan, ESTATE_LINK, flavour);
2368
2372
  const path = join3(dir, "chant.config.ts");
2369
2373
  if (!existsSync3(path) || readFileSync3(path, "utf8") !== config) writeFileSync2(path, config);
2370
2374
  return dir;
@@ -2398,7 +2402,7 @@ var init_terraform_member = __esm({
2398
2402
  init_terraform_lens();
2399
2403
  init_scratch();
2400
2404
  TERRAFORM_LEXICON_PKG = "@intentius/chant-lexicon-terraform";
2401
- HCL_PARSER_PKG = "@cdktf/hcl2json";
2405
+ HCL_PARSER_PKG = "@cdktn/hcl2json";
2402
2406
  ROOT_SCAN_DEPTH = 3;
2403
2407
  SKIP_DIRS = /* @__PURE__ */ new Set([".terraform", "node_modules", ".git", "dist", ".chant", "cdk.out"]);
2404
2408
  ROOT_BLOCK = /^(terraform\s*\{|provider\s+")/m;
@@ -2636,6 +2640,7 @@ function verdictFor(address, ls, plan) {
2636
2640
  const gap = ls?.gaps?.find((g) => g.address === address);
2637
2641
  const omission = plan?.omissions?.find((o) => o.addr === address);
2638
2642
  const unowned = plan?.unowned?.find((u) => u.addr === address);
2643
+ const adoptable = plan?.adoptable?.find((a) => a.addr === address);
2639
2644
  const listing = gap ? { listing: gap.detail } : {};
2640
2645
  if (bound) {
2641
2646
  const id = bound.identity || item?.id;
@@ -2649,6 +2654,26 @@ function verdictFor(address, ls, plan) {
2649
2654
  if (omission) {
2650
2655
  const tone = REASON_TONE[omission.reason] ?? "neutral";
2651
2656
  const base = { omission: omission.reason, detail: omission.detail, ...listing };
2657
+ if (omission.reason === "NEEDS_DISCOVERY" && adoptable) {
2658
+ const adopt = adoptLine(adoptable);
2659
+ const matched = (adoptable.matched ?? []).filter((m) => m && m.attribute);
2660
+ return {
2661
+ _status: "warn",
2662
+ physicalId: adoptable.identity,
2663
+ ownership: "foreign",
2664
+ attrs: {
2665
+ // The write to run first, as #393 item 10 settled for the unowned
2666
+ // card. behold composes none of its own: `adoptLine` prints
2667
+ // choudoufu's `adopt_command` when there is one and the two marker
2668
+ // values when there is not.
2669
+ ...adopt ? { adopt } : {},
2670
+ // Then why choudoufu believes it. Rendered as the estate's own
2671
+ // words rather than a shape behold invented.
2672
+ ...matched.length ? { matchedOn: matched.map((m) => `${m.attribute}=${m.value}`).join(", ") } : {},
2673
+ ...base
2674
+ }
2675
+ };
2676
+ }
2652
2677
  if (omission.reason === "UNOWNED" && unowned) {
2653
2678
  const adopt = adoptLine(unowned);
2654
2679
  if (adopt) {
@@ -2918,6 +2943,12 @@ var init_member_kind = __esm({
2918
2943
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
2919
2944
  import { join as join4 } from "node:path";
2920
2945
  import { pathToFileURL } from "node:url";
2946
+ function readBehaviour(raw) {
2947
+ const b = raw.behaviour;
2948
+ if (!b || typeof b !== "object" || Array.isArray(b)) return void 0;
2949
+ const traffic = b.traffic;
2950
+ return typeof traffic === "string" && traffic.trim() ? { traffic: traffic.trim() } : void 0;
2951
+ }
2921
2952
  function readMember(v) {
2922
2953
  if (typeof v === "string") return v ? { dir: v, kind: "chant" } : void 0;
2923
2954
  if (!v || typeof v !== "object") return void 0;
@@ -3072,7 +3103,13 @@ function loadBeholdConfig(projectDir) {
3072
3103
  const tiers = readTiers(raw);
3073
3104
  const members = Array.isArray(raw.members) ? raw.members.map(readMember).filter((m) => !!m) : [];
3074
3105
  const executor = readExecutor(raw);
3075
- return { ...tiers ? { tiers } : {}, ...members.length ? { members } : {}, ...executor ? { executor } : {} };
3106
+ const behaviour = readBehaviour(raw);
3107
+ return {
3108
+ ...tiers ? { tiers } : {},
3109
+ ...members.length ? { members } : {},
3110
+ ...executor ? { executor } : {},
3111
+ ...behaviour ? { behaviour } : {}
3112
+ };
3076
3113
  } catch {
3077
3114
  return {};
3078
3115
  }
@@ -3142,8 +3179,12 @@ var init_project = __esm({
3142
3179
  import { createHash as createHash2 } from "node:crypto";
3143
3180
  import { readdirSync as readdirSync3, statSync } from "node:fs";
3144
3181
  import { join as join5, relative as relative2, resolve as resolve2 } from "node:path";
3182
+ function setStampExclusions(next) {
3183
+ exclusions = new Map([...next].map(([k, v]) => [resolve2(k), new Set(v.map((d) => resolve2(d)))]));
3184
+ }
3145
3185
  function memberSourceStamp(dir) {
3146
3186
  const root = resolve2(dir);
3187
+ const skipDirs = exclusions.get(root);
3147
3188
  const h = createHash2("sha1");
3148
3189
  let any = false;
3149
3190
  const walk = (at) => {
@@ -3152,7 +3193,7 @@ function memberSourceStamp(dir) {
3152
3193
  if (SKIP.has(e.name)) continue;
3153
3194
  const path = join5(at, e.name);
3154
3195
  if (e.isDirectory()) {
3155
- walk(path);
3196
+ if (!skipDirs?.has(path)) walk(path);
3156
3197
  continue;
3157
3198
  }
3158
3199
  if (!e.isFile()) continue;
@@ -3169,11 +3210,12 @@ function memberSourceStamp(dir) {
3169
3210
  }
3170
3211
  return any ? h.digest("hex") : void 0;
3171
3212
  }
3172
- var SKIP;
3213
+ var SKIP, exclusions;
3173
3214
  var init_member_source = __esm({
3174
3215
  "src/member-source.ts"() {
3175
3216
  "use strict";
3176
3217
  SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
3218
+ exclusions = /* @__PURE__ */ new Map();
3177
3219
  }
3178
3220
  });
3179
3221
 
@@ -3887,6 +3929,7 @@ var chant_exports = {};
3887
3929
  __export(chant_exports, {
3888
3930
  CI_FORGES: () => CI_FORGES,
3889
3931
  ChantCliError: () => ChantCliError,
3932
+ TRAFFIC_FLOOR: () => TRAFFIC_FLOOR,
3890
3933
  applyArgs: () => applyArgs,
3891
3934
  carveStatus: () => carveStatus,
3892
3935
  chantBinFrom: () => chantBinFrom,
@@ -3920,6 +3963,7 @@ __export(chant_exports, {
3920
3963
  lifecyclePlan: () => lifecyclePlan,
3921
3964
  lifecyclePlanArgs: () => lifecyclePlanArgs,
3922
3965
  meetsFloor: () => meetsFloor,
3966
+ memberTakesTraffic: () => memberTakesTraffic,
3923
3967
  parseCiPipeline: () => parseCiPipeline,
3924
3968
  pipelineEnvFromYaml: () => pipelineEnvFromYaml,
3925
3969
  readsInFlight: () => readsInFlight,
@@ -3940,7 +3984,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
3940
3984
  import { dirname as dirname4, join as join7, resolve as resolve5 } from "node:path";
3941
3985
  import { createHash as createHash3 } from "node:crypto";
3942
3986
  function envOverridesFor(opts) {
3943
- const overrides = {};
3987
+ const overrides = { ...opts.spawnEnv ?? {} };
3944
3988
  if (opts.tier && opts.tierEnvVar) overrides[opts.tierEnvVar] = opts.tier;
3945
3989
  if (opts.substrateTargets?.length) {
3946
3990
  Object.assign(overrides, targetEnvOverrides(opts.substrateTargets, opts.target));
@@ -3949,6 +3993,11 @@ function envOverridesFor(opts) {
3949
3993
  }
3950
3994
  return Object.keys(overrides).length ? overrides : void 0;
3951
3995
  }
3996
+ function memberTakesTraffic(dir, kind) {
3997
+ if (kind === "terraform") return true;
3998
+ if (kind !== "chant") return false;
3999
+ return meetsFloor(resolveChant(dir).version, TRAFFIC_FLOOR);
4000
+ }
3952
4001
  function graphFlags(opts) {
3953
4002
  const flags = [];
3954
4003
  if (opts.detail !== void 0) flags.push("--detail", String(opts.detail));
@@ -3959,6 +4008,7 @@ function graphFlags(opts) {
3959
4008
  if (opts.live) flags.push("--live");
3960
4009
  if (opts.overlay) flags.push("--overlay");
3961
4010
  if (opts.namespace) flags.push("--namespace", opts.namespace);
4011
+ if (opts.traffic) flags.push("--traffic", opts.traffic);
3962
4012
  return flags;
3963
4013
  }
3964
4014
  function packageManifestFrom(req, name) {
@@ -4304,7 +4354,7 @@ async function helmRenderDiffLive(projectDir, contentDigest, env, opts = {}) {
4304
4354
  function applyArgs(target, env) {
4305
4355
  return ["run", target, "--components", "--env", env, "--progress-json"];
4306
4356
  }
4307
- var reads, unstampableRead, ChantCliError, CI_FORGES;
4357
+ var TRAFFIC_FLOOR, reads, unstampableRead, ChantCliError, CI_FORGES;
4308
4358
  var init_chant = __esm({
4309
4359
  "src/chant.ts"() {
4310
4360
  "use strict";
@@ -4318,6 +4368,7 @@ var init_chant = __esm({
4318
4368
  init_yaml();
4319
4369
  init_carve_manifest();
4320
4370
  init_foreign();
4371
+ TRAFFIC_FLOOR = "0.75.0";
4321
4372
  reads = new ReadScheduler();
4322
4373
  unstampableRead = 0;
4323
4374
  ChantCliError = class extends Error {
@@ -4333,8 +4384,8 @@ var init_chant = __esm({
4333
4384
  });
4334
4385
 
4335
4386
  // src/cli.ts
4336
- import { resolve as resolve14, dirname as dirname9, join as join27 } from "node:path";
4337
- import { realpathSync as realpathSync2, existsSync as existsSync19, readFileSync as readFileSync21, rmSync as rmSync4 } from "node:fs";
4387
+ import { resolve as resolve17, dirname as dirname10, join as join29 } from "node:path";
4388
+ import { realpathSync as realpathSync2, existsSync as existsSync21, readFileSync as readFileSync22, rmSync as rmSync4 } from "node:fs";
4338
4389
  import { fileURLToPath as fileURLToPath6 } from "node:url";
4339
4390
  import { spawn as spawn7, spawnSync as spawnSync3 } from "node:child_process";
4340
4391
 
@@ -4392,8 +4443,8 @@ import { serve } from "@hono/node-server";
4392
4443
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4393
4444
  import { execFile as execFile3 } from "node:child_process";
4394
4445
  import { promisify as promisify3 } from "node:util";
4395
- import { existsSync as existsSync17, readFileSync as readFileSync19 } from "node:fs";
4396
- import { basename as basename2, dirname as dirname7, join as join25, relative as relative8, resolve as resolve13, sep as sep7 } from "node:path";
4446
+ import { existsSync as existsSync18, readFileSync as readFileSync19 } from "node:fs";
4447
+ import { basename as basename3, dirname as dirname7, join as join26, relative as relative8, resolve as resolve15, sep as sep7 } from "node:path";
4397
4448
 
4398
4449
  // src/recents.ts
4399
4450
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -4635,19 +4686,19 @@ function finishPipelineProgress(state, exitCode) {
4635
4686
  }
4636
4687
 
4637
4688
  // src/gh-run.ts
4638
- var defaultGhExec = (args) => new Promise((resolve15) => {
4689
+ var defaultGhExec = (args) => new Promise((resolve18) => {
4639
4690
  let out = "";
4640
4691
  let proc;
4641
4692
  try {
4642
4693
  proc = spawn3("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
4643
4694
  } catch {
4644
- resolve15({ code: 127, out: "" });
4695
+ resolve18({ code: 127, out: "" });
4645
4696
  return;
4646
4697
  }
4647
4698
  proc.stdout.on("data", (d) => out += d);
4648
4699
  proc.stderr.on("data", (d) => out += d);
4649
- proc.on("error", () => resolve15({ code: 127, out }));
4650
- proc.on("close", (code) => resolve15({ code: code ?? 1, out }));
4700
+ proc.on("error", () => resolve18({ code: 127, out }));
4701
+ proc.on("close", (code) => resolve18({ code: code ?? 1, out }));
4651
4702
  });
4652
4703
  async function ghReady(exec = defaultGhExec) {
4653
4704
  const { code, out } = await exec(["auth", "status"]);
@@ -6143,7 +6194,7 @@ async function carveStatesUnder(dirs) {
6143
6194
  init_read_scheduler();
6144
6195
  init_chant();
6145
6196
  import { statSync as statSync5 } from "node:fs";
6146
- import { join as join12, resolve as resolve9, sep as sep4 } from "node:path";
6197
+ import { join as join13, resolve as resolve10, sep as sep4 } from "node:path";
6147
6198
  import { composeStacks, shortStackNames } from "@intentius/pinhole";
6148
6199
 
6149
6200
  // src/overlay-ir.ts
@@ -6271,6 +6322,241 @@ function tierMismatchNote(ir, tiers, currentTier) {
6271
6322
  return void 0;
6272
6323
  }
6273
6324
 
6325
+ // src/workspace.ts
6326
+ init_chant();
6327
+ init_floor();
6328
+ init_member_source();
6329
+ import { existsSync as existsSync8 } from "node:fs";
6330
+ import { join as join12, resolve as resolve9 } from "node:path";
6331
+ var WORKSPACE_CONTRACT = 1;
6332
+ var WORKSPACE_CHANT_FLOOR = "0.81.0";
6333
+ var DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"];
6334
+ function findDeclaration(dir) {
6335
+ const found = DECLARATION_FILES.filter((f) => existsSync8(join12(dir, f)));
6336
+ if (found.length > 1) return { ambiguous: true };
6337
+ return found.length === 1 ? { file: found[0] } : void 0;
6338
+ }
6339
+ function hasDeclaration(dir) {
6340
+ return findDeclaration(dir) !== void 0;
6341
+ }
6342
+ var refuse4 = (error, code, remedy) => ({ ok: false, refusal: { error, code, remedy } });
6343
+ var isRecord6 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
6344
+ function parseWorkspaceLs(text, root) {
6345
+ let doc;
6346
+ try {
6347
+ doc = JSON.parse(text);
6348
+ } catch {
6349
+ return refuse4(`chant workspace ls printed something that is not JSON for ${root}.`, "workspace-ls", "Run `chant workspace ls --json` in the workspace root to see what it says.");
6350
+ }
6351
+ if (!isRecord6(doc)) return refuse4("chant workspace ls printed JSON that is not an object.", "workspace-ls", "Run `chant workspace ls --json` in the workspace root.");
6352
+ const contract = contractRefusal(doc, "ls");
6353
+ if (contract) return { ok: false, refusal: contract };
6354
+ if (isRecord6(doc.error)) {
6355
+ const code = String(doc.error.code ?? "unknown");
6356
+ return refuse4(`chant could not read the workspace declaration in ${root}: ${code}: ${String(doc.error.message ?? "")}`, code, "`chant workspace check` names the problem and where it is.");
6357
+ }
6358
+ const ws = doc.workspace;
6359
+ if (!isRecord6(ws) || typeof ws.name !== "string" || !Array.isArray(doc.members)) {
6360
+ return refuse4("That is not a workspace ls document: it has no `workspace` and `members`.", "workspace-ls", "Run `chant workspace ls --json` in the workspace root.");
6361
+ }
6362
+ const members = [];
6363
+ for (const m of doc.members) {
6364
+ if (!isRecord6(m) || typeof m.name !== "string" || typeof m.dir !== "string" || typeof m.kind !== "string") {
6365
+ return refuse4("A member in the workspace ls document has no name, dir or kind.", "workspace-ls", "Run `chant workspace ls --json` in the workspace root.");
6366
+ }
6367
+ const reason = isRecord6(m.reason) ? { code: String(m.reason.code), message: String(m.reason.message ?? "") } : null;
6368
+ members.push({
6369
+ name: m.name,
6370
+ dir: m.dir,
6371
+ kind: m.kind,
6372
+ abs: m.dir === "." ? resolve9(root) : resolve9(root, m.dir),
6373
+ reason,
6374
+ because: typeof m.because === "string" ? m.because : null
6375
+ });
6376
+ }
6377
+ return {
6378
+ ok: true,
6379
+ workspace: { name: ws.name, root: resolve9(root), file: typeof ws.file === "string" ? ws.file : "chant.workspace.json", chant: String(doc.chant ?? ""), members }
6380
+ };
6381
+ }
6382
+ function contractRefusal(doc, command) {
6383
+ if (doc.contract === WORKSPACE_CONTRACT) return void 0;
6384
+ const said4 = doc.contract === void 0 ? "no contract version" : `contract ${JSON.stringify(doc.contract)}`;
6385
+ return {
6386
+ error: `chant workspace ${command} wrote a document with ${said4}; behold reads contract ${WORKSPACE_CONTRACT}.`,
6387
+ code: "workspace-contract",
6388
+ remedy: doc.contract === void 0 ? `Upgrade the workspace's chant to ${WORKSPACE_CHANT_FLOOR} or newer.` : "Upgrade behold: this chant writes a newer workspace contract than it knows."
6389
+ };
6390
+ }
6391
+ async function readWorkspace(root) {
6392
+ const dir = resolve9(root);
6393
+ const found = findDeclaration(dir);
6394
+ if (!found) return refuse4(`${dir} has no chant.workspace.json.`, "declaration-missing", "`chant workspace init` proposes one.");
6395
+ if ("ambiguous" in found) {
6396
+ return refuse4(`${dir} has both chant.workspace.json and chant.workspace.jsonc.`, "declaration-ambiguous", "Keep one of the two files.");
6397
+ }
6398
+ const chant = resolveChant(dir);
6399
+ if (chant.version && !meetsFloor(chant.version, WORKSPACE_CHANT_FLOOR)) {
6400
+ return refuse4(
6401
+ `${dir} declares a chant workspace, and the chant behold would read it with is ${chant.version}; workspaces need ${WORKSPACE_CHANT_FLOOR} or newer.`,
6402
+ "workspace-chant-too-old",
6403
+ `Install @intentius/chant ${WORKSPACE_CHANT_FLOOR} or newer in the workspace root.`
6404
+ );
6405
+ }
6406
+ const run3 = await runChantRaw(["workspace", "ls", dir, "--json"], dir);
6407
+ if (!run3.stdout.trim()) {
6408
+ return refuse4(`chant workspace ls failed in ${dir} (exit ${run3.code}): ${run3.stderr.trim().split("\n").slice(-3).join(" ")}`, "workspace-ls", "Run `chant workspace ls` in the workspace root.");
6409
+ }
6410
+ return parseWorkspaceLs(run3.stdout, dir);
6411
+ }
6412
+ var served;
6413
+ var byDir = /* @__PURE__ */ new Map();
6414
+ function setServedWorkspace(ws) {
6415
+ served = ws;
6416
+ byDir.clear();
6417
+ for (const m of ws?.members ?? []) byDir.set(m.abs, m);
6418
+ const root = ws?.members.find((m) => m.dir === ".");
6419
+ setStampExclusions(root && ws ? /* @__PURE__ */ new Map([[root.abs, ws.members.filter((m) => m !== root).map((m) => m.abs)]]) : /* @__PURE__ */ new Map());
6420
+ }
6421
+ function servedWorkspace() {
6422
+ return served;
6423
+ }
6424
+ function workspaceMemberOf(dir) {
6425
+ return byDir.get(resolve9(dir));
6426
+ }
6427
+ var UNDRAWN_KINDS = /* @__PURE__ */ new Set(["other", "workspace"]);
6428
+ function drawnMembers(ws) {
6429
+ return ws.members.filter((m) => m.reason === null && !UNDRAWN_KINDS.has(m.kind));
6430
+ }
6431
+ function unreadableMembers(ws) {
6432
+ return ws.members.filter((m) => m.reason !== null && !UNDRAWN_KINDS.has(m.kind));
6433
+ }
6434
+ function undrawnMembers(ws) {
6435
+ return ws.members.filter((m) => UNDRAWN_KINDS.has(m.kind));
6436
+ }
6437
+ function unreadableMemberIr(m) {
6438
+ return {
6439
+ nodes: [
6440
+ {
6441
+ id: "unreadable",
6442
+ kind: "UnreadableMember",
6443
+ lexicon: "workspace",
6444
+ attrs: {
6445
+ _status: "neutral",
6446
+ _unreadable: { code: m.reason?.code ?? "unknown", message: m.reason?.message ?? "" },
6447
+ dir: m.dir,
6448
+ kind: m.kind
6449
+ }
6450
+ }
6451
+ ],
6452
+ edges: [],
6453
+ groups: {}
6454
+ };
6455
+ }
6456
+ function workspaceGraphTakes(opts) {
6457
+ return opts.detail === void 0 && !opts.lens && !opts.up && !opts.down && !opts.namespace;
6458
+ }
6459
+ var WorkspaceMemberError = class extends Error {
6460
+ constructor(member, reason) {
6461
+ super(`chant workspace graph could not read ${member}: ${reason.code}: ${reason.message}`);
6462
+ this.member = member;
6463
+ this.reason = reason;
6464
+ }
6465
+ member;
6466
+ reason;
6467
+ };
6468
+ function stripRefs(value, prefix) {
6469
+ if (Array.isArray(value)) return value.map((v) => stripRefs(v, prefix));
6470
+ if (!isRecord6(value)) return value;
6471
+ const out = {};
6472
+ for (const [k, v] of Object.entries(value)) {
6473
+ out[k] = k === "$ref" && typeof v === "string" && v.startsWith(prefix) ? v.slice(prefix.length) : stripRefs(v, prefix);
6474
+ }
6475
+ return out;
6476
+ }
6477
+ function memberIrFromWorkspaceGraph(text, member) {
6478
+ let doc;
6479
+ try {
6480
+ doc = JSON.parse(text);
6481
+ } catch {
6482
+ throw new Error("chant workspace graph printed something that is not JSON");
6483
+ }
6484
+ if (!isRecord6(doc)) throw new Error("chant workspace graph printed JSON that is not an object");
6485
+ const contract = contractRefusal(doc, "graph");
6486
+ if (contract) throw new Error(`${contract.error} ${contract.remedy}`);
6487
+ if (isRecord6(doc.error)) throw new Error(`chant workspace graph: ${String(doc.error.code)}: ${String(doc.error.message ?? "")}`);
6488
+ const entry = Array.isArray(doc.members) ? doc.members.find((m) => isRecord6(m) && m.name === member) : void 0;
6489
+ if (!isRecord6(entry)) throw new Error(`chant workspace graph did not list member ${member}`);
6490
+ if (entry.status !== "composed") {
6491
+ const r = isRecord6(entry.reason) ? entry.reason : {};
6492
+ throw new WorkspaceMemberError(member, { code: String(r.code ?? entry.status), message: String(r.message ?? "") });
6493
+ }
6494
+ const p = `${member}/`;
6495
+ const un = (id) => typeof id === "string" && id.startsWith(p) ? id.slice(p.length) : String(id);
6496
+ const mine = (x) => isRecord6(x) && x.member === member;
6497
+ const nodes = (Array.isArray(doc.nodes) ? doc.nodes : []).filter(mine).map((n) => {
6498
+ const { member: _m, ...rest } = n;
6499
+ const out = { ...rest, id: un(n.id), attrs: stripRefs(n.attrs ?? {}, p) };
6500
+ if (typeof n.compositeInstance === "string") out.compositeInstance = un(n.compositeInstance);
6501
+ if (typeof n.runtimeOwner === "string") out.runtimeOwner = un(n.runtimeOwner);
6502
+ return out;
6503
+ });
6504
+ const edges = (Array.isArray(doc.edges) ? doc.edges : []).filter(mine).map((e) => {
6505
+ const { member: _m, ...rest } = e;
6506
+ return { ...rest, from: un(e.from), to: un(e.to) };
6507
+ });
6508
+ const own = new Set(nodes.map((n) => n.id));
6509
+ const groups = {};
6510
+ if (isRecord6(doc.groups)) {
6511
+ for (const [key, table] of Object.entries(doc.groups)) {
6512
+ if (key === "byMember" || !isRecord6(table)) continue;
6513
+ const scoped = key === "byStack" || key === "byContainer" || key === "byWave";
6514
+ for (const [k, ids] of Object.entries(table)) {
6515
+ if (scoped && !k.startsWith(p)) continue;
6516
+ const members = (Array.isArray(ids) ? ids : []).map(un).filter((id) => own.has(id));
6517
+ if (members.length) (groups[key] ??= {})[scoped ? k.slice(p.length) : k] = members;
6518
+ }
6519
+ }
6520
+ }
6521
+ const exports = (Array.isArray(doc.exports) ? doc.exports : []).filter(mine).map((e) => {
6522
+ const { member: _m, ...rest } = e;
6523
+ return typeof e.node === "string" ? { ...rest, node: un(e.node) } : rest;
6524
+ });
6525
+ const imports = (Array.isArray(doc.imports) ? doc.imports : []).filter(mine).map((i) => {
6526
+ const { member: _m, ...rest } = i;
6527
+ return { ...rest, node: un(i.node) };
6528
+ });
6529
+ const ir = { version: 1, nodes, edges, groups, exports, imports };
6530
+ if (isRecord6(entry.meta)) ir.meta = entry.meta;
6531
+ if (entry.pipeline !== void 0) ir.pipeline = entry.pipeline;
6532
+ if (isRecord6(doc.derivedAttrs)) ir.derivedAttrs = doc.derivedAttrs;
6533
+ return ir;
6534
+ }
6535
+ function workspaceGraphArgs(root, member, opts) {
6536
+ const args = ["workspace", "graph", root, "--member", member];
6537
+ if (opts.env) args.push("--env", opts.env);
6538
+ if (opts.live) args.push("--live");
6539
+ if (opts.overlay) args.push("--overlay");
6540
+ if (opts.traffic) args.push("--traffic", opts.traffic);
6541
+ return args;
6542
+ }
6543
+ var CONTRACT_READ_KINDS = /* @__PURE__ */ new Set(["chant", "terraform"]);
6544
+ function workspaceVia(member, root, fallback) {
6545
+ return {
6546
+ tool: (dir) => {
6547
+ const chant = resolveChant(root);
6548
+ return `workspace\0${chant.bin}\0${chant.version ?? ""}\0${fallback.tool(dir)}`;
6549
+ },
6550
+ read: async (dir, opts) => {
6551
+ if (!workspaceGraphTakes(opts)) return fallback.read(dir, opts);
6552
+ const args = workspaceGraphArgs(root, member.name, opts);
6553
+ const run3 = await runChantRaw(args, root, envOverridesFor(opts));
6554
+ if (!run3.stdout.trim()) throw new Error(`chant ${args.join(" ")} exited ${run3.code}: ${run3.stderr.trim()}`);
6555
+ return memberIrFromWorkspaceGraph(run3.stdout, member.name);
6556
+ }
6557
+ };
6558
+ }
6559
+
6274
6560
  // src/estate.ts
6275
6561
  function estateReadPool(members, env = process.env) {
6276
6562
  return Math.max(1, Math.min(readConcurrency(env), members));
@@ -6287,14 +6573,35 @@ async function mapPool(items, width, fn) {
6287
6573
  return results;
6288
6574
  }
6289
6575
  function estateMembers(projectDirs) {
6290
- const names = shortStackNames([...projectDirs]);
6291
- return projectDirs.map((dir, i) => ({ name: names[i], dir, kind: memberKindOf(dir) ?? "chant" }));
6576
+ const names = estateMemberNames(projectDirs);
6577
+ const kindOf2 = (dir) => workspaceMemberOf(dir)?.kind ?? memberKindOf(dir) ?? "chant";
6578
+ return projectDirs.map((dir, i) => ({ name: names[i], dir, kind: kindOf2(dir) }));
6579
+ }
6580
+ function estateMemberNames(projectDirs) {
6581
+ const declared = projectDirs.map((d) => workspaceMemberOf(d)?.name);
6582
+ if (declared.every((n) => n !== void 0)) return declared;
6583
+ return shortStackNames([...projectDirs]);
6584
+ }
6585
+ function unreadableStacks(projectDirs) {
6586
+ const ws = servedWorkspace();
6587
+ if (!ws || !projectDirs.every((d) => workspaceMemberOf(d))) return [];
6588
+ return unreadableMembers(ws).map((m) => ({ name: m.name, ir: unreadableMemberIr(m) }));
6589
+ }
6590
+ var memberViaFor = (dir) => {
6591
+ const own = memberKindSpec(memberKindOf(dir) ?? "chant")?.via ?? chantVia;
6592
+ const declared = workspaceMemberOf(dir);
6593
+ const ws = servedWorkspace();
6594
+ return declared && ws && CONTRACT_READ_KINDS.has(declared.kind) ? workspaceVia(declared, ws.root, own) : own;
6595
+ };
6596
+ function forMember(dir, opts) {
6597
+ if (!opts.traffic || memberTakesTraffic(dir, memberKindOf(dir) ?? "chant")) return opts;
6598
+ const { traffic: _traffic, ...rest } = opts;
6599
+ return rest;
6292
6600
  }
6293
- var memberViaFor = (dir) => memberKindSpec(memberKindOf(dir) ?? "chant")?.via ?? chantVia;
6294
- var memberSource = (dir, opts) => memberIr(dir, opts, memberViaFor(dir));
6295
- var memberLive = (dir, opts, fresh) => overlayIr(dir, opts, memberViaFor(dir), fresh);
6601
+ var memberSource = (dir, opts) => memberIr(dir, forMember(dir, opts), memberViaFor(dir));
6602
+ var memberLive = (dir, opts, fresh) => overlayIr(dir, forMember(dir, opts), memberViaFor(dir), fresh);
6296
6603
  async function composeEstate(projectDirs, opts = {}) {
6297
- const names = shortStackNames(projectDirs);
6604
+ const names = estateMemberNames(projectDirs);
6298
6605
  const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
6299
6606
  name: names[i],
6300
6607
  // chant#2040: the manifests a member holds name the files its carved
@@ -6302,17 +6609,17 @@ async function composeEstate(projectDirs, opts = {}) {
6302
6609
  // known, onto a COPY (the member IR is the #312 cache's own object).
6303
6610
  ir: joinCarvedSources(await memberSource(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
6304
6611
  }));
6305
- return composeStacks(stacks);
6612
+ return composeStacks([...stacks, ...unreadableStacks(projectDirs)]);
6306
6613
  }
6307
6614
  async function composeEstatePending(projectDirs, opts = {}) {
6308
- const names = shortStackNames(projectDirs);
6615
+ const names = estateMemberNames(projectDirs);
6309
6616
  const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
6310
6617
  const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
6311
6618
  const ir = joinCarvedSources(await memberSource(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
6312
6619
  for (const n of ir.nodes) n.attrs = { ...n.attrs, _pendingRead: true };
6313
6620
  return { name: names[i], ir };
6314
6621
  });
6315
- return composeStacks(stacks);
6622
+ return composeStacks([...stacks, ...unreadableStacks(projectDirs)]);
6316
6623
  }
6317
6624
  var firstLine2 = (e) => {
6318
6625
  const msg = e instanceof Error ? e.message : String(e);
@@ -6361,11 +6668,11 @@ function declaredSegments(path) {
6361
6668
  function pathAlignment(declared, dir, isDir = realIsDir) {
6362
6669
  const want = declaredSegments(declared);
6363
6670
  if (!want) return 0;
6364
- const root = resolve9(dir);
6671
+ const root = resolve10(dir);
6365
6672
  const have = root.split(sep4).filter(Boolean);
6366
6673
  for (let k = Math.min(want.length, have.length); k >= 1; k--) {
6367
6674
  if (!have.slice(-k).every((s, i) => s === want[i])) continue;
6368
- if (!isDir(join12(root, ...want.slice(k)))) continue;
6675
+ if (!isDir(join13(root, ...want.slice(k)))) continue;
6369
6676
  return k;
6370
6677
  }
6371
6678
  return 0;
@@ -6395,10 +6702,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
6395
6702
  if (!winner || tied) continue;
6396
6703
  const prev = best.get(winner.dir);
6397
6704
  if (prev === "ambiguous") continue;
6398
- const join28 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
6399
- if (!prev) best.set(winner.dir, { score: winner.score, join: join28 });
6705
+ const join30 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
6706
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join30 });
6400
6707
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
6401
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join28 });
6708
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join30 });
6402
6709
  }
6403
6710
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
6404
6711
  }
@@ -6422,7 +6729,7 @@ function withoutJoinedMembers(nodes, joined) {
6422
6729
  return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
6423
6730
  }
6424
6731
  async function composeEstateOverlay(projectDirs, opts, classify, observe = {}) {
6425
- const names = shortStackNames(projectDirs);
6732
+ const names = estateMemberNames(projectDirs);
6426
6733
  const unobserved = [];
6427
6734
  const dropped = [];
6428
6735
  const joined = [];
@@ -6456,7 +6763,7 @@ async function composeEstateOverlay(projectDirs, opts, classify, observe = {}) {
6456
6763
  });
6457
6764
  const present = stacks.filter((s) => !!s);
6458
6765
  return {
6459
- ir: composeStacks(present),
6766
+ ir: composeStacks([...present, ...unreadableStacks(projectDirs)]),
6460
6767
  observed: present.length - unobserved.length,
6461
6768
  total: projectDirs.length,
6462
6769
  unobserved,
@@ -7123,8 +7430,8 @@ function projectHelmLogical(ir) {
7123
7430
  }
7124
7431
 
7125
7432
  // src/logical-kustomize.ts
7126
- import { existsSync as existsSync8 } from "node:fs";
7127
- import { join as join13 } from "node:path";
7433
+ import { existsSync as existsSync9 } from "node:fs";
7434
+ import { join as join14 } from "node:path";
7128
7435
  function overlayBoxTitle(name) {
7129
7436
  return `overlay ${name}`;
7130
7437
  }
@@ -7144,7 +7451,7 @@ function kustomizationRoot(bases, dir, exists) {
7144
7451
  let current = dir;
7145
7452
  for (let i = 0; i < 4 && current; i++) {
7146
7453
  for (const base of bases) {
7147
- if (exists(join13(base, current, "kustomization.yaml")) || exists(join13(base, current, "kustomization.yml"))) {
7454
+ if (exists(join14(base, current, "kustomization.yaml")) || exists(join14(base, current, "kustomization.yml"))) {
7148
7455
  return current;
7149
7456
  }
7150
7457
  }
@@ -7153,7 +7460,7 @@ function kustomizationRoot(bases, dir, exists) {
7153
7460
  }
7154
7461
  return void 0;
7155
7462
  }
7156
- function projectKustomizeLogical(ir, sourceRoots, exists = existsSync8) {
7463
+ function projectKustomizeLogical(ir, sourceRoots, exists = existsSync9) {
7157
7464
  const bases = Array.isArray(sourceRoots) ? sourceRoots : [sourceRoots];
7158
7465
  const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
7159
7466
  if (k8s.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
@@ -7934,9 +8241,9 @@ import {
7934
8241
 
7935
8242
  // src/icon-packs.ts
7936
8243
  import { readFileSync as readFileSync10 } from "node:fs";
7937
- import { dirname as dirname5, join as join14 } from "node:path";
8244
+ import { dirname as dirname5, join as join15 } from "node:path";
7938
8245
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7939
- var ICON_ROOT = join14(dirname5(fileURLToPath3(import.meta.url)), "..", "web", "icons");
8246
+ var ICON_ROOT = join15(dirname5(fileURLToPath3(import.meta.url)), "..", "web", "icons");
7940
8247
  var cache4 = /* @__PURE__ */ new Map();
7941
8248
  function unprefix(body) {
7942
8249
  return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
@@ -7958,7 +8265,7 @@ function plate(glyph) {
7958
8265
  function loadIcon(rel) {
7959
8266
  const hit = cache4.get(rel);
7960
8267
  if (hit) return hit;
7961
- const raw = readFileSync10(join14(ICON_ROOT, `${rel}.svg`), "utf8");
8268
+ const raw = readFileSync10(join15(ICON_ROOT, `${rel}.svg`), "utf8");
7962
8269
  const open = raw.indexOf("<svg");
7963
8270
  const openEnd = raw.indexOf(">", open);
7964
8271
  const close = raw.lastIndexOf("</svg");
@@ -8119,22 +8426,22 @@ init_carve_manifest();
8119
8426
 
8120
8427
  // src/ops-lens.ts
8121
8428
  var SUPPORTED_MAJOR2 = 1;
8122
- var refuse4 = (error, remedy) => ({ ok: false, refusal: { error, code: "op-ir", remedy } });
8429
+ var refuse5 = (error, remedy) => ({ ok: false, refusal: { error, code: "op-ir", remedy } });
8123
8430
  var HOW_TO_EMIT = "Build the project with a chant >= 0.50 (`chant build --lexicon temporal`), which writes `dist/ops/<name>/op.json` beside each Op's generated workflow.";
8124
- var isRecord6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8431
+ var isRecord7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8125
8432
  function parseOpIr(value) {
8126
- if (!isRecord6(value)) {
8127
- return refuse4("That file isn't an op.json \u2014 its top level isn't a JSON object.", HOW_TO_EMIT);
8433
+ if (!isRecord7(value)) {
8434
+ return refuse5("That file isn't an op.json \u2014 its top level isn't a JSON object.", HOW_TO_EMIT);
8128
8435
  }
8129
8436
  if (typeof value.name !== "string" || !Array.isArray(value.phases)) {
8130
- return refuse4(
8437
+ return refuse5(
8131
8438
  "That JSON doesn't look like an op.json \u2014 it needs a string `name` and a `phases` array.",
8132
8439
  HOW_TO_EMIT
8133
8440
  );
8134
8441
  }
8135
- const bad2 = value.phases.findIndex((p) => !isRecord6(p) || typeof p.name !== "string" || !Array.isArray(p.steps));
8442
+ const bad2 = value.phases.findIndex((p) => !isRecord7(p) || typeof p.name !== "string" || !Array.isArray(p.steps));
8136
8443
  if (bad2 >= 0) {
8137
- return refuse4(
8444
+ return refuse5(
8138
8445
  `That JSON has a \`phases\` array, but entry ${bad2} isn't a phase (needs a string \`name\` and a \`steps\` array).`,
8139
8446
  HOW_TO_EMIT
8140
8447
  );
@@ -8143,7 +8450,7 @@ function parseOpIr(value) {
8143
8450
  if (version !== void 0) {
8144
8451
  const major = Number(String(version).split(".")[0]);
8145
8452
  if (!Number.isFinite(major) || major !== SUPPORTED_MAJOR2) {
8146
- return refuse4(
8453
+ return refuse5(
8147
8454
  `This op.json declares format version ${String(version)}; behold reads version ${SUPPORTED_MAJOR2}.`,
8148
8455
  "Upgrade behold, or render the Op with a behold that speaks its version."
8149
8456
  );
@@ -8156,13 +8463,13 @@ function readOpIr(path, readFile) {
8156
8463
  try {
8157
8464
  text = readFile(path);
8158
8465
  } catch (err) {
8159
- return refuse4(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
8466
+ return refuse5(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
8160
8467
  }
8161
8468
  let json;
8162
8469
  try {
8163
8470
  json = JSON.parse(text);
8164
8471
  } catch (err) {
8165
- return refuse4(`${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
8472
+ return refuse5(`${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
8166
8473
  }
8167
8474
  return parseOpIr(json);
8168
8475
  }
@@ -8229,14 +8536,14 @@ function opPlacedSteps(op) {
8229
8536
  }
8230
8537
  var RULE_BOX = (opName) => `${opName} \xB7 rule table`;
8231
8538
  var ruleId = (opName, rule) => `${opName}/rules/${rule.id}`;
8232
- var isPredicate = (v) => isRecord6(v) && typeof v.kind === "string";
8539
+ var isPredicate = (v) => isRecord7(v) && typeof v.kind === "string";
8233
8540
  function isConvergeRule(v) {
8234
- if (!isRecord6(v)) return false;
8541
+ if (!isRecord7(v)) return false;
8235
8542
  if (typeof v.id !== "string" || !v.id) return false;
8236
8543
  if (typeof v.why !== "string" || !v.why) return false;
8237
8544
  if (!isPredicate(v.when)) return false;
8238
8545
  const then = v.then;
8239
- if (!isRecord6(then)) return false;
8546
+ if (!isRecord7(then)) return false;
8240
8547
  return then.kind === "run" && typeof then.op === "string" || then.kind === "report" && typeof then.reason === "string";
8241
8548
  }
8242
8549
  function convergeTable(op) {
@@ -8577,15 +8884,15 @@ function entityCountClause(ir) {
8577
8884
  var LAYOUT_ROW_VIA = "_layout-row";
8578
8885
  var ROW_ASPECT = 2.4;
8579
8886
  var ROW_MIN_CARDS = 6;
8580
- function rowColumns(n, cell, sep8, aspect = ROW_ASPECT) {
8887
+ function rowColumns(n, cell, sep9, aspect = ROW_ASPECT) {
8581
8888
  if (n < 2) return Math.max(1, n);
8582
- const cols = Math.round(Math.sqrt(n * (cell.h + sep8.y) * aspect / (cell.w + sep8.x)));
8889
+ const cols = Math.round(Math.sqrt(n * (cell.h + sep9.y) * aspect / (cell.w + sep9.x)));
8583
8890
  return Math.max(1, Math.min(n, cols));
8584
8891
  }
8585
8892
  var FALLBACK = { w: 175, h: 104 };
8586
8893
  function withRowChains(ir, groups, opts) {
8587
8894
  const min = opts.min ?? ROW_MIN_CARDS;
8588
- const sep8 = { x: opts.nodesep ?? 64, y: opts.ranksep ?? 72 };
8895
+ const sep9 = { x: opts.nodesep ?? 64, y: opts.ranksep ?? 72 };
8589
8896
  const known = new Set(ir.nodes.map((n) => n.id));
8590
8897
  const boxOf2 = /* @__PURE__ */ new Map();
8591
8898
  for (const [key, ids] of Object.entries(groups)) for (const id of ids) if (known.has(id)) boxOf2.set(id, key);
@@ -8606,7 +8913,7 @@ function withRowChains(ir, groups, opts) {
8606
8913
  },
8607
8914
  { w: 0, h: 0 }
8608
8915
  );
8609
- const cols = rowColumns(ids.length, cell.w ? cell : FALLBACK, sep8, opts.aspect);
8916
+ const cols = rowColumns(ids.length, cell.w ? cell : FALLBACK, sep9, opts.aspect);
8610
8917
  if (cols >= ids.length) continue;
8611
8918
  const bands = bandsOf(ids, opts.bandOf);
8612
8919
  let rows = 0;
@@ -9364,17 +9671,17 @@ function gridComponent(one, sizes) {
9364
9671
  },
9365
9672
  { w: NODE_W, h: NODE_H }
9366
9673
  );
9367
- const sep8 = { x: 48, y: 60 };
9368
- const cols = rowColumns(order.length, cell, sep8);
9674
+ const sep9 = { x: 48, y: 60 };
9675
+ const cols = rowColumns(order.length, cell, sep9);
9369
9676
  const local = /* @__PURE__ */ new Map();
9370
9677
  order.forEach((n, i) => {
9371
9678
  local.set(n.id, {
9372
- x: i % cols * (cell.w + sep8.x) + cell.w / 2,
9373
- y: Math.floor(i / cols) * (cell.h + sep8.y) + cell.h / 2
9679
+ x: i % cols * (cell.w + sep9.x) + cell.w / 2,
9680
+ y: Math.floor(i / cols) * (cell.h + sep9.y) + cell.h / 2
9374
9681
  });
9375
9682
  });
9376
9683
  const rows = Math.ceil(order.length / cols);
9377
- return { local, w: Math.min(cols, order.length) * (cell.w + sep8.x) - sep8.x, h: rows * (cell.h + sep8.y) - sep8.y };
9684
+ return { local, w: Math.min(cols, order.length) * (cell.w + sep9.x) - sep9.x, h: rows * (cell.h + sep9.y) - sep9.y };
9378
9685
  }
9379
9686
  function packBoxComponents(layout, ir, boxes, bandOf) {
9380
9687
  const grids = /* @__PURE__ */ new Map();
@@ -9618,12 +9925,12 @@ init_carve_manifest();
9618
9925
 
9619
9926
  // src/carve-actions.ts
9620
9927
  init_chant();
9621
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
9622
- import { isAbsolute as isAbsolute3, join as join17, relative as relative4, resolve as resolve10, sep as sep5 } from "node:path";
9928
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
9929
+ import { isAbsolute as isAbsolute3, join as join18, relative as relative4, resolve as resolve11, sep as sep5 } from "node:path";
9623
9930
 
9624
9931
  // src/layout.ts
9625
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4, accessSync, constants } from "node:fs";
9626
- import { join as join15 } from "node:path";
9932
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4, accessSync, constants } from "node:fs";
9933
+ import { join as join16 } from "node:path";
9627
9934
  var LAYOUT_DIR = ".behold";
9628
9935
  var LAYOUT_FILE = "layout.json";
9629
9936
  var MAX_BODY_BYTES = 64 * 1024;
@@ -9664,10 +9971,10 @@ function lensFromQuery(params) {
9664
9971
  return [slug2(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug2(`stack-${stack}`) : null].filter(Boolean).join("+");
9665
9972
  }
9666
9973
  function layoutPath(projectDir) {
9667
- return join15(projectDir, LAYOUT_DIR, LAYOUT_FILE);
9974
+ return join16(projectDir, LAYOUT_DIR, LAYOUT_FILE);
9668
9975
  }
9669
9976
  function unwritableReason(projectDir) {
9670
- if (!projectDir || !existsSync9(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
9977
+ if (!projectDir || !existsSync10(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
9671
9978
  try {
9672
9979
  accessSync(projectDir, constants.W_OK);
9673
9980
  } catch {
@@ -9679,7 +9986,7 @@ function readLayoutFile(projectDir) {
9679
9986
  const empty = { version: 1, lenses: {} };
9680
9987
  const file = layoutPath(projectDir);
9681
9988
  try {
9682
- if (!existsSync9(file)) return empty;
9989
+ if (!existsSync10(file)) return empty;
9683
9990
  const raw = readFileSync11(file, "utf8");
9684
9991
  if (raw.length > MAX_FILE_BYTES) return empty;
9685
9992
  const parsed = JSON.parse(raw);
@@ -9720,7 +10027,7 @@ function writeLens(projectDir, lens, deltas) {
9720
10027
  }
9721
10028
  const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
9722
10029
  if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
9723
- mkdirSync4(join15(projectDir, LAYOUT_DIR), { recursive: true });
10030
+ mkdirSync4(join16(projectDir, LAYOUT_DIR), { recursive: true });
9724
10031
  const tmp = `${file}.tmp`;
9725
10032
  writeFileSync4(tmp, body, "utf8");
9726
10033
  renameSync(tmp, file);
@@ -9806,8 +10113,8 @@ function reanchorEdges(svg, moved) {
9806
10113
  // src/carve-live.ts
9807
10114
  init_scratch();
9808
10115
  import { spawn as spawn4 } from "node:child_process";
9809
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
9810
- import { join as join16 } from "node:path";
10116
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
10117
+ import { join as join17 } from "node:path";
9811
10118
  var LIVE_CONTAINER = "behold-carve-floci";
9812
10119
  var LIVE_PORT = 4602;
9813
10120
  var LIVE_IMAGE = "floci/floci:1.5.34";
@@ -9867,9 +10174,9 @@ async function teardownScratchFloci() {
9867
10174
  await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
9868
10175
  }
9869
10176
  async function applyIntoFloci(fromDir, step) {
9870
- const template = join16(fromDir, "floci-override.tf.disabled");
9871
- if (!existsSync10(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
9872
- writeFileSync5(join16(fromDir, "floci_override.tf"), armOverride(readFileSync12(template, "utf8"), LIVE_PORT));
10177
+ const template = join17(fromDir, "floci-override.tf.disabled");
10178
+ if (!existsSync11(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
10179
+ writeFileSync5(join17(fromDir, "floci_override.tf"), armOverride(readFileSync12(template, "utf8"), LIVE_PORT));
9873
10180
  const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
9874
10181
  if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
9875
10182
  const apply = await step(
@@ -9893,7 +10200,7 @@ ${tail}`, exitCode: r.code };
9893
10200
  // src/carve-actions.ts
9894
10201
  async function runCarvePlan(demo) {
9895
10202
  if (!demo?.live) {
9896
- return refuse5(
10203
+ return refuse6(
9897
10204
  "carve-action",
9898
10205
  "terraform plan is the live tier's beat, and this server isn't serving one",
9899
10206
  "run `behold demo carve --live` (needs docker + terraform)"
@@ -9901,7 +10208,7 @@ async function runCarvePlan(demo) {
9901
10208
  }
9902
10209
  const r = await runLivePlan(demo.from);
9903
10210
  if ("error" in r) {
9904
- return refuse5("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
10211
+ return refuse6("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
9905
10212
  }
9906
10213
  return {
9907
10214
  ok: true,
@@ -9913,14 +10220,14 @@ async function runCarvePlan(demo) {
9913
10220
  }
9914
10221
  async function runCarveObserve(demo, select) {
9915
10222
  if (!demo.live) {
9916
- return refuse5(
10223
+ return refuse6(
9917
10224
  "carve-action",
9918
10225
  "the observe beat reads the scratch Floci, and this server isn't serving one",
9919
10226
  "run `behold demo carve --live` (needs docker + terraform)"
9920
10227
  );
9921
10228
  }
9922
- if (!existsSync11(join17(demo.out, "src"))) {
9923
- return refuse5("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
10229
+ if (!existsSync12(join18(demo.out, "src"))) {
10230
+ return refuse6("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
9924
10231
  }
9925
10232
  const args = ["lifecycle", "diff", "prod", "--live", "--json"];
9926
10233
  const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
@@ -9929,7 +10236,7 @@ async function runCarveObserve(demo, select) {
9929
10236
  stderr: err instanceof Error ? err.message : String(err)
9930
10237
  }));
9931
10238
  if (run3.code !== 0) {
9932
- return refuse5(
10239
+ return refuse6(
9933
10240
  "carve-action",
9934
10241
  `chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
9935
10242
  "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
@@ -9939,10 +10246,10 @@ async function runCarveObserve(demo, select) {
9939
10246
  try {
9940
10247
  parsed = JSON.parse(run3.stdout);
9941
10248
  } catch {
9942
- return refuse5("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
10249
+ return refuse6("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
9943
10250
  }
9944
10251
  if (parsed.stacks && !parsed.lexicons) {
9945
- return refuse5(
10252
+ return refuse6(
9946
10253
  "carve-action",
9947
10254
  "chant lifecycle diff answered a stacked shape ({stacks}) \u2014 observe only reads the stack-less {lexicons} shape",
9948
10255
  "point the target at a stack-less carveout, or file a shape-read gap against observe for the stacked case"
@@ -9983,13 +10290,13 @@ async function runCarveObserve(demo, select) {
9983
10290
  var MAX_ARTIFACT_BYTES = 64 * 1024;
9984
10291
  var MAX_ARTIFACTS = 24;
9985
10292
  var MAX_OUTPUT_BYTES = 32 * 1024;
9986
- var refuse5 = (code, error, remedy) => ({
10293
+ var refuse6 = (code, error, remedy) => ({
9987
10294
  ok: false,
9988
10295
  refusal: { error, code, remedy }
9989
10296
  });
9990
10297
  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.";
9991
10298
  function insideDemo(root, p) {
9992
- const rel = relative4(resolve10(root), resolve10(p));
10299
+ const rel = relative4(resolve11(root), resolve11(p));
9993
10300
  return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
9994
10301
  }
9995
10302
  function selectFromReport(report, raw) {
@@ -10000,7 +10307,7 @@ function carveWriteBlock(demo) {
10000
10307
  if (!demo) {
10001
10308
  return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
10002
10309
  }
10003
- if (!existsSync11(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
10310
+ if (!existsSync12(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
10004
10311
  if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
10005
10312
  return unwritableReason(demo.root);
10006
10313
  }
@@ -10017,7 +10324,7 @@ function readArtifacts(demo, dir, filter) {
10017
10324
  for (const name of entries) {
10018
10325
  if (out.length >= MAX_ARTIFACTS) return;
10019
10326
  if (name === "node_modules" || name.startsWith(".")) continue;
10020
- const full = join17(d, name);
10327
+ const full = join18(d, name);
10021
10328
  let st;
10022
10329
  try {
10023
10330
  st = statSync6(full);
@@ -10062,9 +10369,9 @@ function merge(r, root) {
10062
10369
  }
10063
10370
  async function runCarveEmit(demo, select) {
10064
10371
  const block = carveWriteBlock(demo);
10065
- if (block) return refuse5("read-only", block, "Start the walkthrough with `behold demo carve`.");
10372
+ if (block) return refuse6("read-only", block, "Start the walkthrough with `behold demo carve`.");
10066
10373
  mkdirSync5(demo.out, { recursive: true });
10067
- const reportFile = join17(demo.out, `${carveSlug(select)}-boundary.json`);
10374
+ const reportFile = join18(demo.out, `${carveSlug(select)}-boundary.json`);
10068
10375
  const args = [
10069
10376
  "carve",
10070
10377
  "emit",
@@ -10085,10 +10392,10 @@ async function runCarveEmit(demo, select) {
10085
10392
  }));
10086
10393
  const output = merge(run3, demo.root);
10087
10394
  if (run3.code !== 0) {
10088
- return refuse5(
10395
+ return refuse6(
10089
10396
  "carve-action",
10090
10397
  `chant carve emit exited ${run3.code}: ${output || "(no output)"}`,
10091
- "The offline emit needs `@cdktf/hcl2json` in the demo copy and the demo's own chant install \u2014 `npm install` in the copy's `app/` and re-run `behold demo carve`."
10398
+ "The offline emit needs `@cdktn/hcl2json` in the demo copy and the demo's own chant install \u2014 `npm install` in the copy's `app/` and re-run `behold demo carve`."
10092
10399
  );
10093
10400
  }
10094
10401
  let boundary = null;
@@ -10096,7 +10403,7 @@ async function runCarveEmit(demo, select) {
10096
10403
  boundary = JSON.parse(readFileSync13(reportFile, "utf8"));
10097
10404
  } catch {
10098
10405
  }
10099
- const lintPath = relative4(demo.project, join17(demo.out, "src")).split(sep5).join("/");
10406
+ const lintPath = relative4(demo.project, join18(demo.out, "src")).split(sep5).join("/");
10100
10407
  const lintArgs = ["lint", lintPath];
10101
10408
  const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
10102
10409
  code: 127,
@@ -10120,7 +10427,7 @@ async function runCarveEmit(demo, select) {
10120
10427
  }
10121
10428
  async function runCarveBridge(demo, select) {
10122
10429
  const block = carveWriteBlock(demo);
10123
- if (block) return refuse5("read-only", block, "Start the walkthrough with `behold demo carve`.");
10430
+ if (block) return refuse6("read-only", block, "Start the walkthrough with `behold demo carve`.");
10124
10431
  mkdirSync5(demo.out, { recursive: true });
10125
10432
  const args = [
10126
10433
  "carve",
@@ -10140,7 +10447,7 @@ async function runCarveBridge(demo, select) {
10140
10447
  }));
10141
10448
  const output = merge(run3, demo.root);
10142
10449
  if (run3.code !== 0) {
10143
- return refuse5(
10450
+ return refuse6(
10144
10451
  "carve-action",
10145
10452
  `chant carve bridge exited ${run3.code}: ${output || "(no output)"}`,
10146
10453
  "Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
@@ -10159,8 +10466,8 @@ async function runCarveBridge(demo, select) {
10159
10466
  }
10160
10467
 
10161
10468
  // src/ops.ts
10162
- import { readdirSync as readdirSync9, readFileSync as readFileSync14, existsSync as existsSync12 } from "node:fs";
10163
- import { join as join18 } from "node:path";
10469
+ import { readdirSync as readdirSync9, readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
10470
+ import { join as join19 } from "node:path";
10164
10471
  var APPLY_TARGET_LEXICON = {
10165
10472
  cloudformation: "aws",
10166
10473
  kubectl: "k8s",
@@ -10182,11 +10489,11 @@ function discoverOps(projectDir) {
10182
10489
  const seen = /* @__PURE__ */ new Set();
10183
10490
  const out = [];
10184
10491
  for (const sub of ["ops", "src", "."]) {
10185
- const dir = join18(projectDir, sub);
10186
- if (!existsSync12(dir)) continue;
10492
+ const dir = join19(projectDir, sub);
10493
+ if (!existsSync13(dir)) continue;
10187
10494
  for (const f of readdirSync9(dir)) {
10188
10495
  if (!f.endsWith(".op.ts")) continue;
10189
- const content = readFileSync14(join18(dir, f), "utf8");
10496
+ const content = readFileSync14(join19(dir, f), "utf8");
10190
10497
  const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
10191
10498
  if (!name || seen.has(name)) continue;
10192
10499
  seen.add(name);
@@ -10202,25 +10509,25 @@ function discoverOps(projectDir) {
10202
10509
  ...gate ? { gate } : {},
10203
10510
  ...env ? { env } : {},
10204
10511
  ...substrate ? { substrate } : {},
10205
- ...existsSync12(irPath) ? { irPath } : {}
10512
+ ...existsSync13(irPath) ? { irPath } : {}
10206
10513
  });
10207
10514
  }
10208
10515
  }
10209
10516
  return out.sort((a, b) => a.name.localeCompare(b.name));
10210
10517
  }
10211
10518
  function opIrPath(projectDir, name) {
10212
- return join18(projectDir, "dist", "ops", name, "op.json");
10519
+ return join19(projectDir, "dist", "ops", name, "op.json");
10213
10520
  }
10214
10521
  function discoverOpIrs(projectDirs) {
10215
10522
  const seen = /* @__PURE__ */ new Set();
10216
10523
  const out = [];
10217
10524
  for (const dir of projectDirs) {
10218
- const root = join18(dir, "dist", "ops");
10219
- if (!existsSync12(root)) continue;
10525
+ const root = join19(dir, "dist", "ops");
10526
+ if (!existsSync13(root)) continue;
10220
10527
  for (const name of readdirSync9(root)) {
10221
10528
  if (seen.has(name)) continue;
10222
10529
  const path = opIrPath(dir, name);
10223
- if (!existsSync12(path)) continue;
10530
+ if (!existsSync13(path)) continue;
10224
10531
  seen.add(name);
10225
10532
  out.push({ name, dir, path });
10226
10533
  }
@@ -10244,9 +10551,9 @@ function discoverEstateOps(projectDirs) {
10244
10551
  var RECEIPT_READ_FN = "receiptRead";
10245
10552
  var RECEIPT_WRITE_FN = "receiptWrite";
10246
10553
  var STEP_STATUSES = /* @__PURE__ */ new Set(["ok", "fail", "skipped"]);
10247
- var isRecord7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10554
+ var isRecord8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10248
10555
  function asStepRecord(value) {
10249
- if (!isRecord7(value)) return null;
10556
+ if (!isRecord8(value)) return null;
10250
10557
  if (typeof value.phase !== "string" || typeof value.fn !== "string") return null;
10251
10558
  if (typeof value.status !== "string" || !STEP_STATUSES.has(value.status)) return null;
10252
10559
  if (typeof value.durationMs !== "number") return null;
@@ -10261,7 +10568,7 @@ function parseRunProgressLine(line) {
10261
10568
  }
10262
10569
  const one = asStepRecord(parsed);
10263
10570
  if (one) return [one];
10264
- if (isRecord7(parsed) && typeof parsed.op === "string" && Array.isArray(parsed.records)) {
10571
+ if (isRecord8(parsed) && typeof parsed.op === "string" && Array.isArray(parsed.records)) {
10265
10572
  const records = parsed.records.map(asStepRecord).filter((r) => r !== null);
10266
10573
  return records.length ? records : null;
10267
10574
  }
@@ -10455,11 +10762,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
10455
10762
  function tookLabel(ms) {
10456
10763
  return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
10457
10764
  }
10458
- function paintPlayhead(ir, join28, gate) {
10459
- if (!join28.byNode.size && !join28.at) return ir;
10765
+ function paintPlayhead(ir, join30, gate) {
10766
+ if (!join30.byNode.size && !join30.at) return ir;
10460
10767
  const nodes = ir.nodes.map((node) => {
10461
- const run3 = join28.byNode.get(node.id);
10462
- const here = join28.at === node.id;
10768
+ const run3 = join30.byNode.get(node.id);
10769
+ const here = join30.at === node.id;
10463
10770
  if (!run3 && !here) return node;
10464
10771
  const isGate = node.attrs?._step === "gate";
10465
10772
  const attrs = { ...node.attrs };
@@ -10474,7 +10781,7 @@ function paintPlayhead(ir, join28, gate) {
10474
10781
  if (here) {
10475
10782
  attrs._playhead = true;
10476
10783
  attrs._status = isGate && gate ? "warn" : "accent";
10477
- attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join28.atReason ?? "here";
10784
+ attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join30.atReason ?? "here";
10478
10785
  if (isGate && gate?.description) attrs.gateDescription = gate.description;
10479
10786
  }
10480
10787
  return { ...node, attrs };
@@ -10496,11 +10803,11 @@ function pendingGateCard(op, gate, ir) {
10496
10803
  }
10497
10804
  };
10498
10805
  }
10499
- function playheadNote(state, join28) {
10806
+ function playheadNote(state, join30) {
10500
10807
  if (!state.op) {
10501
10808
  return "No run state: nothing has been run from this behold session, so every step paints as declared.";
10502
10809
  }
10503
- const settled = join28 ? join28.byNode.size : state.records.length;
10810
+ const settled = join30 ? join30.byNode.size : state.records.length;
10504
10811
  const bits = [];
10505
10812
  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`;
10506
10813
  if (state.status === "running") {
@@ -10516,9 +10823,9 @@ function playheadNote(state, join28) {
10516
10823
  } else if (state.gateNote) {
10517
10824
  bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
10518
10825
  }
10519
- if (join28?.unplaced.length) {
10826
+ if (join30?.unplaced.length) {
10520
10827
  bits.push(
10521
- `${join28.unplaced.length} record${join28.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join28.unplaced.length === 1 ? "is" : "are"} not painted`
10828
+ `${join30.unplaced.length} record${join30.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join30.unplaced.length === 1 ? "is" : "are"} not painted`
10522
10829
  );
10523
10830
  }
10524
10831
  if (!bits.length) return `${head}.`;
@@ -11190,24 +11497,24 @@ var OpRunner = class {
11190
11497
 
11191
11498
  // src/substrates.ts
11192
11499
  import { spawn as spawn5 } from "node:child_process";
11193
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
11194
- import { join as join19 } from "node:path";
11500
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
11501
+ import { join as join20 } from "node:path";
11195
11502
  import { platform } from "node:os";
11196
11503
  var DOCKER_DOWN_DETAIL = "docker is down";
11197
11504
  function probe(cmd, args) {
11198
- return new Promise((resolve15) => {
11505
+ return new Promise((resolve18) => {
11199
11506
  let out = "";
11200
11507
  let proc;
11201
11508
  try {
11202
11509
  proc = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
11203
11510
  } catch {
11204
- resolve15({ code: 127, out: "" });
11511
+ resolve18({ code: 127, out: "" });
11205
11512
  return;
11206
11513
  }
11207
11514
  proc.stdout.on("data", (d) => out += d);
11208
11515
  proc.stderr.on("data", (d) => out += d);
11209
- proc.on("error", () => resolve15({ code: 127, out }));
11210
- proc.on("close", (code) => resolve15({ code: code ?? 1, out }));
11516
+ proc.on("error", () => resolve18({ code: 127, out }));
11517
+ proc.on("close", (code) => resolve18({ code: code ?? 1, out }));
11211
11518
  });
11212
11519
  }
11213
11520
  async function dockerAvailable(run3) {
@@ -11220,11 +11527,11 @@ async function dockerRunning(run3, nameFilter) {
11220
11527
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
11221
11528
  }
11222
11529
  function scriptBringUp(projectDir, relPath, label) {
11223
- return existsSync13(join19(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
11530
+ return existsSync14(join20(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
11224
11531
  }
11225
11532
  function projectLexicons(projectDir) {
11226
11533
  try {
11227
- const src = readFileSync15(join19(projectDir, "chant.config.ts"), "utf-8");
11534
+ const src = readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8");
11228
11535
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
11229
11536
  if (!m) return [];
11230
11537
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -11276,7 +11583,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
11276
11583
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
11277
11584
  ];
11278
11585
  for (const [name, label, marker, script] of forges) {
11279
- if (!existsSync13(join19(projectDir, marker))) continue;
11586
+ if (!existsSync14(join20(projectDir, marker))) continue;
11280
11587
  const c = docker ? await dockerRunning(run3, name) : [];
11281
11588
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
11282
11589
  subs.push({
@@ -11310,7 +11617,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
11310
11617
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
11311
11618
  });
11312
11619
  }
11313
- if (existsSync13(join19(projectDir, ".github", "workflows"))) {
11620
+ if (existsSync14(join20(projectDir, ".github", "workflows"))) {
11314
11621
  const gh = await run3("gh", ["auth", "status"]);
11315
11622
  const ready = gh.code === 0;
11316
11623
  subs.push({
@@ -11323,7 +11630,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
11323
11630
  if (lexicons.includes("temporal")) {
11324
11631
  let hasProfiles = false;
11325
11632
  try {
11326
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync15(join19(projectDir, "chant.config.ts"), "utf-8"));
11633
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8"));
11327
11634
  } catch {
11328
11635
  }
11329
11636
  subs.push({
@@ -11467,7 +11774,7 @@ function statusVocabulary(kinds) {
11467
11774
 
11468
11775
  // src/behaviour.ts
11469
11776
  import { readFileSync as readFileSync16 } from "node:fs";
11470
- import { join as join20 } from "node:path";
11777
+ import { join as join21 } from "node:path";
11471
11778
  var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
11472
11779
  var str5 = (v) => typeof v === "string" && v.trim().length > 0 ? v : void 0;
11473
11780
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
@@ -11570,6 +11877,7 @@ function validateBehaviourReport(v) {
11570
11877
  if (d.entities !== void 0 && !rec5(d.entities)) return bad("entities is not an object");
11571
11878
  return { ok: true, value: { meta: meta.value, entities } };
11572
11879
  }
11880
+ var FIRST_TOTAL_SHOWN = "two members stated an estate total; the first is shown";
11573
11881
  var behaviourFileName = (env) => `behaviour.${env}.json`;
11574
11882
  var realRead = (path) => {
11575
11883
  try {
@@ -11612,7 +11920,7 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
11612
11920
  const label = m.name ?? "the project";
11613
11921
  const slice = sliceOf(ir.nodes, m.name);
11614
11922
  const inline = slice.filter((n) => n.attrs?.[BEHAVIOUR_ATTR] !== void 0);
11615
- if (inline.length > 0) {
11923
+ if (inline.length > 0 || m.meta !== void 0) {
11616
11924
  sources.set(label, "attrs");
11617
11925
  for (const n of inline) {
11618
11926
  const v = validateBehaviourBlock(n.attrs[BEHAVIOUR_ATTR]);
@@ -11631,7 +11939,7 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
11631
11939
  }
11632
11940
  const file = behaviourFileName(env);
11633
11941
  looked.push(`${label}/${file}`);
11634
- const raw = readFile(join20(m.dir, file));
11942
+ const raw = readFile(join21(m.dir, file));
11635
11943
  if (raw === void 0) {
11636
11944
  sources.set(label, "none");
11637
11945
  continue;
@@ -11698,7 +12006,7 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
11698
12006
  const engineTotal = metas.map((m) => m.meta.total).find((t) => !!t);
11699
12007
  if (engineTotal) {
11700
12008
  meta.total = engineTotal;
11701
- if (metas.filter((m) => m.meta.total).length > 1) diagnostics.push("two members stated an estate total; the first is shown");
12009
+ if (metas.filter((m) => m.meta.total).length > 1) diagnostics.push(FIRST_TOTAL_SHOWN);
11702
12010
  } else {
11703
12011
  const sum = sumOver(ir.nodes, (cs) => diagnostics.push(`no estate sum: the figures are in ${cs.join(" and ")} and behold converts no currency`));
11704
12012
  if (sum) meta.sum = sum;
@@ -11722,6 +12030,239 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
11722
12030
  return meta;
11723
12031
  }
11724
12032
 
12033
+ // src/behaviour-delta.ts
12034
+ init_chant();
12035
+ import { basename as basename2, resolve as resolve12 } from "node:path";
12036
+ init_choudoufu_member();
12037
+ init_terraform_member();
12038
+ var DECLARED_ATTR = "_behaviourDeclared";
12039
+ function trafficFor(query, config) {
12040
+ const asked = query.get("traffic")?.trim();
12041
+ return asked || config.behaviour?.traffic || void 0;
12042
+ }
12043
+ function reportFromIr(ir, strip3 = "") {
12044
+ const meta = ir.meta?._behaviour;
12045
+ const entities = {};
12046
+ for (const n of ir.nodes) {
12047
+ const block = n.attrs?._behaviour;
12048
+ if (block === void 0) continue;
12049
+ entities[strip3 && n.id.startsWith(strip3) ? n.id.slice(strip3.length) : n.id] = block;
12050
+ }
12051
+ if (meta === void 0 && Object.keys(entities).length === 0) return void 0;
12052
+ return { meta, entities };
12053
+ }
12054
+ function choudoufuPredictVia(read, readerState = terraformReaderState) {
12055
+ return {
12056
+ tool: () => `${TERRAFORM_LEXICON_PKG}\0predict\0${readerState().lexicon.version ?? "absent"}`,
12057
+ read: async (dir, opts) => {
12058
+ const state = readerState();
12059
+ if (state.refusal) throw new TerraformReadError(state.refusal, dir);
12060
+ const own = [{ name: basename2(dir), dir: "." }];
12061
+ const project = writeTerraformScratchProject(dir, { roots: own, skipped: [] }, void 0, "chdf");
12062
+ const spawnEnv = choudoufuSpawnOverride();
12063
+ const ir = await read(project, {
12064
+ traffic: opts.traffic,
12065
+ ...opts.live ? { live: true, env: opts.env } : {},
12066
+ ...spawnEnv ? { spawnEnv } : {}
12067
+ });
12068
+ return Object.assign(ir, { behaviourRoot: own[0].name });
12069
+ }
12070
+ };
12071
+ }
12072
+ var rootPrefix = (ir) => {
12073
+ const root = ir.behaviourRoot;
12074
+ return root ? `${root}/` : "";
12075
+ };
12076
+ var said2 = (err) => (err instanceof Error ? err.message : String(err)).split("\n")[0];
12077
+ async function predictMember(member, env, traffic, deps, fresh = false) {
12078
+ const dir = resolve12(member.dir);
12079
+ if (member.kind === "terraform") {
12080
+ try {
12081
+ const declared = reportFromIr(await memberIr(dir, { traffic }, terraformVia));
12082
+ return declared ? { declared } : {};
12083
+ } catch (err) {
12084
+ return { note: `declared prediction unavailable: ${said2(err)}` };
12085
+ }
12086
+ }
12087
+ if (member.kind === "choudoufu") {
12088
+ const via = choudoufuPredictVia(deps.read, deps.readerState);
12089
+ const notes = [];
12090
+ const side = async (from) => {
12091
+ try {
12092
+ const ir = from === "live" ? await overlayIr(dir, { traffic, live: true, env }, via, fresh) : await memberIr(dir, { traffic }, via);
12093
+ return reportFromIr(ir, rootPrefix(ir));
12094
+ } catch (err) {
12095
+ notes.push(`${from} prediction unavailable: ${said2(err)}`);
12096
+ return void 0;
12097
+ }
12098
+ };
12099
+ const [live2, declared] = await Promise.all([env === void 0 ? void 0 : side("live"), side("declared")]);
12100
+ return { ...live2 ? { live: live2 } : {}, ...declared ? { declared } : {}, ...notes.length ? { note: notes.join("; ") } : {} };
12101
+ }
12102
+ if (member.kind === "chant" && memberTakesTraffic(dir, "chant")) {
12103
+ try {
12104
+ const declared = reportFromIr(await memberIr(dir, { traffic }));
12105
+ return declared ? { declared } : {};
12106
+ } catch (err) {
12107
+ return { note: `declared prediction unavailable: ${said2(err)}` };
12108
+ }
12109
+ }
12110
+ if (member.kind === "chant") {
12111
+ return { note: `this member's chant predates \`graph --traffic\` (${TRAFFIC_FLOOR}), so nothing was predicted for it` };
12112
+ }
12113
+ return {};
12114
+ }
12115
+ var idFor = (name, address) => name === void 0 ? address : `${name}/${address}`;
12116
+ function paintLive(ir, name, report, diagnostics) {
12117
+ const byId = new Map(ir.nodes.map((n) => [n.id, n]));
12118
+ for (const [address, block] of Object.entries(report.entities)) {
12119
+ const node = byId.get(idFor(name, address));
12120
+ if (!node) {
12121
+ diagnostics.push(`${name ?? "the project"}: the live prediction prices ${address}, which this overlay has no entity for`);
12122
+ continue;
12123
+ }
12124
+ node.attrs = { ...node.attrs, _behaviour: block };
12125
+ }
12126
+ return report.meta;
12127
+ }
12128
+ var round62 = (n) => Math.round(n * 1e6) / 1e6;
12129
+ function figureOf(meta, blocks, onMixed) {
12130
+ if (meta?.total) return { perHour: meta.total.perHour, currency: meta.total.currency, basis: "total" };
12131
+ if (!blocks.length) return void 0;
12132
+ const currencies = [...new Set(blocks.map((b) => b.cost.currency))].sort();
12133
+ if (currencies.length > 1) {
12134
+ onMixed(currencies);
12135
+ return void 0;
12136
+ }
12137
+ return { perHour: round62(blocks.reduce((sum, b) => sum + b.cost.perHour, 0)), currency: currencies[0], basis: "sum" };
12138
+ }
12139
+ function deltaOf(live2, declared, label, diagnostics) {
12140
+ if (!live2 || !declared) return void 0;
12141
+ if (live2.currency !== declared.currency) {
12142
+ diagnostics.push(`no delta for ${label}: the live figure is in ${live2.currency} and the declared one in ${declared.currency}`);
12143
+ return void 0;
12144
+ }
12145
+ if (live2.basis !== declared.basis) {
12146
+ diagnostics.push(`no delta for ${label}: one side is the engine's own total and the other is behold's sum, and those are not the same kind of figure`);
12147
+ return void 0;
12148
+ }
12149
+ return { perHour: round62(live2.perHour - declared.perHour), currency: live2.currency };
12150
+ }
12151
+ function attachBehaviourDelta(ir, members, behaviour) {
12152
+ const diagnostics = [...behaviour.diagnostics ?? []];
12153
+ const perMember = {};
12154
+ const byId = new Map(ir.nodes.map((n) => [n.id, n]));
12155
+ for (const m of members) {
12156
+ const label = m.name ?? "the project";
12157
+ if (m.prediction.note) diagnostics.push(`${label}: ${m.prediction.note}`);
12158
+ const slice = m.name === void 0 ? ir.nodes : ir.nodes.filter((n) => n.id.startsWith(`${m.name}/`));
12159
+ const liveMeta = m.liveMeta === void 0 ? void 0 : validateBehaviourMeta(m.liveMeta);
12160
+ const liveBlocks = slice.map((n) => n.attrs?._behaviour).filter((b) => !!b);
12161
+ const live3 = behaviour.refusal ? void 0 : figureOf(liveMeta?.ok ? liveMeta.value : void 0, liveBlocks, (cs) => diagnostics.push(`no live figure for ${label}: the figures are in ${cs.join(" and ")}`));
12162
+ let declared2;
12163
+ const report = m.prediction.declared;
12164
+ if (report) {
12165
+ const meta = validateBehaviourMeta(report.meta);
12166
+ if (!meta.ok) diagnostics.push(`${label}: dropped the declared prediction's meta \u2014 ${meta.reason}`);
12167
+ else if (meta.value.refusal) diagnostics.push(`${label}: the declared prediction was refused \u2014 ${meta.value.refusal.reason}`);
12168
+ else {
12169
+ const blocks = [];
12170
+ for (const [address, raw] of Object.entries(report.entities)) {
12171
+ const v = validateBehaviourBlock(raw);
12172
+ if (!v.ok) {
12173
+ diagnostics.push(`dropped the declared behaviour block on ${idFor(m.name, address)}: ${v.reason}`);
12174
+ continue;
12175
+ }
12176
+ blocks.push(v.value);
12177
+ const node = byId.get(idFor(m.name, address));
12178
+ if (node) node.attrs = { ...node.attrs, [DECLARED_ATTR]: v.value };
12179
+ }
12180
+ declared2 = figureOf(meta.value, blocks, (cs) => diagnostics.push(`no declared figure for ${label}: the figures are in ${cs.join(" and ")}`));
12181
+ }
12182
+ }
12183
+ const delta2 = deltaOf(live3, declared2, label, diagnostics);
12184
+ if (live3 || declared2) perMember[label] = { ...live3 ? { live: live3 } : {}, ...declared2 ? { declared: declared2 } : {}, ...delta2 ? { delta: delta2 } : {} };
12185
+ }
12186
+ const rows = Object.values(perMember);
12187
+ const addUp = (side) => {
12188
+ const figures = rows.map((r) => r[side]).filter((f) => !!f);
12189
+ if (!figures.length || figures.length !== rows.length) return void 0;
12190
+ if (new Set(figures.map((f) => f.currency)).size > 1) return void 0;
12191
+ const basis = figures.every((f) => f.basis === "total") && figures.length === 1 ? "total" : "sum";
12192
+ return { perHour: round62(figures.reduce((sum, f) => sum + f.perHour, 0)), currency: figures[0].currency, basis };
12193
+ };
12194
+ const live2 = addUp("live");
12195
+ const declared = addUp("declared");
12196
+ const allDeltas = rows.length > 0 && rows.every((r) => r.delta);
12197
+ const delta = live2 && declared && allDeltas ? { perHour: round62(live2.perHour - declared.perHour), currency: live2.currency } : void 0;
12198
+ const out = { ...behaviour };
12199
+ const stated = rows.filter((r) => r.live?.basis === "total").length;
12200
+ if (live2 && stated > 1) {
12201
+ delete out.total;
12202
+ const at = diagnostics.indexOf(FIRST_TOTAL_SHOWN);
12203
+ if (at >= 0) diagnostics.splice(at, 1);
12204
+ }
12205
+ if (live2) out.live = live2;
12206
+ if (declared) out.declared = declared;
12207
+ if (delta) out.delta = delta;
12208
+ if (rows.length && members.some((m) => m.name !== void 0)) out.members = perMember;
12209
+ if (diagnostics.length) out.diagnostics = diagnostics;
12210
+ else delete out.diagnostics;
12211
+ return out;
12212
+ }
12213
+ function attachDeclaredBehaviour(ir, members) {
12214
+ const diagnostics = [];
12215
+ const perMember = {};
12216
+ const byId = new Map(ir.nodes.map((n) => [n.id, n]));
12217
+ let first;
12218
+ for (const m of members) {
12219
+ const label = m.name ?? "the project";
12220
+ if (m.prediction.note) diagnostics.push(`${label}: ${m.prediction.note}`);
12221
+ const report = m.prediction.declared;
12222
+ if (!report) continue;
12223
+ const meta = validateBehaviourMeta(report.meta);
12224
+ if (!meta.ok) {
12225
+ diagnostics.push(`${label}: dropped the declared prediction's meta \u2014 ${meta.reason}`);
12226
+ continue;
12227
+ }
12228
+ if (meta.value.refusal) {
12229
+ for (const n of ir.nodes) if (n.attrs?._behaviour !== void 0) delete n.attrs._behaviour;
12230
+ return { refusal: meta.value.refusal };
12231
+ }
12232
+ first ??= meta.value;
12233
+ const blocks = [];
12234
+ for (const [address, raw] of Object.entries(report.entities)) {
12235
+ const v = validateBehaviourBlock(raw);
12236
+ if (!v.ok) {
12237
+ diagnostics.push(`dropped the behaviour block on ${idFor(m.name, address)}: ${v.reason}`);
12238
+ continue;
12239
+ }
12240
+ blocks.push(v.value);
12241
+ const node = byId.get(idFor(m.name, address));
12242
+ if (node) node.attrs = { ...node.attrs, _behaviour: v.value };
12243
+ }
12244
+ const declared2 = figureOf(meta.value, blocks, (cs) => diagnostics.push(`no declared figure for ${label}: the figures are in ${cs.join(" and ")}`));
12245
+ if (declared2) perMember[label] = { declared: declared2 };
12246
+ }
12247
+ if (!first) {
12248
+ return {
12249
+ absent: "no behaviour block: a traffic level was given and no member's declared prediction came back",
12250
+ ...diagnostics.length ? { diagnostics } : {}
12251
+ };
12252
+ }
12253
+ const figures = Object.values(perMember).map((r) => r.declared);
12254
+ const oneCurrency = new Set(figures.map((f) => f.currency)).size === 1;
12255
+ const declared = figures.length && oneCurrency ? { perHour: round62(figures.reduce((sum, f) => sum + f.perHour, 0)), currency: figures[0].currency, basis: figures.length === 1 ? figures[0].basis : "sum" } : void 0;
12256
+ return {
12257
+ ...first.engine ? { engine: first.engine } : {},
12258
+ ...first.version ? { version: first.version } : {},
12259
+ ...first.at ? { at: first.at } : {},
12260
+ ...declared ? { declared } : {},
12261
+ ...members.some((m) => m.name !== void 0) && figures.length ? { members: perMember } : {},
12262
+ ...diagnostics.length ? { diagnostics } : {}
12263
+ };
12264
+ }
12265
+
11725
12266
  // src/estate-edges.ts
11726
12267
  import { statSync as statSync7 } from "node:fs";
11727
12268
  var rec6 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
@@ -11834,29 +12375,29 @@ init_choudoufu_live();
11834
12375
  init_choudoufu_member();
11835
12376
  import { mkdtempSync, rmSync as rmSync3 } from "node:fs";
11836
12377
  import { tmpdir as tmpdir2 } from "node:os";
11837
- import { join as join21 } from "node:path";
11838
- var isRecord8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11839
- var refuse6 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
12378
+ import { join as join22 } from "node:path";
12379
+ var isRecord9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12380
+ var refuse7 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
11840
12381
  var CREDS2 = "Give behold the ambient read-only AWS credentials the estate's `plan` needs (AWS_PROFILE, or AWS_ENDPOINT_URL for an emulator), then re-check live with plan.";
11841
- function said2(run3, fallback) {
12382
+ function said3(run3, fallback) {
11842
12383
  const lines = stripAnsiLines(run3.stderr);
11843
12384
  return lines.find((l) => /^Error:/.test(l)) ?? lines[0] ?? fallback;
11844
12385
  }
11845
12386
  function parsePlanShow(run3, dir) {
11846
- if (run3.code === 127) return refuse6("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
12387
+ if (run3.code === 127) return refuse7("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
11847
12388
  const text = run3.stdout.trim();
11848
- if (!text) return refuse6(`choudoufu show could not read the plan for ${dir}: ${said2(run3, `exit ${run3.code}`)}`, CREDS2);
12389
+ if (!text) return refuse7(`choudoufu show could not read the plan for ${dir}: ${said3(run3, `exit ${run3.code}`)}`, CREDS2);
11849
12390
  let json;
11850
12391
  try {
11851
12392
  json = JSON.parse(text);
11852
12393
  } catch {
11853
- return refuse6(`choudoufu show printed something that is not JSON for ${dir}: ${said2(run3, "unparseable stdout")}`, CREDS2);
12394
+ return refuse7(`choudoufu show printed something that is not JSON for ${dir}: ${said3(run3, "unparseable stdout")}`, CREDS2);
11854
12395
  }
11855
- if (!isRecord8(json) || typeof json.format_version !== "string") {
11856
- return refuse6("That is not a plan document \u2014 no `format_version`.", CREDS2);
12396
+ if (!isRecord9(json) || typeof json.format_version !== "string") {
12397
+ return refuse7("That is not a plan document \u2014 no `format_version`.", CREDS2);
11857
12398
  }
11858
12399
  if (json.resource_changes !== void 0 && json.resource_changes !== null && !Array.isArray(json.resource_changes)) {
11859
- return refuse6("That plan document's `resource_changes` is not a list.", CREDS2);
12400
+ return refuse7("That plan document's `resource_changes` is not a list.", CREDS2);
11860
12401
  }
11861
12402
  return { ok: true, doc: json };
11862
12403
  }
@@ -11864,9 +12405,9 @@ var KNOWN_AFTER_APPLY = "(known after apply)";
11864
12405
  var SENSITIVE = "(sensitive)";
11865
12406
  var flagged = (tree, key) => {
11866
12407
  if (tree === true) return true;
11867
- if (!isRecord8(tree)) return false;
12408
+ if (!isRecord9(tree)) return false;
11868
12409
  const v = tree[key];
11869
- return v === true || isRecord8(v) && Object.values(v).some((x) => x === true) || Array.isArray(v) && v.some((x) => x === true);
12410
+ return v === true || isRecord9(v) && Object.values(v).some((x) => x === true) || Array.isArray(v) && v.some((x) => x === true);
11870
12411
  };
11871
12412
  function same(a, b) {
11872
12413
  if (a === b) return true;
@@ -11874,7 +12415,7 @@ function same(a, b) {
11874
12415
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
11875
12416
  return a.every((x, i) => same(x, b[i]));
11876
12417
  }
11877
- if (isRecord8(a) && isRecord8(b)) {
12418
+ if (isRecord9(a) && isRecord9(b)) {
11878
12419
  const ka = Object.keys(a);
11879
12420
  const kb = Object.keys(b);
11880
12421
  if (ka.length !== kb.length) return false;
@@ -11885,10 +12426,10 @@ function same(a, b) {
11885
12426
  function planDrift(doc) {
11886
12427
  const out = /* @__PURE__ */ new Map();
11887
12428
  for (const rc of doc.resource_changes ?? []) {
11888
- if (!rc || typeof rc.address !== "string" || !isRecord8(rc.change)) continue;
12429
+ if (!rc || typeof rc.address !== "string" || !isRecord9(rc.change)) continue;
11889
12430
  const { actions, before, after } = rc.change;
11890
12431
  if (!Array.isArray(actions) || actions.every((a) => a === "no-op")) continue;
11891
- if (!isRecord8(before) || !isRecord8(after)) continue;
12432
+ if (!isRecord9(before) || !isRecord9(after)) continue;
11892
12433
  const changes = [];
11893
12434
  for (const key of [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort()) {
11894
12435
  const unknownAfter = flagged(rc.change.after_unknown, key);
@@ -11908,14 +12449,14 @@ var planArgv = (out) => ({
11908
12449
  show: ["show", "-json", out]
11909
12450
  });
11910
12451
  async function readChoudoufuPlan(dir, run3 = captureChoudoufu) {
11911
- const scratch = mkdtempSync(join21(tmpdir2(), "behold-choudoufu-plan-"));
11912
- const file = join21(scratch, "plan.tfplan");
12452
+ const scratch = mkdtempSync(join22(tmpdir2(), "behold-choudoufu-plan-"));
12453
+ const file = join22(scratch, "plan.tfplan");
11913
12454
  const argv = planArgv(file);
11914
12455
  try {
11915
12456
  const planRun = await run3(argv.plan, dir);
11916
12457
  if (planRun.code === 127) throw new ChoudoufuReadError({ error: "choudoufu is not on PATH.", code: "choudoufu-live-check", remedy: "Install choudoufu and put it on PATH." }, dir);
11917
12458
  if (planRun.code !== 0) {
11918
- throw new ChoudoufuReadError({ error: `choudoufu plan could not plan ${dir}: ${said2(planRun, `exit ${planRun.code}`)}`, code: "choudoufu-live-check", remedy: CREDS2 }, dir);
12459
+ throw new ChoudoufuReadError({ error: `choudoufu plan could not plan ${dir}: ${said3(planRun, `exit ${planRun.code}`)}`, code: "choudoufu-live-check", remedy: CREDS2 }, dir);
11919
12460
  }
11920
12461
  const parsed = parsePlanShow(await run3(argv.show, dir), dir);
11921
12462
  if (!parsed.ok) throw new ChoudoufuReadError(parsed.refusal, dir);
@@ -11941,16 +12482,16 @@ init_choudoufu_refs();
11941
12482
  // src/choudoufu-moves.ts
11942
12483
  init_choudoufu_member();
11943
12484
  init_choudoufu_live();
11944
- import { existsSync as existsSync14, readFileSync as readFileSync17 } from "node:fs";
11945
- import { join as join22, relative as relative5 } from "node:path";
11946
- var isRecord9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11947
- var refuse7 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
12485
+ import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
12486
+ import { join as join23, relative as relative5 } from "node:path";
12487
+ var isRecord10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12488
+ var refuse8 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
11948
12489
  var HOW_TO_GET_ONE2 = "A plan is the live-mv workbench's carve.json: `{from, estates[], moves[{address, from, to}]}` \u2014 the workbench's plan phase writes one, or write it by hand.";
11949
12490
  function parseCarvePlan(value) {
11950
- if (!isRecord9(value)) return refuse7("That file is not a move plan \u2014 its top level is not a JSON object.", HOW_TO_GET_ONE2);
11951
- if (!Array.isArray(value.moves)) return refuse7("That JSON is not a move plan \u2014 it has no `moves` array.", HOW_TO_GET_ONE2);
11952
- const bad2 = value.moves.findIndex((m) => !isRecord9(m) || typeof m.address !== "string" || typeof m.from !== "string" || typeof m.to !== "string");
11953
- if (bad2 >= 0) return refuse7(`moves[${bad2}] is not a move (needs string \`address\`, \`from\` and \`to\`).`, HOW_TO_GET_ONE2);
12491
+ if (!isRecord10(value)) return refuse8("That file is not a move plan \u2014 its top level is not a JSON object.", HOW_TO_GET_ONE2);
12492
+ if (!Array.isArray(value.moves)) return refuse8("That JSON is not a move plan \u2014 it has no `moves` array.", HOW_TO_GET_ONE2);
12493
+ const bad2 = value.moves.findIndex((m) => !isRecord10(m) || typeof m.address !== "string" || typeof m.from !== "string" || typeof m.to !== "string");
12494
+ if (bad2 >= 0) return refuse8(`moves[${bad2}] is not a move (needs string \`address\`, \`from\` and \`to\`).`, HOW_TO_GET_ONE2);
11954
12495
  const moves = value.moves;
11955
12496
  return {
11956
12497
  ok: true,
@@ -11967,34 +12508,34 @@ function readCarvePlan(path, readFile = (p) => readFileSync17(p, "utf8")) {
11967
12508
  try {
11968
12509
  text = readFile(path);
11969
12510
  } catch (err) {
11970
- return refuse7(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_GET_ONE2);
12511
+ return refuse8(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_GET_ONE2);
11971
12512
  }
11972
12513
  try {
11973
12514
  return parseCarvePlan(JSON.parse(text));
11974
12515
  } catch (err) {
11975
- return refuse7(`${path} is not JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_GET_ONE2);
12516
+ return refuse8(`${path} is not JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_GET_ONE2);
11976
12517
  }
11977
12518
  }
11978
12519
  function discoverCarvePlans(memberDirs, base) {
11979
12520
  const out = [];
11980
12521
  for (const dir of memberDirs) {
11981
- const p = join22(dir, "carve.json");
11982
- if (existsSync14(p)) out.push(relative5(base, p));
12522
+ const p = join23(dir, "carve.json");
12523
+ if (existsSync15(p)) out.push(relative5(base, p));
11983
12524
  }
11984
12525
  return out;
11985
12526
  }
11986
12527
  function parseLiveMv(run3, what) {
11987
- if (run3.code === 127) return refuse7("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
12528
+ if (run3.code === 127) return refuse8("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
11988
12529
  const text = run3.stdout.trim();
11989
- if (!text) return refuse7(`choudoufu live-mv printed no document for ${what} (exit ${run3.code}).`, "Run the line by hand to see choudoufu's own words.");
12530
+ if (!text) return refuse8(`choudoufu live-mv printed no document for ${what} (exit ${run3.code}).`, "Run the line by hand to see choudoufu's own words.");
11990
12531
  let json;
11991
12532
  try {
11992
12533
  json = JSON.parse(text);
11993
12534
  } catch {
11994
- return refuse7(`choudoufu live-mv printed something that is not JSON for ${what}.`, "Run the line by hand to see choudoufu's own words.");
12535
+ return refuse8(`choudoufu live-mv printed something that is not JSON for ${what}.`, "Run the line by hand to see choudoufu's own words.");
11995
12536
  }
11996
- if (!isRecord9(json) || !isRecord9(json.from) || !isRecord9(json.to) || typeof json.dry_run !== "boolean") {
11997
- return refuse7(`That is not a live-mv document (${what}).`, "Upgrade choudoufu to 0.16.0 or newer.");
12537
+ if (!isRecord10(json) || !isRecord10(json.from) || !isRecord10(json.to) || typeof json.dry_run !== "boolean") {
12538
+ return refuse8(`That is not a live-mv document (${what}).`, "Upgrade choudoufu to 0.16.0 or newer.");
11998
12539
  }
11999
12540
  return { ok: true, doc: json };
12000
12541
  }
@@ -12079,8 +12620,8 @@ init_chant();
12079
12620
  init_member_kind();
12080
12621
  init_choudoufu_member();
12081
12622
  init_project();
12623
+ import { relative as relative6, resolve as resolve13 } from "node:path";
12082
12624
  init_terraform_member();
12083
- import { relative as relative6, resolve as resolve11 } from "node:path";
12084
12625
  init_targets();
12085
12626
  var list2 = (xs) => xs.join(", ");
12086
12627
  var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
@@ -12262,11 +12803,11 @@ async function choudoufuCheck(root, members, probe2) {
12262
12803
  };
12263
12804
  }
12264
12805
  if (!choudoufuMeetsFloor(v)) {
12265
- const said3 = v.forkField ? `choudoufu ${v.version}` : `a choudoufu whose \`version -json\` carries no \`choudoufu_version\` (v0.15.0 or older)`;
12806
+ const said4 = v.forkField ? `choudoufu ${v.version}` : `a choudoufu whose \`version -json\` carries no \`choudoufu_version\` (v0.15.0 or older)`;
12266
12807
  return {
12267
12808
  name: "choudoufu",
12268
12809
  status: "fail",
12269
- detail: `${said3} at ${v.bin} \u2014 below behold's floor ${CHOUDOUFU_FLOOR}, whose \`-json\` documents carry the fields behold reads (schemas, identity, choudoufu_version)`,
12810
+ detail: `${said4} at ${v.bin} \u2014 below behold's floor ${CHOUDOUFU_FLOOR}, whose \`-json\` documents carry the fields behold reads (schemas, identity, choudoufu_version)`,
12270
12811
  fix: `Upgrade choudoufu to ${CHOUDOUFU_FLOOR} or newer (a build from main after 2026-09-08 also carries them).`
12271
12812
  };
12272
12813
  }
@@ -12308,10 +12849,41 @@ function terraformCheck(members, probe2) {
12308
12849
  const reader = `${TERRAFORM_LEXICON_PKG} ${state.lexicon.version || "dev"} (${HCL_PARSER_PKG} ${state.parser.version || "dev"})`;
12309
12850
  return { name: "terraform", status: "pass", detail: `${reader}; ${perMember}` };
12310
12851
  }
12852
+ function workspaceShape(ws) {
12853
+ return { kind: "estate", members: drawnMembers(ws).map((m) => ({ dir: m.dir, kind: m.kind })) };
12854
+ }
12855
+ function workspaceCheck(ws, beholdMembers) {
12856
+ const drawn = drawnMembers(ws);
12857
+ const unreadable = unreadableMembers(ws);
12858
+ const undrawn = undrawnMembers(ws);
12859
+ const parts = [
12860
+ `chant workspace ${ws.name} (${ws.file}, listed by chant ${ws.chant}): ${drawn.length} of ${ws.members.length} members drawn: ${list2(drawn.map((m) => `${m.name} (${m.kind})`))}`,
12861
+ ...unreadable.length ? [`unreadable: ${list2(unreadable.map((m) => `${m.name} ${m.reason.code}: ${m.reason.message}`))}`] : [],
12862
+ ...undrawn.length ? [`listed, not drawn: ${list2(undrawn.map((m) => `${m.name} (${m.kind})`))}`] : [],
12863
+ ...beholdMembers ? [".behold.json also lists members, which are ignored beside the declaration"] : []
12864
+ ];
12865
+ const warn = unreadable.length > 0 || beholdMembers;
12866
+ const fix = [
12867
+ ...unreadable.length ? ["`chant workspace check` says what each unreadable member needs."] : [],
12868
+ ...beholdMembers ? ["Delete `members` from .behold.json; chant.workspace.json lists them now."] : []
12869
+ ].join(" ");
12870
+ return { name: "project", status: warn ? "warn" : "pass", detail: parts.join("; "), ...warn ? { fix } : {} };
12871
+ }
12311
12872
  async function diagnose(dir, probes = {}) {
12312
- const root = resolve11(dir);
12313
- const shape = detectProjectShape(root);
12873
+ const root = resolve13(dir);
12314
12874
  const behold = beholdVersion();
12875
+ const listing = hasDeclaration(root) ? await (probes.readWorkspace ?? readWorkspace)(root) : void 0;
12876
+ if (listing && !listing.ok) {
12877
+ return {
12878
+ behold,
12879
+ dir: root,
12880
+ kind: "estate",
12881
+ ok: false,
12882
+ checks: [{ name: "project", status: "fail", detail: listing.refusal.error, fix: listing.refusal.remedy }]
12883
+ };
12884
+ }
12885
+ const workspace = listing?.ok ? listing.workspace : void 0;
12886
+ const shape = workspace ? workspaceShape(workspace) : detectProjectShape(root);
12315
12887
  const invalid = shape.invalidMembers ?? [];
12316
12888
  const invalidDetail = invalid.map((m) => `${m.dir} \u2014 ${m.invalid}`);
12317
12889
  const kindsFix = `Member kinds this behold reads: ${registeredMemberKinds().join(", ")}. Fix or remove the entry in .behold.json \`members\`.`;
@@ -12337,14 +12909,14 @@ async function diagnose(dir, probes = {}) {
12337
12909
  };
12338
12910
  }
12339
12911
  const estate = shape.kind === "estate";
12340
- const members = (shape.members ?? []).map((m) => ({ ...m, abs: resolve11(root, m.dir) }));
12912
+ const members = (shape.members ?? []).map((m) => ({ ...m, abs: resolve13(root, m.dir) }));
12341
12913
  const targets = estate ? members.map((m) => m.abs) : [root];
12342
12914
  const primary = targets[0];
12343
12915
  const chantTargets = estate ? members.filter((m) => m.kind === "chant").map((m) => m.abs) : [root];
12344
12916
  const memberList = list2(members.map((m) => `${m.dir} (${m.kind})`));
12345
12917
  const membersFrom = shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces";
12346
12918
  const estateDetail = shape.membersFrom === "itself" ? `a ${members[0].kind} member \u2014 the directory itself, no member list` : `estate of ${members.length} members (${membersFrom}): ${memberList}`;
12347
- const projectCheck = estate ? invalid.length ? {
12919
+ const projectCheck = workspace ? workspaceCheck(workspace, !!loadBeholdConfig(root).members) : estate ? invalid.length ? {
12348
12920
  name: "project",
12349
12921
  status: "fail",
12350
12922
  detail: `${estateDetail}; invalid: ${list2(invalidDetail)}`,
@@ -12397,12 +12969,11 @@ function formatReport(report) {
12397
12969
 
12398
12970
  // src/server.ts
12399
12971
  init_read_stats();
12400
- import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
12401
12972
  init_foreign();
12402
12973
 
12403
12974
  // src/events.ts
12404
- import { watch, existsSync as existsSync15 } from "node:fs";
12405
- import { join as join23 } from "node:path";
12975
+ import { watch, existsSync as existsSync16 } from "node:fs";
12976
+ import { join as join24 } from "node:path";
12406
12977
  var Broadcaster = class {
12407
12978
  listeners = /* @__PURE__ */ new Set();
12408
12979
  subscribe(fn) {
@@ -12420,7 +12991,7 @@ var Broadcaster = class {
12420
12991
  };
12421
12992
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
12422
12993
  function watchSource(projectDir, onChange, debounceMs = 200) {
12423
- const dir = existsSync15(join23(projectDir, "src")) ? join23(projectDir, "src") : projectDir;
12994
+ const dir = existsSync16(join24(projectDir, "src")) ? join24(projectDir, "src") : projectDir;
12424
12995
  let timer;
12425
12996
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
12426
12997
  const name = typeof file === "string" ? file : "";
@@ -12722,8 +13293,8 @@ async function emulatorDown(projectDir) {
12722
13293
  }
12723
13294
 
12724
13295
  // src/demos.ts
12725
- import { readFileSync as readFileSync18, existsSync as existsSync16, cpSync, mkdirSync as mkdirSync6 } from "node:fs";
12726
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join24, relative as relative7, resolve as resolve12, sep as sep6 } from "node:path";
13296
+ import { readFileSync as readFileSync18, existsSync as existsSync17, cpSync, mkdirSync as mkdirSync6 } from "node:fs";
13297
+ import { dirname as dirname6, isAbsolute as isAbsolute4, join as join25, relative as relative7, resolve as resolve14, sep as sep6 } from "node:path";
12727
13298
  import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
12728
13299
  function readCatalog(file, catalog) {
12729
13300
  let raw;
@@ -12734,7 +13305,7 @@ function readCatalog(file, catalog) {
12734
13305
  }
12735
13306
  const list3 = raw?.demos;
12736
13307
  if (!Array.isArray(list3)) return [];
12737
- const catalogDir = dirname6(resolve12(file));
13308
+ const catalogDir = dirname6(resolve14(file));
12738
13309
  return list3.filter((e) => validEntry(e)).map((e) => ({ ...e, catalog, catalogDir }));
12739
13310
  }
12740
13311
  function validEntry(e) {
@@ -12768,10 +13339,10 @@ function validEntry(e) {
12768
13339
  return true;
12769
13340
  }
12770
13341
  function loadDemoRegistry(pkgRoot3) {
12771
- const merged = readCatalog(join24(pkgRoot3, "demos.json"), "demos");
13342
+ const merged = readCatalog(join25(pkgRoot3, "demos.json"), "demos");
12772
13343
  const seen = new Set(merged.map((e) => e.name));
12773
13344
  const named = process.env.BEHOLD_WORKBENCH;
12774
- for (const e of [...readCatalog(join24(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve12(named), "workbench") : []]) {
13345
+ for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve14(named), "workbench") : []]) {
12775
13346
  if (seen.has(e.name)) {
12776
13347
  process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
12777
13348
  `);
@@ -12784,17 +13355,17 @@ function loadDemoRegistry(pkgRoot3) {
12784
13355
  }
12785
13356
  function demoLocalPath(entry) {
12786
13357
  if (entry.source !== "local" || !entry.path) return void 0;
12787
- return isAbsolute4(entry.path) ? entry.path : resolve12(entry.catalogDir ?? process.cwd(), entry.path);
13358
+ return isAbsolute4(entry.path) ? entry.path : resolve14(entry.catalogDir ?? process.cwd(), entry.path);
12788
13359
  }
12789
13360
  function missingRequirements(entry) {
12790
13361
  const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
12791
13362
  const finder = process.platform === "win32" ? "where" : "which";
12792
13363
  const missing = bins.filter((bin) => {
12793
- if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN && existsSync16(process.env.CHOUDOUFU_BIN)) return false;
13364
+ if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN && existsSync17(process.env.CHOUDOUFU_BIN)) return false;
12794
13365
  return spawnSync2(finder, [bin], { stdio: "ignore" }).status !== 0;
12795
13366
  });
12796
13367
  const local = demoLocalPath(entry);
12797
- if (local && !existsSync16(local)) missing.push(`${entry.path} (not checked out)`);
13368
+ if (local && !existsSync17(local)) missing.push(`${entry.path} (not checked out)`);
12798
13369
  return missing;
12799
13370
  }
12800
13371
  function fetchesFromNetwork(entry) {
@@ -12803,9 +13374,9 @@ function fetchesFromNetwork(entry) {
12803
13374
  function demoTargetDir(entry, cwd = process.cwd()) {
12804
13375
  const inPlace = entry.inPlace ? demoLocalPath(entry) : void 0;
12805
13376
  if (inPlace) return inPlace;
12806
- const legacy = resolve12(cwd, "behold-demo");
12807
- if (entry.name === "writes" && existsSync16(legacy)) return legacy;
12808
- return resolve12(cwd, "behold-demos", entry.name);
13377
+ const legacy = resolve14(cwd, "behold-demo");
13378
+ if (entry.name === "writes" && existsSync17(legacy)) return legacy;
13379
+ return resolve14(cwd, "behold-demos", entry.name);
12809
13380
  }
12810
13381
  async function loadDemo(entry, opts) {
12811
13382
  const { pkgRoot: pkgRoot3, target } = opts;
@@ -12814,12 +13385,12 @@ async function loadDemo(entry, opts) {
12814
13385
  const inPlace = entry.source === "local" && entry.inPlace;
12815
13386
  const work = inPlace ? local : target;
12816
13387
  if (inPlace) {
12817
- if (!existsSync16(work)) return { ok: false, error: `${entry.path} is not checked out (looked in ${work})` };
13388
+ if (!existsSync17(work)) return { ok: false, error: `${entry.path} is not checked out (looked in ${work})` };
12818
13389
  say(`serving ${work} where it is \u2014 nothing copied`);
12819
- } else if (!existsSync16(target)) {
13390
+ } else if (!existsSync17(target)) {
12820
13391
  if (entry.source === "bundled" || entry.source === "local" && local) {
12821
- const from = entry.source === "bundled" ? join24(pkgRoot3, entry.dir) : local;
12822
- if (!existsSync16(from)) return { ok: false, error: entry.source === "bundled" ? `this install has no bundled ${entry.dir}` : `${entry.path} is not checked out (looked in ${from})` };
13392
+ const from = entry.source === "bundled" ? join25(pkgRoot3, entry.dir) : local;
13393
+ if (!existsSync17(from)) return { ok: false, error: entry.source === "bundled" ? `this install has no bundled ${entry.dir}` : `${entry.path} is not checked out (looked in ${from})` };
12823
13394
  say(`copying to ${target} (it's yours \u2014 edit it)`);
12824
13395
  try {
12825
13396
  cpSync(from, target, {
@@ -12845,7 +13416,7 @@ async function loadDemo(entry, opts) {
12845
13416
  } else {
12846
13417
  say(`reusing ${target}`);
12847
13418
  }
12848
- if (!inPlace && existsSync16(join24(work, "package.json")) && !existsSync16(join24(work, "node_modules"))) {
13419
+ if (!inPlace && existsSync17(join25(work, "package.json")) && !existsSync17(join25(work, "node_modules"))) {
12849
13420
  say("npm install\u2026");
12850
13421
  if (await runStep("npm", ["install"], { cwd: work, shell: process.platform === "win32" }) !== 0) {
12851
13422
  return { ok: false, error: `npm install failed in ${work}` };
@@ -12862,7 +13433,7 @@ async function loadDemo(entry, opts) {
12862
13433
  return { ok: false, error: `setup failed (${entry.setup})` };
12863
13434
  }
12864
13435
  }
12865
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join24(work, d)) : [work] };
13436
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join25(work, d)) : [work] };
12866
13437
  }
12867
13438
  function runStep(cmd, args, opts = {}) {
12868
13439
  return new Promise((res) => {
@@ -12873,8 +13444,8 @@ function runStep(cmd, args, opts = {}) {
12873
13444
  }
12874
13445
 
12875
13446
  // src/server.ts
12876
- var pkgRoot = join25(dirname7(fileURLToPath4(import.meta.url)), "..");
12877
- var webRoot = join25(pkgRoot, "web");
13447
+ var pkgRoot = join26(dirname7(fileURLToPath4(import.meta.url)), "..");
13448
+ var webRoot = join26(pkgRoot, "web");
12878
13449
  var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
12879
13450
  function optsFromQuery(url, tierEnvVar, projectDir) {
12880
13451
  const q = url.searchParams;
@@ -12925,7 +13496,7 @@ function tierFailure(tier, message) {
12925
13496
  function beholdVersion() {
12926
13497
  try {
12927
13498
  const here = dirname7(fileURLToPath4(import.meta.url));
12928
- return JSON.parse(readFileSync19(join25(here, "..", "package.json"), "utf8")).version ?? "unknown";
13499
+ return JSON.parse(readFileSync19(join26(here, "..", "package.json"), "utf8")).version ?? "unknown";
12929
13500
  } catch {
12930
13501
  return "unknown";
12931
13502
  }
@@ -13216,11 +13787,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13216
13787
  const app = new Hono();
13217
13788
  setChoudoufuRunner(cfg.choudoufu?.run);
13218
13789
  setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
13790
+ setServedWorkspace(cfg.workspace);
13219
13791
  app.use("/api/*", async (c, next) => {
13220
13792
  if (c.req.method === "GET") return withReadSignal(c.req.raw.signal, next);
13221
13793
  await next();
13222
13794
  });
13223
13795
  if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
13796
+ const predictDeps = cfg.behaviour ?? { read: graphIr };
13224
13797
  let beholdConfig = loadBeholdConfig(cfg.projectDir);
13225
13798
  let tierEnvVar = beholdConfig.tiers?.envVar;
13226
13799
  const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
@@ -13331,9 +13904,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13331
13904
  return c.json({ started: true, name, ran: label });
13332
13905
  });
13333
13906
  app.post("/api/local/reset", (c) => {
13334
- const down = join25(cfg.projectDir, "scripts/local/local-down.sh");
13335
- const up = join25(cfg.projectDir, "scripts/local/local-up.sh");
13336
- if (!existsSync17(down) || !existsSync17(up)) {
13907
+ const down = join26(cfg.projectDir, "scripts/local/local-down.sh");
13908
+ const up = join26(cfg.projectDir, "scripts/local/local-up.sh");
13909
+ if (!existsSync18(down) || !existsSync18(up)) {
13337
13910
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
13338
13911
  }
13339
13912
  if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
@@ -13566,9 +14139,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13566
14139
  app.post("/api/project/open", async (c) => {
13567
14140
  if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
13568
14141
  const body = await c.req.json().catch(() => ({}));
13569
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve13(body.dir.trim()) : "";
13570
- if (!dir || !existsSync17(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
13571
- if (!existsSync17(join25(dir, "chant.config.ts"))) {
14142
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve15(body.dir.trim()) : "";
14143
+ if (!dir || !existsSync18(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
14144
+ if (!existsSync18(join26(dir, "chant.config.ts"))) {
13572
14145
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
13573
14146
  }
13574
14147
  switchServedProject([dir]);
@@ -13577,6 +14150,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13577
14150
  const switchServedProject = (dirs, env, spawnEnv) => {
13578
14151
  addRecent(cfg.projectDir);
13579
14152
  cfg.projectDir = dirs[0];
14153
+ cfg.workspace = void 0;
14154
+ setServedWorkspace(void 0);
13580
14155
  cfg.projectDirs = servesAsEstate(dirs) ? dirs : void 0;
13581
14156
  setChoudoufuSpawnEnv(spawnEnv);
13582
14157
  cfg.env = env;
@@ -13589,7 +14164,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13589
14164
  };
13590
14165
  app.post("/api/project/reveal", async (c) => {
13591
14166
  const body = await c.req.json().catch(() => ({}));
13592
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve13(body.dir.trim()) : cfg.projectDir;
14167
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve15(body.dir.trim()) : cfg.projectDir;
13593
14168
  const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
13594
14169
  if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
13595
14170
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
@@ -13617,7 +14192,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13617
14192
  fetches: fetchesFromNetwork(e),
13618
14193
  ...e.repo ? { repo: e.repo } : {},
13619
14194
  target,
13620
- loaded: existsSync17(target),
14195
+ loaded: existsSync18(target),
13621
14196
  satisfiable: missing.length === 0,
13622
14197
  // #254: the carve walkthrough runs fine here — it just isn't a project
13623
14198
  // to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
@@ -13676,20 +14251,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13676
14251
  loadingDemo = null;
13677
14252
  }
13678
14253
  });
14254
+ const layoutDir = () => cfg.workspace?.root ?? cfg.projectDir;
13679
14255
  const layoutWriteBlock = () => {
13680
14256
  if (cfg.previewMode) return "the layout sidecar is read-only in preview mode";
13681
14257
  if (cfg.carveReport) return "a carve report isn't a project \u2014 there's nowhere to keep a hand layout";
13682
14258
  if (cfg.layoutWrites === false) return "a static export captures a snapshot \u2014 it doesn't write to the project";
13683
- return unwritableReason(cfg.projectDir);
14259
+ return unwritableReason(layoutDir());
13684
14260
  };
13685
14261
  app.get("/api/layout", (c) => {
13686
14262
  const block = layoutWriteBlock();
13687
- const shared2 = { path: layoutPath(cfg.projectDir), writable: !block, ...block ? { reason: block } : {} };
14263
+ const shared2 = { path: layoutPath(layoutDir()), writable: !block, ...block ? { reason: block } : {} };
13688
14264
  const raw = new URL(c.req.url).searchParams.get("lens");
13689
- if (raw === null) return c.json({ ...shared2, lenses: readLayoutFile(cfg.projectDir).lenses });
14265
+ if (raw === null) return c.json({ ...shared2, lenses: readLayoutFile(layoutDir()).lenses });
13690
14266
  const lens = normalizeLens(raw);
13691
14267
  if (!lens) return c.json({ error: `not a lens key: ${raw}`, code: "bad-layout" }, 400);
13692
- return c.json({ ...shared2, lens, deltas: readLens(cfg.projectDir, lens) });
14268
+ return c.json({ ...shared2, lens, deltas: readLens(layoutDir(), lens) });
13693
14269
  });
13694
14270
  app.post("/api/layout", async (c) => {
13695
14271
  const block = layoutWriteBlock();
@@ -13713,8 +14289,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13713
14289
  return c.json({ error: "body needs `deltas`: {<node id>: {dx,dy,dw,dh}}", code: "bad-layout" }, 400);
13714
14290
  }
13715
14291
  try {
13716
- const stored = writeLens(cfg.projectDir, lens, body.deltas);
13717
- return c.json({ ok: true, lens: stored.lens, deltas: stored.deltas, count: Object.keys(stored.deltas).length, path: layoutPath(cfg.projectDir) });
14292
+ const stored = writeLens(layoutDir(), lens, body.deltas);
14293
+ return c.json({ ok: true, lens: stored.lens, deltas: stored.deltas, count: Object.keys(stored.deltas).length, path: layoutPath(layoutDir()) });
13718
14294
  } catch (err) {
13719
14295
  if (err instanceof LayoutTooLarge) return c.json({ error: err.message, code: "too-large" }, 413);
13720
14296
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
@@ -13727,7 +14303,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13727
14303
  const res = c.res;
13728
14304
  if (!res || res.status !== 200 || !(res.headers.get("content-type") ?? "").includes("json")) return;
13729
14305
  const lens = lensFromQuery(url.searchParams);
13730
- const deltas = readLens(cfg.projectDir, lens);
14306
+ const deltas = readLens(layoutDir(), lens);
13731
14307
  if (!Object.keys(deltas).length) return;
13732
14308
  let body;
13733
14309
  try {
@@ -13776,7 +14352,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13776
14352
  {
13777
14353
  method: "GET",
13778
14354
  path: "/api/overlay",
13779
- desc: "live drift overlay for ?env= \u2014 same shape/params as /api/graph, plus runtime=1; each priced entity carries attrs._behaviour (cost/headroom/errorRate/resilience/rightSize/provenance, #398) and meta.behaviour holds the engine, its sums, a refusal or an absent line"
14355
+ desc: "live drift overlay for ?env= \u2014 same shape/params as /api/graph, plus runtime=1; each priced entity carries attrs._behaviour (cost/headroom/errorRate/resilience/rightSize/provenance, #398) and meta.behaviour holds the engine, its sums, a refusal or an absent line; with ?traffic= (or .behold.json behaviour.traffic) each member is predicted live and declared, and meta.behaviour adds live, declared and delta with attrs._behaviourDeclared per entity (#402)"
13780
14356
  },
13781
14357
  { method: "GET", path: "/api/layout", desc: "hand-layout sidecar (.behold/layout.json): ?lens=<key> \u2192 {lens, deltas, writable}; no lens \u2192 every lens" },
13782
14358
  { method: "POST", path: "/api/layout", desc: "store one lens's deltas: JSON body {lens, deltas: {<node id>: {dx,dy,dw,dh}}} (the only file behold writes in your project)" },
@@ -13878,8 +14454,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13878
14454
  }
13879
14455
  const runState = runner.runState;
13880
14456
  const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
13881
- const join28 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
13882
- const opsIr = join28 ? paintPlayhead(declaredIr, join28, runState.gate) : declaredIr;
14457
+ const join30 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
14458
+ const opsIr = join30 ? paintPlayhead(declaredIr, join30, runState.gate) : declaredIr;
13883
14459
  const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
13884
14460
  const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
13885
14461
  return c.json({
@@ -13892,7 +14468,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13892
14468
  target: null,
13893
14469
  mode: "ops",
13894
14470
  ops: parsedOps.length,
13895
- note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join28)} ${operatorNote(operator)}`,
14471
+ note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join30)} ${operatorNote(operator)}`,
13896
14472
  // The operator strip's own model: the ConvergeOps this project
13897
14473
  // declares, plus whatever the last status read said. `declared`
13898
14474
  // without a `strip` is the honest starting state — a loop exists and
@@ -13999,12 +14575,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
13999
14575
  srcCompositeEdgesAttached = 0;
14000
14576
  }
14001
14577
  }
14002
- if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync17(join25(cfg.projectDir, "chant.config.ts"))) {
14578
+ if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync18(join26(cfg.projectDir, "chant.config.ts"))) {
14003
14579
  return c.json(noProjectError(cfg.projectDir), 404);
14004
14580
  }
14005
14581
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
14006
14582
  const collapse = new URL(c.req.url).searchParams.get("collapse") === "1";
14007
14583
  let collapsedNote;
14584
+ const srcTraffic = components ? void 0 : trafficFor(new URL(c.req.url).searchParams, beholdConfig);
14585
+ const srcBehaviour = srcTraffic ? attachDeclaredBehaviour(
14586
+ ir,
14587
+ await Promise.all(
14588
+ (multi ? estateMembers(cfg.projectDirs) : [{ name: void 0, dir: cfg.projectDir, kind: memberKindOf(cfg.projectDir) ?? "chant" }]).map(
14589
+ async (m) => ({ name: m.name, prediction: await predictMember(m, void 0, srcTraffic, predictDeps) })
14590
+ )
14591
+ )
14592
+ ) : void 0;
14008
14593
  const expanded = ir;
14009
14594
  if (multi && collapse) {
14010
14595
  const shut = collapseBoxes(ir);
@@ -14029,6 +14614,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14029
14614
  meta: {
14030
14615
  projectDir: cfg.projectDir,
14031
14616
  env: metaEnv,
14617
+ ...srcBehaviour ? { behaviour: srcBehaviour } : {},
14032
14618
  ...srcNote ? { note: srcNote } : {},
14033
14619
  // #393 item 7: the strip's form of the same note, when there is a
14034
14620
  // shorter true one. Absent means "the note fits" — the SPA shows
@@ -14162,7 +14748,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14162
14748
  });
14163
14749
  const choudoufuPlanDrift = async (dirs, wanted) => {
14164
14750
  const members = estateMembers(dirs).filter((m) => m.kind === "choudoufu");
14165
- const byDir = /* @__PURE__ */ new Map();
14751
+ const byDir2 = /* @__PURE__ */ new Map();
14166
14752
  let read = false;
14167
14753
  for (const m of members) {
14168
14754
  const stamp = memberSourceStamp(m.dir);
@@ -14170,7 +14756,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14170
14756
  const hit = cachedChoudoufuPlan(m.dir, stamp);
14171
14757
  if (hit) {
14172
14758
  read = true;
14173
- byDir.set(m.dir, { name: m.name, drift: hit });
14759
+ byDir2.set(m.dir, { name: m.name, drift: hit });
14174
14760
  }
14175
14761
  continue;
14176
14762
  }
@@ -14178,11 +14764,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14178
14764
  const drift = await readChoudoufuPlan(m.dir, cfg.choudoufu?.run);
14179
14765
  cacheChoudoufuPlan(m.dir, stamp, drift);
14180
14766
  read = true;
14181
- byDir.set(m.dir, { name: m.name, drift });
14767
+ byDir2.set(m.dir, { name: m.name, drift });
14182
14768
  } catch {
14183
14769
  }
14184
14770
  }
14185
- return { read, byDir };
14771
+ return { read, byDir: byDir2 };
14186
14772
  };
14187
14773
  app.get("/api/overlay", async (c) => {
14188
14774
  const query = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
@@ -14224,11 +14810,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14224
14810
  mode: "progressive",
14225
14811
  // Which members the picture is still waiting on, so a client can
14226
14812
  // say so without inferring it from the attrs.
14227
- pending: shortStackNames2(cfg.projectDirs)
14813
+ pending: estateMemberNames(cfg.projectDirs)
14228
14814
  }
14229
14815
  });
14230
14816
  }
14231
- const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env }, reclassifyOverlay, { fresh: planWanted });
14817
+ const traffic2 = trafficFor(new URL(c.req.url).searchParams, beholdConfig);
14818
+ const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env, ...traffic2 ? { traffic: traffic2 } : {} }, reclassifyOverlay, { fresh: planWanted });
14232
14819
  if (est.dropped.length === est.total) {
14233
14820
  return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
14234
14821
  }
@@ -14267,11 +14854,24 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14267
14854
  meta: { projectDir: cfg.projectDir, env, mode: "logical", estate: est.total, vocabulary, ...note3 ? { note: note3 } : {} }
14268
14855
  });
14269
14856
  }
14270
- const behaviour2 = attachBehaviour(
14857
+ const predicted = traffic2 ? await Promise.all(
14858
+ estateMembers(cfg.projectDirs).map(async (m) => ({ m, prediction: await predictMember(m, env, traffic2, predictDeps, planWanted) }))
14859
+ ) : [];
14860
+ const paintNotes2 = [];
14861
+ for (const { m, prediction: prediction2 } of predicted) {
14862
+ if (prediction2.live) est.memberMeta[m.name] = paintLive(ir2, m.name, prediction2.live, paintNotes2);
14863
+ }
14864
+ const attached2 = attachBehaviour(
14271
14865
  ir2,
14272
14866
  estateMembers(cfg.projectDirs).map((m) => ({ name: m.name, dir: m.dir, meta: est.memberMeta[m.name] })),
14273
- env
14867
+ env,
14868
+ ...traffic2 ? [() => void 0] : []
14274
14869
  );
14870
+ const behaviour2 = traffic2 ? attachBehaviourDelta(
14871
+ ir2,
14872
+ predicted.map(({ m, prediction: prediction2 }) => ({ name: m.name, liveMeta: est.memberMeta[m.name], prediction: prediction2 })),
14873
+ paintNotes2.length ? { ...attached2, diagnostics: [...attached2.diagnostics ?? [], ...paintNotes2] } : attached2
14874
+ ) : attached2;
14275
14875
  const collapsed = new URL(c.req.url).searchParams.get("collapse") === "1" && !runtime ? collapseBoxes(ir2) : void 0;
14276
14876
  const expanded = ir2;
14277
14877
  if (collapsed) ir2 = collapsed.ir;
@@ -14317,7 +14917,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14317
14917
  }
14318
14918
  });
14319
14919
  }
14320
- const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
14920
+ const traffic = trafficFor(new URL(c.req.url).searchParams, beholdConfig);
14921
+ const ownKind = memberKindOf(cfg.projectDir) ?? "chant";
14922
+ const opts = {
14923
+ ...query,
14924
+ live: true,
14925
+ overlay: true,
14926
+ env,
14927
+ ...logical || runtime ? { detail: 3 } : {},
14928
+ ...traffic && memberTakesTraffic(cfg.projectDir, ownKind) ? { traffic } : {}
14929
+ };
14321
14930
  const read = await graphIr(cfg.projectDir, opts);
14322
14931
  const readMeta = read.meta;
14323
14932
  let ir = joinCarvedSources(reclassifyOverlay(read), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
@@ -14358,7 +14967,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14358
14967
  compositeEdgesAttached = 0;
14359
14968
  }
14360
14969
  }
14361
- const behaviour = attachBehaviour(ir, [{ dir: cfg.projectDir, meta: readMeta }], env);
14970
+ const prediction = traffic ? await predictMember({ dir: cfg.projectDir, kind: ownKind }, env, traffic, predictDeps) : void 0;
14971
+ const paintNotes = [];
14972
+ const liveMeta = prediction?.live ? paintLive(ir, void 0, prediction.live, paintNotes) : readMeta;
14973
+ const attached = attachBehaviour(ir, [{ dir: cfg.projectDir, meta: liveMeta }], env, ...traffic ? [() => void 0] : []);
14974
+ const behaviour = prediction ? attachBehaviourDelta(ir, [{ liveMeta, prediction }], paintNotes.length ? { ...attached, diagnostics: [...attached.diagnostics ?? [], ...paintNotes] } : attached) : attached;
14362
14975
  const { svg } = renderGraph(ir, { boxes: "byContainer", radial: new URL(c.req.url).searchParams.get("radial") === "1" });
14363
14976
  const zoom = runtime ? "runtime" : query.detail === 1 ? "composites" : query.detail === 3 ? "attributes" : "resources";
14364
14977
  const tierNote = tierMismatchNote(ir, beholdConfig.tiers, query.tier);
@@ -14451,9 +15064,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14451
15064
  });
14452
15065
  const choudoufuPlanPath = (plan) => {
14453
15066
  if (!plan) return { error: "no plan named", code: "choudoufu_no_plan", remedy: "pass ?plan=<path to a carve.json inside a served choudoufu member> \u2014 GET /api/project lists the ones found" };
14454
- const abs = resolve13(cfg.projectDir, plan);
15067
+ const abs = resolve15(cfg.projectDir, plan);
14455
15068
  const members = (cfg.projectDirs ?? [cfg.projectDir]).filter((d) => memberKindOf(d) === "choudoufu");
14456
- const inside = members.some((d) => abs.startsWith(resolve13(d) + sep7));
15069
+ const inside = members.some((d) => abs.startsWith(resolve15(d) + sep7));
14457
15070
  if (!inside) return { error: `${plan} is not inside a served choudoufu member`, code: "choudoufu_plan_outside", remedy: "a plan lives in the estate it moves; point ?plan= at a carve.json under one of the served members" };
14458
15071
  return { path: abs };
14459
15072
  };
@@ -14585,7 +15198,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
14585
15198
  });
14586
15199
  const rel = relative8(process.cwd(), webRoot) || ".";
14587
15200
  app.use("/*", serveStatic({ root: rel }));
14588
- app.get("/", serveStatic({ path: join25(rel, "index.html") }));
15201
+ app.get("/", serveStatic({ path: join26(rel, "index.html") }));
14589
15202
  return app;
14590
15203
  }
14591
15204
  async function bootLocalEmulators(dir, who) {
@@ -14614,7 +15227,7 @@ async function bootLocalEmulators(dir, who) {
14614
15227
  async function startServer(cfg) {
14615
15228
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
14616
15229
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
14617
- if (existsSync17(join25(dir, "chant.config.ts"))) addRecent(dir);
15230
+ if (existsSync18(join26(dir, "chant.config.ts"))) addRecent(dir);
14618
15231
  }
14619
15232
  const broadcaster = new Broadcaster();
14620
15233
  const frames = new FrameBuffer();
@@ -14646,7 +15259,7 @@ async function startServer(cfg) {
14646
15259
  invalidateOverlay(memberDir);
14647
15260
  onEstateChange(memberDir);
14648
15261
  };
14649
- const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename2(dir)}]` : "";
15262
+ const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename3(dir)}]` : "";
14650
15263
  const onPollDrift = (dir, movedLexicons) => {
14651
15264
  broadcaster.emit("changed", dir);
14652
15265
  if (autoSync === "off") return;
@@ -14656,7 +15269,7 @@ async function startServer(cfg) {
14656
15269
  const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
14657
15270
  const { routed, local } = splitForgeRouted(discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]), executorDesignation);
14658
15271
  for (const { op: o, designation: d } of routed) {
14659
- 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 ${basename2(o.dir)}/.behold.json \u2014 never applied from here`);
15272
+ 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 ${basename3(o.dir)}/.behold.json \u2014 never applied from here`);
14660
15273
  }
14661
15274
  const { picks, declined } = pickAutoSyncOps(
14662
15275
  autoSync,
@@ -14778,9 +15391,132 @@ async function startServer(cfg) {
14778
15391
  // src/cli.ts
14779
15392
  init_chant();
14780
15393
 
15394
+ // src/workspace-convert.ts
15395
+ init_chant();
15396
+ import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
15397
+ import { basename as basename4, dirname as dirname8, join as join27, relative as relative9, resolve as resolve16, sep as sep8 } from "node:path";
15398
+ import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
15399
+ init_project();
15400
+ init_terraform_member();
15401
+ var TERRAFORM_LEXICON2 = "@intentius/chant-lexicon-terraform";
15402
+ function declarationName(raw) {
15403
+ const folded = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
15404
+ return folded || "member";
15405
+ }
15406
+ function unique(name, taken) {
15407
+ let out = name;
15408
+ for (let i = 2; taken.has(out); i++) out = `${name.slice(0, 40 - String(i).length - 1)}-${i}`;
15409
+ taken.add(out);
15410
+ return out;
15411
+ }
15412
+ var posix = (p) => p.split(sep8).join("/");
15413
+ function installedVersion(pkg, dir) {
15414
+ for (let at = resolve16(dir); ; at = dirname8(at)) {
15415
+ const manifest = join27(at, "node_modules", ...pkg.split("/"), "package.json");
15416
+ if (existsSync19(manifest)) {
15417
+ try {
15418
+ const v = JSON.parse(readFileSync20(manifest, "utf8")).version;
15419
+ return typeof v === "string" ? v : void 0;
15420
+ } catch {
15421
+ return void 0;
15422
+ }
15423
+ }
15424
+ if (dirname8(at) === at) return void 0;
15425
+ }
15426
+ }
15427
+ function planConversion(rootArg) {
15428
+ const root = resolve16(rootArg);
15429
+ const existing = DECLARATION_FILES.find((f) => existsSync19(join27(root, f)));
15430
+ if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
15431
+ const shape = detectProjectShape(root);
15432
+ if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
15433
+ return { ok: false, error: `${root} is not an estate root with a member list, so there is no member list to convert.` };
15434
+ }
15435
+ const abs = shape.members.map((m) => resolve16(root, m.dir));
15436
+ const shorts = shortStackNames2(abs);
15437
+ const taken = /* @__PURE__ */ new Set();
15438
+ const members = [];
15439
+ const moves = [];
15440
+ const notes = [];
15441
+ let needsLexicon = false;
15442
+ shape.members.forEach((m, i) => {
15443
+ const short = shorts[i];
15444
+ const dir = posix(relative9(root, abs[i])) || ".";
15445
+ if (m.kind === "terraform") {
15446
+ needsLexicon = true;
15447
+ const scan = discoverTerraformRoots(abs[i]);
15448
+ for (const r of scan.roots) {
15449
+ const name2 = unique(declarationName(r.name), taken);
15450
+ members.push({ name: name2, dir: r.dir === "." ? dir : posix(join27(dir, r.dir)), kind: "terraform" });
15451
+ moves.push({ from: `${short}/${r.name}/`, to: `${name2}/${name2}/` });
15452
+ }
15453
+ for (const s of scan.skipped) notes.push(`${posix(join27(dir, s.dir))} is not declared: ${s.why}.`);
15454
+ if (scan.roots.length > 1) notes.push(`${dir} held ${scan.roots.length} Terraform roots; each is its own member now, since a chant terraform member is one root.`);
15455
+ return;
15456
+ }
15457
+ if (m.kind === "choudoufu") needsLexicon = true;
15458
+ const name = unique(declarationName(short), taken);
15459
+ members.push({ name, dir, kind: m.kind });
15460
+ if (name !== short) {
15461
+ moves.push({ from: `${short}/`, to: `${name}/` });
15462
+ notes.push(`member ${short} is declared as ${name}: a declared name is lowercase letters, digits and hyphens.`);
15463
+ }
15464
+ });
15465
+ const pins = [];
15466
+ const chant = resolveChant(root);
15467
+ if (chant.source === "project" && chant.version) pins.push({ package: "@intentius/chant", version: chant.version });
15468
+ if (needsLexicon) {
15469
+ const version = installedVersion(TERRAFORM_LEXICON2, root);
15470
+ if (version) pins.push({ package: TERRAFORM_LEXICON2, version });
15471
+ else notes.push(`${TERRAFORM_LEXICON2} is not installed where chant looks from ${root}; install it there and add it to "pins", or chant cannot read the terraform and choudoufu members.`);
15472
+ }
15473
+ const declaration = {
15474
+ name: declarationName(basename4(root)),
15475
+ schema: 1,
15476
+ ...pins.length ? { pins } : {},
15477
+ members
15478
+ };
15479
+ const firstLayout = layoutPath(abs[0]);
15480
+ moves.sort((a, b) => b.from.length - a.from.length);
15481
+ return { ok: true, plan: { root, from: shape.membersFrom === "behold-config" ? "behold-config" : "workspaces", declaration, moves, ...existsSync19(firstLayout) ? { layoutFrom: firstLayout } : {}, notes } };
15482
+ }
15483
+ function moveLayoutIds(file, moves) {
15484
+ const lenses = {};
15485
+ for (const [lens, deltas] of Object.entries(file.lenses)) {
15486
+ const out = {};
15487
+ for (const [id, d] of Object.entries(deltas)) {
15488
+ const move = moves.find((m) => id.startsWith(m.from));
15489
+ out[move ? move.to + id.slice(move.from.length) : id] = d;
15490
+ }
15491
+ lenses[lens] = out;
15492
+ }
15493
+ return { version: 1, lenses };
15494
+ }
15495
+ function applyConversion(plan) {
15496
+ const declarationFile = join27(plan.root, DECLARATION_FILES[0]);
15497
+ if (existsSync19(declarationFile)) throw new Error(`${declarationFile} appeared while converting; nothing was written`);
15498
+ writeFileSync6(declarationFile, `${JSON.stringify(plan.declaration, null, 2)}
15499
+ `);
15500
+ let layout;
15501
+ const target = layoutPath(plan.root);
15502
+ if (!plan.layoutFrom) layout = { skipped: "no saved layout to move" };
15503
+ else if (resolve16(plan.layoutFrom) === resolve16(target)) layout = { skipped: "the saved layout is already in the root" };
15504
+ else if (existsSync19(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
15505
+ else {
15506
+ const moved = moveLayoutIds(readLayoutFile(dirname8(dirname8(plan.layoutFrom))), plan.moves);
15507
+ mkdirSync7(join27(plan.root, LAYOUT_DIR), { recursive: true });
15508
+ const tmp = `${target}.tmp`;
15509
+ writeFileSync6(tmp, `${JSON.stringify(moved, null, 2)}
15510
+ `);
15511
+ renameSync2(tmp, target);
15512
+ layout = { wrote: target, ids: Object.values(moved.lenses).reduce((n, l) => n + Object.keys(l).length, 0) };
15513
+ }
15514
+ return { declaration: declarationFile, layout };
15515
+ }
15516
+
14781
15517
  // src/export.ts
14782
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, copyFileSync, readFileSync as readFileSync20, readdirSync as readdirSync10, existsSync as existsSync18 } from "node:fs";
14783
- import { join as join26, dirname as dirname8, basename as basename3 } from "node:path";
15518
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, copyFileSync, readFileSync as readFileSync21, readdirSync as readdirSync10, existsSync as existsSync20 } from "node:fs";
15519
+ import { join as join28, dirname as dirname9, basename as basename5 } from "node:path";
14784
15520
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14785
15521
  var LENS_PARAMS = ["collapse", "components", "detail", "env", "logical", "ops", "radial", "stacks", "tier"];
14786
15522
  function canonicalKey(path, params) {
@@ -14829,31 +15565,31 @@ function captureKeys(axes) {
14829
15565
  return [...keys];
14830
15566
  }
14831
15567
  function webDir() {
14832
- return join26(dirname8(fileURLToPath5(import.meta.url)), "..", "web");
15568
+ return join28(dirname9(fileURLToPath5(import.meta.url)), "..", "web");
14833
15569
  }
14834
15570
  function pkgRoot2() {
14835
- return join26(dirname8(fileURLToPath5(import.meta.url)), "..");
15571
+ return join28(dirname9(fileURLToPath5(import.meta.url)), "..");
14836
15572
  }
14837
15573
  var NOTICE_FILES = ["LICENSE", "THIRD_PARTY.md"];
14838
15574
  function copyNotices(outDir, root = pkgRoot2()) {
14839
15575
  const copied = [];
14840
15576
  for (const f of NOTICE_FILES) {
14841
- if (!existsSync18(join26(root, f))) continue;
14842
- copyFileSync(join26(root, f), join26(outDir, f));
15577
+ if (!existsSync20(join28(root, f))) continue;
15578
+ copyFileSync(join28(root, f), join28(outDir, f));
14843
15579
  copied.push(f);
14844
15580
  }
14845
- const lic = join26(root, "licenses");
14846
- if (existsSync18(lic)) {
14847
- mkdirSync7(join26(outDir, "licenses"), { recursive: true });
15581
+ const lic = join28(root, "licenses");
15582
+ if (existsSync20(lic)) {
15583
+ mkdirSync8(join28(outDir, "licenses"), { recursive: true });
14848
15584
  for (const f of readdirSync10(lic)) {
14849
- copyFileSync(join26(lic, f), join26(outDir, "licenses", f));
15585
+ copyFileSync(join28(lic, f), join28(outDir, "licenses", f));
14850
15586
  copied.push(`licenses/${f}`);
14851
15587
  }
14852
15588
  }
14853
15589
  return copied;
14854
15590
  }
14855
15591
  function workerName(project, override) {
14856
- const raw = override ?? `behold-${basename3(project)}`;
15592
+ const raw = override ?? `behold-${basename5(project)}`;
14857
15593
  const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
14858
15594
  return name || "behold-export";
14859
15595
  }
@@ -14861,8 +15597,8 @@ async function runExport(cfg, outDir, opts = {}) {
14861
15597
  const app = createApp({ ...cfg, layoutWrites: false });
14862
15598
  const proj = await (await app.request("/api/project")).json();
14863
15599
  const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
14864
- const snapDir = join26(outDir, "snapshots");
14865
- mkdirSync7(snapDir, { recursive: true });
15600
+ const snapDir = join28(outDir, "snapshots");
15601
+ mkdirSync8(snapDir, { recursive: true });
14866
15602
  const keyToFile = {};
14867
15603
  let ok = 0;
14868
15604
  let failed = 0;
@@ -14870,7 +15606,7 @@ async function runExport(cfg, outDir, opts = {}) {
14870
15606
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
14871
15607
  const body = await res.text();
14872
15608
  const file = slug3(key);
14873
- writeFileSync6(join26(snapDir, file), body);
15609
+ writeFileSync7(join28(snapDir, file), body);
14874
15610
  keyToFile[key] = `snapshots/${file}`;
14875
15611
  if (res.ok) ok++;
14876
15612
  else failed++;
@@ -14882,22 +15618,22 @@ async function runExport(cfg, outDir, opts = {}) {
14882
15618
  axes,
14883
15619
  keyToFile
14884
15620
  };
14885
- writeFileSync6(join26(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
14886
- const html = readFileSync20(join26(webDir(), "index.html"), "utf8").replace(
15621
+ writeFileSync7(join28(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
15622
+ const html = readFileSync21(join28(webDir(), "index.html"), "utf8").replace(
14887
15623
  /<\/head>/i,
14888
15624
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
14889
15625
  </head>`
14890
15626
  );
14891
- writeFileSync6(join26(outDir, "index.html"), html);
15627
+ writeFileSync7(join28(outDir, "index.html"), html);
14892
15628
  for (const f of readdirSync10(webDir())) {
14893
15629
  if (f === "index.html") continue;
14894
- copyFileSync(join26(webDir(), f), join26(outDir, f));
15630
+ copyFileSync(join28(webDir(), f), join28(outDir, f));
14895
15631
  }
14896
- writeFileSync6(join26(outDir, "README.md"), BUNDLE_README);
15632
+ writeFileSync7(join28(outDir, "README.md"), BUNDLE_README);
14897
15633
  copyNotices(outDir);
14898
15634
  const name = workerName(cfg.projectDir, opts.name);
14899
- writeFileSync6(
14900
- join26(outDir, "wrangler.jsonc"),
15635
+ writeFileSync7(
15636
+ join28(outDir, "wrangler.jsonc"),
14901
15637
  JSON.stringify(
14902
15638
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
14903
15639
  null,
@@ -14949,12 +15685,13 @@ init_project();
14949
15685
  init_member_kind();
14950
15686
  init_choudoufu_member();
14951
15687
  init_carve_lens();
15688
+ init_terraform_member();
14952
15689
  var USAGE = `behold \u2014 a live control plane on chant (read-only core)
14953
15690
 
14954
15691
  Usage:
14955
15692
  behold demo [name] [target-dir] [--port <n>] [--list]
14956
15693
  behold demo carve [--live] [--port <n>]
14957
- behold doctor [project-dir] [--json]
15694
+ behold doctor [project-dir] [--json] [--fix]
14958
15695
  behold preview [project-dir] [--port <n>] [--emulator]
14959
15696
  behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
14960
15697
  behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
@@ -15048,6 +15785,10 @@ Options:
15048
15785
  with chant's generic \`chant emulator up\`.
15049
15786
  --json doctor only: the report as JSON (stable keys) instead of
15050
15787
  the console lines \u2014 the AGENTS.md audience.
15788
+ --fix doctor only: write chant.workspace.json for an estate
15789
+ root that lists its members in .behold.json, move its
15790
+ saved layout to the root, and run chant workspace check.
15791
+ The one doctor that writes (#464).
15051
15792
  --out <dir> export only: output directory (default ./behold-export).
15052
15793
  --name <worker> export only: Cloudflare Worker name in the generated
15053
15794
  wrangler.jsonc.
@@ -15139,7 +15880,22 @@ ${USAGE}`);
15139
15880
  process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
15140
15881
  process.exit(2);
15141
15882
  }
15142
- const dirs = projectDirs.map((d) => resolve14(d));
15883
+ const dirs = projectDirs.map((d) => resolve17(d));
15884
+ const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0]) : void 0;
15885
+ if (workspace) {
15886
+ const drawn = drawnMembers(workspace).map((m) => m.abs);
15887
+ await startServer({
15888
+ projectDir: drawn[0] ?? workspace.root,
15889
+ projectDirs: drawn,
15890
+ workspace,
15891
+ port,
15892
+ ...env ? { env } : {},
15893
+ ...pollSecs !== void 0 ? { pollSecs } : {},
15894
+ ...autoSync !== "off" ? { autoSync } : {},
15895
+ ...local ? { local: true } : {}
15896
+ });
15897
+ return;
15898
+ }
15143
15899
  for (const d of dirs) warnIfNotChantProject(d);
15144
15900
  await startServer({
15145
15901
  projectDir: dirs[0],
@@ -15156,6 +15912,28 @@ ${USAGE}`);
15156
15912
  ...local ? { local: true } : {}
15157
15913
  });
15158
15914
  }
15915
+ async function declaredWorkspace(root) {
15916
+ const read = await readWorkspace(root);
15917
+ if (!read.ok) {
15918
+ process.stderr.write(`behold serve: ${read.refusal.error}
15919
+ ${read.refusal.remedy}
15920
+ `);
15921
+ process.exit(1);
15922
+ }
15923
+ const ws = read.workspace;
15924
+ const drawn = drawnMembers(ws);
15925
+ process.stdout.write(`behold: serving chant workspace ${ws.name} (${ws.file}), ${drawn.length} of ${ws.members.length} members drawn
15926
+ `);
15927
+ for (const m of unreadableMembers(ws)) process.stdout.write(` ${m.name}: unreadable, ${m.reason.code}: ${m.reason.message}
15928
+ `);
15929
+ for (const m of undrawnMembers(ws)) process.stdout.write(` ${m.name}: kind ${m.kind}, listed and not drawn${m.because ? ` (${m.because})` : ""}
15930
+ `);
15931
+ if (loadBeholdConfig(root).members) {
15932
+ process.stderr.write(`behold: warning \u2014 ${root}/.behold.json lists members, and chant.workspace.json does too; the declaration wins and .behold.json's members are ignored.
15933
+ `);
15934
+ }
15935
+ return ws;
15936
+ }
15159
15937
  function warnIfNotChantProject(dir) {
15160
15938
  const shape = detectProjectShape(dir);
15161
15939
  if (shape.kind === "project") return;
@@ -15163,7 +15941,7 @@ function warnIfNotChantProject(dir) {
15163
15941
  if (shape.kind === "estate") {
15164
15942
  process.stderr.write(
15165
15943
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
15166
- Serve its members composed: behold serve ${shape.members.map((m) => join27(dir, m.dir)).join(" ")}
15944
+ Serve its members composed: behold serve ${shape.members.map((m) => join29(dir, m.dir)).join(" ")}
15167
15945
  `
15168
15946
  );
15169
15947
  return;
@@ -15198,8 +15976,8 @@ async function runCarve(rest) {
15198
15976
  );
15199
15977
  process.exit(2);
15200
15978
  }
15201
- const reportPath = resolve14(fileArg);
15202
- const parsed = readCarveReport(reportPath, (p) => readFileSync21(p, "utf8"));
15979
+ const reportPath = resolve17(fileArg);
15980
+ const parsed = readCarveReport(reportPath, (p) => readFileSync22(p, "utf8"));
15203
15981
  if (!parsed.ok) {
15204
15982
  process.stderr.write(`behold carve: ${parsed.refusal.error}
15205
15983
  ${parsed.refusal.remedy}
@@ -15210,13 +15988,15 @@ async function runCarve(rest) {
15210
15988
  `behold carve \u2014 ${parsed.report.resources.length} resource(s)/module(s) ranked${parsed.report.from ? ` from ${parsed.report.from}` : ""}
15211
15989
  `
15212
15990
  );
15213
- await startServer({ projectDir: dirname9(reportPath), carveReport: reportPath, port });
15991
+ await startServer({ projectDir: dirname10(reportPath), carveReport: reportPath, port });
15214
15992
  }
15215
15993
  async function runDoctor(rest) {
15216
15994
  let json = false;
15995
+ let fix = false;
15217
15996
  let dirArg;
15218
15997
  for (const a of rest) {
15219
15998
  if (a === "--json") json = true;
15999
+ else if (a === "--fix") fix = true;
15220
16000
  else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
15221
16001
  else if (!a.startsWith("-")) dirArg = a;
15222
16002
  else {
@@ -15226,17 +16006,44 @@ async function runDoctor(rest) {
15226
16006
  }
15227
16007
  }
15228
16008
  const dir = dirArg ?? ".";
15229
- if (!existsSync19(resolve14(dir))) {
15230
- process.stderr.write(`behold doctor: no such directory: ${resolve14(dir)}
16009
+ if (!existsSync21(resolve17(dir))) {
16010
+ process.stderr.write(`behold doctor: no such directory: ${resolve17(dir)}
15231
16011
  `);
15232
16012
  process.exit(2);
15233
16013
  }
16014
+ if (fix) return runDoctorFix(dir);
15234
16015
  const report = await diagnose(dir);
15235
16016
  process.stdout.write(json ? JSON.stringify(report, null, 2) + "\n" : formatReport(report));
15236
16017
  if (!report.ok) process.exitCode = 1;
15237
16018
  }
16019
+ async function runDoctorFix(dir) {
16020
+ const planned = planConversion(dir);
16021
+ if (!planned.ok) {
16022
+ process.stderr.write(`behold doctor --fix: ${planned.error}
16023
+ `);
16024
+ process.exitCode = 1;
16025
+ return;
16026
+ }
16027
+ const { plan } = planned;
16028
+ const wrote = applyConversion(plan);
16029
+ process.stdout.write(`wrote ${wrote.declaration} (${plan.declaration.members.length} members)
16030
+ `);
16031
+ if ("wrote" in wrote.layout) process.stdout.write(`moved the saved layout to ${wrote.layout.wrote} (${wrote.layout.ids} node ids)
16032
+ `);
16033
+ else process.stdout.write(`layout: ${wrote.layout.skipped}
16034
+ `);
16035
+ for (const n of plan.notes) process.stdout.write(`note: ${n}
16036
+ `);
16037
+ if (plan.from === "behold-config") process.stdout.write(`.behold.json's members are ignored from now on, since the declaration lists them; you can delete them.
16038
+ `);
16039
+ const check = await runChantRaw(["workspace", "check", "--format", "json"], plan.root);
16040
+ process.stdout.write(check.code === 0 ? "chant workspace check: passed\n" : `chant workspace check failed (exit ${check.code}):
16041
+ ${check.stdout || check.stderr}
16042
+ `);
16043
+ if (check.code !== 0) process.exitCode = 1;
16044
+ }
15238
16045
  async function runDemo(rest) {
15239
- const pkgRoot3 = join27(dirname9(fileURLToPath6(import.meta.url)), "..");
16046
+ const pkgRoot3 = join29(dirname10(fileURLToPath6(import.meta.url)), "..");
15240
16047
  const registry2 = loadDemoRegistry(pkgRoot3);
15241
16048
  let port = 4600;
15242
16049
  let live2 = false;
@@ -15299,7 +16106,7 @@ async function runDemo(rest) {
15299
16106
  `);
15300
16107
  process.exit(2);
15301
16108
  }
15302
- const target = dirArg ? resolve14(dirArg) : demoTargetDir(entry);
16109
+ const target = dirArg ? resolve17(dirArg) : demoTargetDir(entry);
15303
16110
  const loaded = await loadDemo(entry, { pkgRoot: pkgRoot3, target, log: (line) => process.stdout.write(line + "\n") });
15304
16111
  if (!loaded.ok) {
15305
16112
  process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
@@ -15331,7 +16138,7 @@ function spawnStep(cmd, args, cwd) {
15331
16138
  });
15332
16139
  }
15333
16140
  async function serveCarveDemo(target, carve, port, live2 = false) {
15334
- const at = (rel) => resolve14(target, rel);
16141
+ const at = (rel) => resolve17(target, rel);
15335
16142
  const project = at(carve.project);
15336
16143
  const from = at(carve.from);
15337
16144
  const state = carve.state ? at(carve.state) : void 0;
@@ -15371,7 +16178,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
15371
16178
  };
15372
16179
  process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
15373
16180
  }
15374
- if (existsSync19(join27(project, "package.json")) && !existsSync19(join27(project, "node_modules"))) {
16181
+ if (existsSync21(join29(project, "package.json")) && !existsSync21(join29(project, "node_modules"))) {
15375
16182
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
15376
16183
  `);
15377
16184
  const code = await spawnStep("npm", ["install"], project);
@@ -15382,10 +16189,11 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
15382
16189
  }
15383
16190
  }
15384
16191
  let degraded;
15385
- if (!existsSync19(join27(target, "node_modules", "@cdktf", "hcl2json"))) {
15386
- process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
15387
- const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
15388
- if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
16192
+ if (!existsSync21(join29(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
16193
+ process.stdout.write(`behold demo carve \u2192 npm install ${HCL_PARSER_PKG} (chant's HCL parser, ~2MB, once)\u2026
16194
+ `);
16195
+ const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", HCL_PARSER_PKG], target);
16196
+ if (code !== 0) degraded = `couldn't install ${HCL_PARSER_PKG} (chant's HCL parser) \u2014 no network?`;
15389
16197
  }
15390
16198
  const report = at("carve-report.json");
15391
16199
  if (!degraded) {
@@ -15398,7 +16206,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
15398
16206
  );
15399
16207
  if (code !== 0) degraded = `chant carve advise exited ${code}`;
15400
16208
  }
15401
- const serving = !degraded && existsSync19(report) ? report : committed;
16209
+ const serving = !degraded && existsSync21(report) ? report : committed;
15402
16210
  if (degraded) {
15403
16211
  process.stderr.write(
15404
16212
  `behold demo carve: ${degraded}
@@ -15406,7 +16214,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
15406
16214
  `
15407
16215
  );
15408
16216
  }
15409
- if (!existsSync19(serving)) {
16217
+ if (!existsSync21(serving)) {
15410
16218
  process.stderr.write(`behold demo carve: no carve report at ${serving}
15411
16219
  `);
15412
16220
  process.exit(2);
@@ -15451,8 +16259,8 @@ async function runPreview(rest) {
15451
16259
  process.stderr.write("behold preview: --port must be a number\n");
15452
16260
  process.exit(2);
15453
16261
  }
15454
- const projectDir = resolve14(dirArg ?? process.cwd());
15455
- if (!existsSync19(projectDir)) {
16262
+ const projectDir = resolve17(dirArg ?? process.cwd());
16263
+ if (!existsSync21(projectDir)) {
15456
16264
  process.stderr.write(`behold preview: project not found at ${projectDir}
15457
16265
  `);
15458
16266
  process.exit(2);
@@ -15472,26 +16280,26 @@ async function runPreview(rest) {
15472
16280
  await startServer({ projectDir, port, env: "local", previewMode: true });
15473
16281
  }
15474
16282
  async function runExportCmd(rest) {
15475
- let outDir = resolve14("behold-export");
16283
+ let outDir = resolve17("behold-export");
15476
16284
  let env;
15477
16285
  let name;
15478
16286
  let dirArg;
15479
16287
  let emulator = false;
15480
16288
  for (let i = 0; i < rest.length; i++) {
15481
16289
  const a = rest[i];
15482
- if (a === "--out") outDir = resolve14(rest[++i]);
16290
+ if (a === "--out") outDir = resolve17(rest[++i]);
15483
16291
  else if (a === "--env") env = rest[++i];
15484
16292
  else if (a === "--name") name = rest[++i];
15485
16293
  else if (a === "--emulator") emulator = true;
15486
16294
  else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
15487
16295
  else if (!a.startsWith("-")) dirArg = a;
15488
16296
  }
15489
- const projectDir = resolve14(dirArg ?? process.cwd());
16297
+ const projectDir = resolve17(dirArg ?? process.cwd());
15490
16298
  if (emulator) {
15491
16299
  injectEmulatorEnv(env);
15492
16300
  env ??= "local";
15493
16301
  }
15494
- if (!existsSync19(projectDir)) {
16302
+ if (!existsSync21(projectDir)) {
15495
16303
  process.stderr.write(`behold export: project not found at ${projectDir}
15496
16304
  `);
15497
16305
  process.exit(2);