@intentius/behold 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -839,6 +839,31 @@ async function createK8sClient(options = {}) {
839
839
  target: refText(ref)
840
840
  });
841
841
  }
842
+ async function selfSubjectReview(signal) {
843
+ for (const version of SELF_SUBJECT_REVIEW_VERSIONS) {
844
+ const path = `/apis/authentication.k8s.io/${version}/selfsubjectreviews`;
845
+ let response;
846
+ try {
847
+ response = await sendJson(path, "POST", {
848
+ body: JSON.stringify({ apiVersion: `authentication.k8s.io/${version}`, kind: "SelfSubjectReview" }),
849
+ signal,
850
+ target: `self subject review (${version})`
851
+ });
852
+ } catch (err) {
853
+ if (err instanceof K8sApiError && err.notFound) continue;
854
+ throw err;
855
+ }
856
+ const user2 = response.status?.userInfo;
857
+ if (user2?.username) {
858
+ return {
859
+ username: user2.username,
860
+ ...user2.uid ? { uid: user2.uid } : {},
861
+ ...user2.groups && user2.groups.length > 0 ? { groups: user2.groups } : {}
862
+ };
863
+ }
864
+ }
865
+ return void 0;
866
+ }
842
867
  async function resourcesOf(apiVersion, signal) {
843
868
  const list4 = await apiResourceList(apiVersion, signal);
844
869
  if (!list4) return [];
@@ -858,6 +883,7 @@ async function createK8sClient(options = {}) {
858
883
  concurrently: (items, fn) => mapConcurrent(items, fn, concurrency),
859
884
  resources: resourcesOf,
860
885
  preferredGroupVersions,
886
+ selfSubjectReview,
861
887
  discoveryCacheKeys: () => [...discoveryCache.keys()].sort()
862
888
  };
863
889
  }
@@ -874,7 +900,7 @@ function selectorText(selector) {
874
900
  function refText(ref) {
875
901
  return `${ref.apiVersion} ${ref.kind} ${ref.namespace ? `${ref.namespace}/` : ""}${ref.name}`;
876
902
  }
877
- var clientNodeModule;
903
+ var clientNodeModule, SELF_SUBJECT_REVIEW_VERSIONS;
878
904
  var init_client = __esm({
879
905
  "node_modules/@intentius/chant-k8s-client/src/client.ts"() {
880
906
  init_errors();
@@ -883,6 +909,7 @@ var init_client = __esm({
883
909
  init_field_manager();
884
910
  init_concurrency();
885
911
  init_kubeconfig();
912
+ SELF_SUBJECT_REVIEW_VERSIONS = ["v1", "v1beta1", "v1alpha1"];
886
913
  }
887
914
  });
888
915
 
@@ -2283,1110 +2310,1679 @@ function joinCiProgress(ir, state) {
2283
2310
  return ir;
2284
2311
  }
2285
2312
 
2286
- // src/component-status.ts
2287
- function componentStatusColor(row) {
2288
- if (row.stack && row.stack.healthy === false) {
2289
- if (row.stack.status && /ROLLBACK|FAILED/i.test(row.stack.status)) return "warn";
2290
- return "accent";
2291
- }
2292
- const rollup = row.resources;
2293
- if (rollup && rollup.total > 0) {
2294
- if (rollup.unobserved > 0) return "accent";
2295
- if (rollup.present === rollup.total) return "good";
2296
- if (rollup.present === 0) return "neutral";
2297
- return "warn";
2298
- }
2299
- if (row.live !== void 0) return row.live ? "good" : "neutral";
2300
- switch (row.reconciliation) {
2301
- case "reconciled":
2302
- return "good";
2303
- case "unrecorded":
2304
- return row.detail.startsWith("live") ? "good" : "neutral";
2305
- case "stale":
2306
- case "drifted":
2307
- return "warn";
2308
- case "unknown":
2309
- default:
2310
- return "neutral";
2313
+ // src/operator.ts
2314
+ var ANSI = /\x1b\[[0-9;]*m/g;
2315
+ var strip = (s) => s.replace(ANSI, "");
2316
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2317
+ var HOW_TO_DECLARE = "An operating loop is a ConvergeOp: declare one in a `*.op.ts` (`ConvergeOp({ name, env, dial, rules })` from @intentius/chant-lexicon-temporal), build the project, and tick it with `chant operator` (or a cron / CronJob invoking it).";
2318
+ function readRow(value) {
2319
+ if (!isRecord(value)) return null;
2320
+ if (typeof value.op !== "string" || typeof value.env !== "string") return null;
2321
+ if (!Array.isArray(value.pendingGates)) return null;
2322
+ const gates = [];
2323
+ for (const g of value.pendingGates) {
2324
+ if (!isRecord(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
2325
+ gates.push({
2326
+ rule: g.rule,
2327
+ gate: g.gate,
2328
+ ...typeof g.op === "string" ? { op: g.op } : {},
2329
+ // chant#2028's address, carried through verbatim and only when chant sent
2330
+ // one. The scheme check happens where the link is MINTED
2331
+ // ({@link approvalLink}), not here, so a row whose url behold won't link
2332
+ // still parses rather than refusing the whole strip.
2333
+ ...typeof g.url === "string" && g.url ? { url: g.url } : {}
2334
+ });
2311
2335
  }
2312
- }
2313
- function joinComponentStatus(ir, rows) {
2314
- const byComponent = new Map(rows.map((r) => [r.component, r]));
2336
+ const tick = isRecord(value.lastTick) ? value.lastTick : void 0;
2337
+ const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
2338
+ const lease = isRecord(value.lease) ? value.lease : void 0;
2315
2339
  return {
2316
- ...ir,
2317
- nodes: ir.nodes.map((n) => {
2318
- const row = byComponent.get(n.id);
2319
- if (!row) return n;
2340
+ op: value.op,
2341
+ env: value.env,
2342
+ ...usable ? { lastTick: tick } : {},
2343
+ pendingGates: gates,
2344
+ ...lease && typeof lease.holder === "string" && typeof lease.expiresAt === "string" ? { lease: { holder: lease.holder, expiresAt: lease.expiresAt } } : {}
2345
+ };
2346
+ }
2347
+ function readOperatorStatus(run3) {
2348
+ const err = strip(run3.stderr);
2349
+ const out = strip(run3.stdout).trim();
2350
+ if (run3.code !== 0) {
2351
+ const message = err.trim() || out || `chant operator status exited ${run3.code}`;
2352
+ const first = message.split("\n")[0].replace(/^error:\s*/i, "");
2353
+ if (/Unknown command|Unknown operator subcommand/i.test(message)) {
2320
2354
  return {
2321
- ...n,
2322
- attrs: {
2323
- ...n.attrs,
2324
- _status: componentStatusColor(row),
2325
- _liveStatus: {
2326
- reconciliation: row.reconciliation,
2327
- detail: row.detail,
2328
- ...row.live !== void 0 ? { live: row.live } : {},
2329
- ...row.stack ? { stack: row.stack } : {},
2330
- ...row.resources ? { resources: row.resources } : {}
2331
- }
2355
+ ok: false,
2356
+ refusal: {
2357
+ error: `This chant has no \`operator status\` \u2014 ${first}`,
2358
+ code: "no-operator-cli",
2359
+ remedy: "The operating loop's status surface landed in chant 0.52 (chant#1485). Upgrade chant to read it."
2332
2360
  }
2333
2361
  };
2334
- })
2335
- };
2336
- }
2337
-
2338
- // src/overlay.ts
2339
- function pruneImports(ir) {
2340
- const drop = new Set((ir.imports ?? []).map((i) => i.node));
2341
- if (drop.size === 0) return ir;
2342
- ir.nodes = ir.nodes.filter((n) => !drop.has(n.id));
2343
- if (ir.edges) ir.edges = ir.edges.filter((e) => !drop.has(e.from) && !drop.has(e.to));
2344
- return ir;
2345
- }
2346
- function componentDir(node) {
2347
- const parts = node.sourceLoc?.file?.split("/") ?? [];
2348
- if (parts[0] !== "src" || parts[1] === "examples" || parts.length < 3) return void 0;
2349
- return parts[1];
2350
- }
2351
- function reclassifyOverlay(ir) {
2352
- const deployedComponents = /* @__PURE__ */ new Set();
2353
- for (const n of ir.nodes) {
2354
- if (n.attrs?._status === "good") {
2355
- const c = componentDir(n);
2356
- if (c) deployedComponents.add(c);
2357
2362
  }
2363
+ return {
2364
+ ok: false,
2365
+ refusal: {
2366
+ error: `chant operator status failed: ${first}`,
2367
+ code: "operator-status",
2368
+ remedy: "Run `chant operator status --json` in the project to see the whole error."
2369
+ }
2370
+ };
2358
2371
  }
2359
- for (const n of ir.nodes) {
2360
- const file = n.sourceLoc?.file ?? "";
2361
- if (file.startsWith("src/examples/")) {
2362
- n.attrs = { ...n.attrs, _byo: true };
2363
- delete n.attrs._status;
2364
- } else if (n.kind === "AWS::CloudFormation::Parameter") {
2365
- const c = componentDir(n);
2366
- if (c && deployedComponents.has(c)) {
2367
- n.attrs = { ...n.attrs, _status: "good" };
2372
+ if (!out) {
2373
+ return {
2374
+ ok: false,
2375
+ refusal: {
2376
+ error: "This project declares no operating loop \u2014 chant found no ConvergeOp.",
2377
+ code: "no-operator",
2378
+ remedy: HOW_TO_DECLARE
2379
+ }
2380
+ };
2381
+ }
2382
+ let parsed;
2383
+ try {
2384
+ parsed = JSON.parse(out);
2385
+ } catch (e) {
2386
+ return {
2387
+ ok: false,
2388
+ refusal: {
2389
+ error: `chant operator status --json didn't print JSON: ${e instanceof Error ? e.message : String(e)}`,
2390
+ code: "operator-status",
2391
+ remedy: "Run `chant operator status --json` in the project and check what it printed."
2392
+ }
2393
+ };
2394
+ }
2395
+ if (!Array.isArray(parsed)) {
2396
+ return {
2397
+ ok: false,
2398
+ refusal: {
2399
+ error: "chant operator status --json answered with something that isn't an array of ConvergeOp rows.",
2400
+ code: "operator-status",
2401
+ remedy: "behold reads one row per ConvergeOp: `{op, env, lastTick?, pendingGates[], lease?}`."
2368
2402
  }
2403
+ };
2404
+ }
2405
+ const rows = [];
2406
+ for (const value of parsed) {
2407
+ const row = readRow(value);
2408
+ if (!row) {
2409
+ return {
2410
+ ok: false,
2411
+ refusal: {
2412
+ error: "chant operator status --json answered with a row behold couldn't read (needs `op`, `env`, `pendingGates[]`).",
2413
+ code: "operator-status",
2414
+ remedy: "Check `chant operator status --json` in the project \u2014 and file the shape change if chant's is the newer one."
2415
+ }
2416
+ };
2369
2417
  }
2418
+ rows.push(row);
2370
2419
  }
2371
- return ir;
2420
+ return { ok: true, rows };
2372
2421
  }
2373
- function pruneRuntimeChildren(ir) {
2374
- const runtime = new Set(ir.nodes.filter((n) => n.runtimeOwner).map((n) => n.id));
2375
- if (runtime.size === 0) return ir;
2376
- ir.nodes = ir.nodes.filter((n) => !runtime.has(n.id));
2377
- ir.edges = ir.edges.filter((e) => !runtime.has(e.from) && !runtime.has(e.to));
2378
- return ir;
2422
+ function declaredConvergeOps(ops) {
2423
+ return ops.filter((o) => o.searchAttributes?.Converge === "true").map((o) => ({
2424
+ name: o.name,
2425
+ ...o.searchAttributes?.Env ? { env: o.searchAttributes.Env } : {},
2426
+ ...o.searchAttributes?.Dial ? { dial: o.searchAttributes.Dial } : {}
2427
+ }));
2379
2428
  }
2380
- function attachRuntimeContainment(ir) {
2381
- const byOwner = /* @__PURE__ */ new Map();
2429
+ var MANAGED_BY = "app.kubernetes.io/managed-by";
2430
+ var COMPONENT = "app.kubernetes.io/component";
2431
+ var NAME_LABEL = "app.kubernetes.io/name";
2432
+ var CHANT = "chant";
2433
+ var CONVERGE_TICK = "converge-tick";
2434
+ var CRONJOB_KIND = "K8s::Batch::CronJob";
2435
+ var NAMESPACE_KIND = "K8s::Core::Namespace";
2436
+ var rec = (v) => isRecord(v) ? v : void 0;
2437
+ var str = (v) => typeof v === "string" && v ? v : void 0;
2438
+ var labelsOf = (n) => rec(rec(n.attrs?.metadata)?.labels) ?? {};
2439
+ function operatorHomes(ir) {
2440
+ const byNamespace = /* @__PURE__ */ new Map();
2441
+ const conflicted = /* @__PURE__ */ new Set();
2382
2442
  for (const n of ir.nodes) {
2383
- if (!n.runtimeOwner) continue;
2384
- if (!byOwner.has(n.runtimeOwner)) byOwner.set(n.runtimeOwner, /* @__PURE__ */ new Set());
2385
- byOwner.get(n.runtimeOwner).add(n.id);
2443
+ if (n.kind !== CRONJOB_KIND) continue;
2444
+ const labels = labelsOf(n);
2445
+ if (labels[MANAGED_BY] !== CHANT || labels[COMPONENT] !== CONVERGE_TICK) continue;
2446
+ const meta = rec(n.attrs?.metadata);
2447
+ const ns = str(meta?.namespace);
2448
+ const op = str(meta?.name);
2449
+ if (!ns || !op) continue;
2450
+ const home = byNamespace.get(ns) ?? { namespace: ns, ticks: [] };
2451
+ const stack = str(labels[NAME_LABEL]);
2452
+ if (stack && !conflicted.has(ns)) {
2453
+ if (home.stack === void 0) home.stack = stack;
2454
+ else if (home.stack !== stack) {
2455
+ conflicted.add(ns);
2456
+ delete home.stack;
2457
+ }
2458
+ }
2459
+ const schedule = str(rec(n.attrs?.spec)?.schedule);
2460
+ home.ticks.push({ node: n.id, op, ...schedule ? { schedule } : {} });
2461
+ byNamespace.set(ns, home);
2386
2462
  }
2387
- if (byOwner.size === 0) return ir;
2388
- const byContainer = { ...ir.groups.byContainer ?? {} };
2389
- for (const [owner, children] of byOwner) {
2390
- byContainer[owner] = [.../* @__PURE__ */ new Set([owner, ...byContainer[owner] ?? [], ...children])].sort();
2463
+ if (!byNamespace.size) return [];
2464
+ for (const n of ir.nodes) {
2465
+ if (n.kind !== NAMESPACE_KIND) continue;
2466
+ const name = str(rec(n.attrs?.metadata)?.name);
2467
+ const home = name ? byNamespace.get(name) : void 0;
2468
+ if (home && !home.node) home.node = n.id;
2391
2469
  }
2392
- ir.groups.byContainer = byContainer;
2393
- return ir;
2394
- }
2395
-
2396
- // src/value-match.ts
2397
- function typeSegment(kind) {
2398
- const parts = kind.split(kind.includes("::") ? "::" : "/");
2399
- return parts[parts.length - 1] ?? kind;
2400
- }
2401
- function isOwnNameAttr(kind, key) {
2402
- if (!/name$/i.test(key)) return false;
2403
- const prefix = key.slice(0, -4).toLowerCase();
2404
- if (prefix === "") return true;
2405
- return typeSegment(kind).toLowerCase().includes(prefix);
2406
- }
2407
- var MIN_NAME_LEN = 6;
2408
- var MAX_NAME_LEN = 200;
2409
- var MAX_DEPTH = 12;
2410
- var SKIP_KEYS = /* @__PURE__ */ new Set([
2411
- "status",
2412
- "annotations",
2413
- "labels",
2414
- "matchLabels",
2415
- "managedFields",
2416
- "ownerReferences",
2417
- "creationTimestamp",
2418
- "resourceVersion",
2419
- "uid",
2420
- "selfLink",
2421
- "generation",
2422
- "finalizers"
2423
- ]);
2424
- function joinable(v) {
2425
- return v.length >= MIN_NAME_LEN && v.length <= MAX_NAME_LEN && !/\s/.test(v);
2470
+ for (const home of byNamespace.values()) home.ticks.sort((a, b) => a.op.localeCompare(b.op));
2471
+ return [...byNamespace.values()].sort((a, b) => a.namespace.localeCompare(b.namespace));
2426
2472
  }
2427
- function stringLeaves(attrs) {
2428
- const out = [];
2429
- const onPath = /* @__PURE__ */ new Set();
2430
- const step = (v, key, path, depth) => {
2431
- if (typeof v === "string") {
2432
- out.push({ path, key, value: v });
2433
- return;
2473
+ function markOperatorHome(ir) {
2474
+ const homes = operatorHomes(ir);
2475
+ if (!homes.length) return ir;
2476
+ const marks = /* @__PURE__ */ new Map();
2477
+ for (const home of homes) {
2478
+ if (home.node) {
2479
+ marks.set(home.node, {
2480
+ role: "home",
2481
+ namespace: home.namespace,
2482
+ ...home.stack ? { stack: home.stack } : {},
2483
+ ticks: home.ticks.map((t) => t.op)
2484
+ });
2434
2485
  }
2435
- if (depth >= MAX_DEPTH || !v || typeof v !== "object") return;
2436
- if (onPath.has(v)) return;
2437
- onPath.add(v);
2438
- if (Array.isArray(v)) {
2439
- for (const item of v) step(item, key, path, depth + 1);
2440
- } else {
2441
- for (const [k, val] of Object.entries(v)) {
2442
- if (k.startsWith("_") || SKIP_KEYS.has(k)) continue;
2443
- step(val, k, path ? `${path}.${k}` : k, depth + 1);
2444
- }
2486
+ for (const tick of home.ticks) {
2487
+ marks.set(tick.node, {
2488
+ role: "tick",
2489
+ namespace: home.namespace,
2490
+ ...home.stack ? { stack: home.stack } : {},
2491
+ op: tick.op,
2492
+ ...tick.schedule ? { schedule: tick.schedule } : {}
2493
+ });
2445
2494
  }
2446
- onPath.delete(v);
2447
- };
2448
- for (const [k, v] of Object.entries(attrs ?? {})) {
2449
- if (k.startsWith("_") || SKIP_KEYS.has(k)) continue;
2450
- step(v, k, k, 0);
2451
2495
  }
2452
- return out;
2496
+ return {
2497
+ ...ir,
2498
+ nodes: ir.nodes.map((n) => {
2499
+ const mark = marks.get(n.id);
2500
+ return mark ? { ...n, attrs: { ...n.attrs, _operator: mark } } : n;
2501
+ })
2502
+ };
2453
2503
  }
2454
- function addValueMatchEdges(ir) {
2455
- const nodes = ir.nodes;
2456
- const owner = /* @__PURE__ */ new Map();
2457
- for (const n of nodes) {
2458
- for (const { path, key, value } of stringLeaves(n.attrs)) {
2459
- if (path !== key && path !== `metadata.${key}`) continue;
2460
- if (!joinable(value) || !isOwnNameAttr(n.kind, key)) continue;
2461
- owner.set(value, owner.has(value) ? null : n.id);
2462
- }
2463
- }
2464
- if (owner.size === 0) return ir;
2465
- const declared = /* @__PURE__ */ new Set();
2466
- for (const e of ir.edges) declared.add(`${e.from}\0${e.to}`);
2467
- const added = /* @__PURE__ */ new Set();
2468
- for (const a of nodes) {
2469
- for (const { path, value } of stringLeaves(a.attrs)) {
2470
- if (!joinable(value)) continue;
2471
- const b = owner.get(value);
2472
- if (!b || b === a.id) continue;
2473
- const key = `${a.id}\0${b}`;
2474
- if (declared.has(key) || added.has(key)) continue;
2475
- added.add(key);
2476
- ir.edges.push({ from: a.id, to: b, kind: "ref", viaAttr: path, inferred: true });
2477
- }
2504
+ var OPERATOR_HOME_GLYPH = {
2505
+ body: '<path d="M16.5 6.63A7 7 0 1 1 6.64 7.5"/><path d="M16.5 6.63 14.3 5.9M16.5 6.63 15.9 8.1"/>'
2506
+ };
2507
+ function operatorHomeBoxMarks(ir, namespaceBoxes) {
2508
+ const out = {};
2509
+ for (const home of operatorHomes(ir)) {
2510
+ const box = namespaceBoxes[home.namespace];
2511
+ if (box) out[box] = OPERATOR_HOME_GLYPH;
2478
2512
  }
2479
- return ir;
2480
- }
2481
-
2482
- // src/k8s-edges.ts
2483
- var WORKLOAD_KINDS = /* @__PURE__ */ new Set([
2484
- "K8s::Apps::Deployment",
2485
- "K8s::Apps::StatefulSet",
2486
- "K8s::Apps::DaemonSet",
2487
- "K8s::Apps::ReplicaSet"
2488
- ]);
2489
- var SELECTOR_CARRIERS = [
2490
- { kind: "K8s::Core::Service", read: (spec) => spec?.selector, viaAttr: "selector" },
2491
- { kind: "K8s::Policy::PodDisruptionBudget", read: (spec) => labelSelector(spec?.selector), viaAttr: "selector" },
2492
- { kind: "K8s::Networking::NetworkPolicy", read: (spec) => labelSelector(spec?.podSelector), viaAttr: "podSelector" }
2493
- ];
2494
- function rec(v) {
2495
- return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
2496
- }
2497
- function str(v) {
2498
- return typeof v === "string" && v.length > 0 ? v : void 0;
2513
+ return out;
2499
2514
  }
2500
- function namespaceOf(n) {
2501
- return str(rec(n.attrs?.metadata)?.namespace) ?? "default";
2515
+ function operatorStrip(rows, now = Date.now()) {
2516
+ const out = rows.map((r) => {
2517
+ const expiresAt = r.lease ? Date.parse(r.lease.expiresAt) : NaN;
2518
+ const lease = !r.lease ? "free" : Number.isFinite(expiresAt) && expiresAt <= now ? "expired" : "held";
2519
+ return {
2520
+ op: r.op,
2521
+ env: r.env,
2522
+ log: r.lastTick?.log ?? null,
2523
+ at: r.lastTick?.timestamp ?? null,
2524
+ tickId: typeof r.lastTick?.id === "string" && r.lastTick.id ? r.lastTick.id : null,
2525
+ lease,
2526
+ leaseHolder: r.lease?.holder ?? null,
2527
+ leaseExpiresAt: r.lease?.expiresAt ?? null,
2528
+ pendingGates: r.pendingGates.length
2529
+ };
2530
+ });
2531
+ return { rows: out, pendingGates: out.reduce((n, r) => n + r.pendingGates, 0) };
2502
2532
  }
2503
- function nameOf(n) {
2504
- return str(rec(n.attrs?.metadata)?.name);
2533
+ var OPERATOR_INTERVAL_MS = 6e4;
2534
+ var TICK_VERDICT_TTL_MS = 15 * OPERATOR_INTERVAL_MS;
2535
+ function tickFreshness(at, now = Date.now()) {
2536
+ const t = at ? Date.parse(at) : NaN;
2537
+ if (!Number.isFinite(t)) return { ageMs: null, fresh: false };
2538
+ const age = now - t;
2539
+ if (age < 0) return { ageMs: null, fresh: false };
2540
+ return { ageMs: age, fresh: age <= TICK_VERDICT_TTL_MS };
2541
+ }
2542
+ var RECONCILIATIONS = /* @__PURE__ */ new Set(["reconciled", "unrecorded", "stale", "drifted", "unknown"]);
2543
+ function readVerdict(value) {
2544
+ if (!isRecord(value)) return null;
2545
+ if (typeof value.component !== "string" || !value.component) return null;
2546
+ if (typeof value.reconciliation !== "string" || !RECONCILIATIONS.has(value.reconciliation)) return null;
2547
+ const unobserved = isRecord(value.unobserved) && typeof value.unobserved.reason === "string" ? value.unobserved : void 0;
2548
+ return {
2549
+ component: value.component,
2550
+ reconciliation: value.reconciliation,
2551
+ detail: typeof value.detail === "string" ? value.detail : "",
2552
+ ...typeof value.live === "boolean" ? { live: value.live } : {},
2553
+ ...unobserved ? {
2554
+ unobserved: {
2555
+ reason: unobserved.reason,
2556
+ ...typeof unobserved.detail === "string" ? { detail: unobserved.detail } : {}
2557
+ }
2558
+ } : {}
2559
+ };
2505
2560
  }
2506
- function labelRecord(v) {
2507
- const r = rec(v);
2508
- if (!r) return void 0;
2509
- const out = {};
2510
- for (const [k, val] of Object.entries(r)) {
2511
- if (typeof val !== "string") return void 0;
2512
- out[k] = val;
2561
+ function tickComponentVerdicts(rows) {
2562
+ const out = [];
2563
+ for (const row of rows) {
2564
+ const tick = row.lastTick;
2565
+ if (!tick || !Array.isArray(tick.components)) continue;
2566
+ const verdicts = [];
2567
+ for (const value of tick.components) {
2568
+ const verdict = readVerdict(value);
2569
+ if (verdict) verdicts.push(verdict);
2570
+ }
2571
+ out.push({
2572
+ op: row.op,
2573
+ env: tick.env || row.env,
2574
+ at: tick.timestamp,
2575
+ tickId: typeof tick.id === "string" && tick.id ? tick.id : null,
2576
+ verdicts
2577
+ });
2513
2578
  }
2514
- return Object.keys(out).length > 0 ? out : void 0;
2579
+ return out;
2515
2580
  }
2516
- function labelSelector(v) {
2517
- const sel = rec(v);
2518
- if (!sel) return void 0;
2519
- const expressions = sel.matchExpressions;
2520
- if (Array.isArray(expressions) && expressions.length > 0) return void 0;
2521
- return sel.matchLabels;
2581
+ function verdictsForEnv(all, env) {
2582
+ const when = (t) => {
2583
+ const ms = Date.parse(t.at);
2584
+ return Number.isFinite(ms) ? ms : -Infinity;
2585
+ };
2586
+ let best = null;
2587
+ for (const t of all) {
2588
+ if (t.env !== env) continue;
2589
+ if (!best || when(t) > when(best)) best = t;
2590
+ }
2591
+ return best;
2522
2592
  }
2523
- function selects(selector, labels) {
2524
- return Object.entries(selector).every(([k, v]) => labels[k] === v);
2593
+ var CONVERGE_GATE_LOOP = "converge";
2594
+ var APPROVE_SEMANTICS = "Approving records a fact in the gate ledger; it does not unblock the dispatch. The next tick reads it.";
2595
+ function approvalLink(url) {
2596
+ if (!url) return void 0;
2597
+ try {
2598
+ const parsed = new URL(url);
2599
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? url : void 0;
2600
+ } catch {
2601
+ return void 0;
2602
+ }
2525
2603
  }
2526
- function templateLabels(n) {
2527
- const template = rec(rec(n.attrs?.spec)?.template);
2528
- return labelRecord(rec(template?.metadata)?.labels);
2604
+ function convergeGateCards(rows) {
2605
+ const out = [];
2606
+ for (const row of rows) {
2607
+ for (const g of row.pendingGates) {
2608
+ if (!g.op) continue;
2609
+ out.push({
2610
+ loop: CONVERGE_GATE_LOOP,
2611
+ rule: g.rule,
2612
+ op: g.op,
2613
+ gate: g.gate,
2614
+ convergeOp: row.op,
2615
+ env: row.env,
2616
+ since: row.lastTick?.timestamp ?? null,
2617
+ semantics: APPROVE_SEMANTICS,
2618
+ approve: {
2619
+ method: "POST",
2620
+ path: `/api/operator/approve/${encodeURIComponent(g.op)}/${encodeURIComponent(g.gate)}`,
2621
+ command: `chant approve ${g.op} ${g.gate}`
2622
+ },
2623
+ ...approvalLink(g.url) ? { url: g.url } : {}
2624
+ });
2625
+ }
2626
+ }
2627
+ return out;
2529
2628
  }
2530
- function shortKind(kind) {
2531
- const parts = kind.split("::");
2532
- return parts[parts.length - 1] ?? kind;
2629
+ var OPERATOR_LOG_FLOOR = "0.53.1";
2630
+ var OPERATOR_LOG_LIMIT = 50;
2631
+ var OPERATOR_LOG_MAX_LIMIT = 200;
2632
+ function operatorLogWindow(raw) {
2633
+ let limit = OPERATOR_LOG_LIMIT;
2634
+ if (raw.limit !== void 0 && raw.limit !== null && raw.limit !== "") {
2635
+ const n = Number(raw.limit);
2636
+ if (!Number.isInteger(n) || n < 1) return { error: `limit must be a positive integer (got "${raw.limit}")` };
2637
+ limit = Math.min(n, OPERATOR_LOG_MAX_LIMIT);
2638
+ }
2639
+ if (raw.since !== void 0 && raw.since !== null && raw.since !== "") {
2640
+ if (Number.isNaN(new Date(raw.since).getTime())) {
2641
+ return { error: `since must be an ISO-8601 timestamp (got "${raw.since}")` };
2642
+ }
2643
+ return { limit, since: raw.since };
2644
+ }
2645
+ return { limit };
2533
2646
  }
2534
- var FLUX_SOURCE_KINDS = /* @__PURE__ */ new Set([
2535
- "K8s::Flux::GitRepository",
2536
- "K8s::Flux::OCIRepository",
2537
- "K8s::Flux::HelmRepository",
2538
- "K8s::Flux::HelmChart",
2539
- "K8s::Flux::Bucket"
2540
- ]);
2541
- function sourceRefOf(refObj, referrer, defaultKind) {
2542
- const ref = rec(refObj);
2543
- const kind = str(ref?.kind) ?? defaultKind;
2544
- const name = str(ref?.name);
2545
- if (!kind || !name) return void 0;
2546
- return { kind, name, namespace: str(ref?.namespace) ?? namespaceOf(referrer) };
2647
+ function operatorLogArgs(window) {
2648
+ return [
2649
+ "operator",
2650
+ "log",
2651
+ "--json",
2652
+ "--limit",
2653
+ String(window.limit),
2654
+ ...window.since ? ["--since", window.since] : []
2655
+ ];
2547
2656
  }
2548
- function namedRefOf(refObj, referrer) {
2549
- const ref = rec(refObj);
2550
- const name = str(ref?.name);
2551
- if (!name) return void 0;
2552
- return { name, namespace: str(ref?.namespace) ?? namespaceOf(referrer) };
2657
+ function readTickRecord(value) {
2658
+ if (!isRecord(value)) return null;
2659
+ if (typeof value.op !== "string" || typeof value.timestamp !== "string" || typeof value.log !== "string") return null;
2660
+ return value;
2553
2661
  }
2554
- function list(v) {
2555
- return Array.isArray(v) ? v : [];
2662
+ function readResolutionRecord(value) {
2663
+ if (!isRecord(value)) return null;
2664
+ const needed = ["op", "gate", "resolvedBy", "timestamp"];
2665
+ if (needed.some((k) => typeof value[k] !== "string")) return null;
2666
+ return value;
2556
2667
  }
2557
- function helmReleaseSourceRef(n) {
2558
- return rec(rec(rec(n.attrs?.spec)?.chart)?.spec)?.sourceRef;
2668
+ function readOperatorLog(run3) {
2669
+ const err = strip(run3.stderr);
2670
+ const out = strip(run3.stdout).trim();
2671
+ if (run3.code !== 0) {
2672
+ const message = err.trim() || out || `chant operator log exited ${run3.code}`;
2673
+ const first = message.split("\n")[0].replace(/^error:\s*/i, "");
2674
+ if (/Unknown command|Unknown operator subcommand|Unknown flag/i.test(message)) {
2675
+ return {
2676
+ ok: false,
2677
+ refusal: {
2678
+ error: `This chant has no \`operator log\` \u2014 ${first}`,
2679
+ code: "no-operator-log-cli",
2680
+ remedy: `The converge tick history landed in chant ${OPERATOR_LOG_FLOOR} (chant#2029). Upgrade chant to read it; the strip above needs only 0.52.`
2681
+ }
2682
+ };
2683
+ }
2684
+ return {
2685
+ ok: false,
2686
+ refusal: {
2687
+ error: `chant operator log failed: ${first}`,
2688
+ code: "operator-log",
2689
+ remedy: "Run `chant operator log --json` in the project to see the whole error."
2690
+ }
2691
+ };
2692
+ }
2693
+ if (!out) {
2694
+ return {
2695
+ ok: false,
2696
+ refusal: {
2697
+ error: "This project declares no operating loop \u2014 chant found no ConvergeOp.",
2698
+ code: "no-operator",
2699
+ remedy: HOW_TO_DECLARE
2700
+ }
2701
+ };
2702
+ }
2703
+ let parsed;
2704
+ try {
2705
+ parsed = JSON.parse(out);
2706
+ } catch (e) {
2707
+ return {
2708
+ ok: false,
2709
+ refusal: {
2710
+ error: `chant operator log --json didn't print JSON: ${e instanceof Error ? e.message : String(e)}`,
2711
+ code: "operator-log",
2712
+ remedy: "Run `chant operator log --json` in the project and check what it printed."
2713
+ }
2714
+ };
2715
+ }
2716
+ const unreadable = (why) => ({
2717
+ ok: false,
2718
+ refusal: {
2719
+ error: `chant operator log --json answered with something behold couldn't read: ${why}`,
2720
+ code: "operator-log",
2721
+ remedy: "behold reads `{entries: [{kind, timestamp, record}], malformed: {converge, gates}}`."
2722
+ }
2723
+ });
2724
+ if (!isRecord(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
2725
+ const entries = [];
2726
+ for (const value of parsed.entries) {
2727
+ if (!isRecord(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
2728
+ if (value.kind === "tick") {
2729
+ const record = readTickRecord(value.record);
2730
+ if (!record) return unreadable("a tick entry with no `op`/`timestamp`/`log`");
2731
+ entries.push({ kind: "tick", timestamp: value.timestamp, record });
2732
+ } else if (value.kind === "gate-resolution") {
2733
+ const record = readResolutionRecord(value.record);
2734
+ if (!record) return unreadable("a gate-resolution entry with no `op`/`gate`/`resolvedBy`/`timestamp`");
2735
+ entries.push({ kind: "gate-resolution", timestamp: value.timestamp, record });
2736
+ } else {
2737
+ return unreadable(`an entry of an unknown kind "${String(value.kind)}"`);
2738
+ }
2739
+ }
2740
+ const counts = isRecord(parsed.malformed) ? parsed.malformed : {};
2741
+ const count = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
2742
+ return { ok: true, entries, malformed: { converge: count(counts.converge), gates: count(counts.gates) } };
2559
2743
  }
2560
- function ingressBackendServices(n) {
2561
- const spec = rec(n.attrs?.spec);
2562
- const out = [];
2563
- const backendName = (b) => str(rec(rec(b)?.service)?.name);
2564
- const fromDefault = backendName(spec?.defaultBackend);
2565
- if (fromDefault) out.push(fromDefault);
2566
- const rules = Array.isArray(spec?.rules) ? spec.rules : [];
2567
- for (const rule of rules) {
2568
- const paths = rec(rec(rule)?.http)?.paths;
2569
- for (const p of Array.isArray(paths) ? paths : []) {
2570
- const name = backendName(rec(p)?.backend);
2571
- if (name) out.push(name);
2744
+ function operatorTimeline(entries) {
2745
+ return entries.map((entry) => {
2746
+ if (entry.kind === "gate-resolution") {
2747
+ const r = entry.record;
2748
+ return {
2749
+ kind: "gate-resolution",
2750
+ at: r.timestamp,
2751
+ op: r.op,
2752
+ gate: r.gate,
2753
+ resolvedBy: r.resolvedBy,
2754
+ note: r.note ?? null,
2755
+ url: approvalLink(r.url) ?? null
2756
+ };
2572
2757
  }
2758
+ const t = entry.record;
2759
+ return {
2760
+ kind: "tick",
2761
+ at: t.timestamp,
2762
+ op: t.op,
2763
+ env: typeof t.env === "string" ? t.env : null,
2764
+ id: typeof t.id === "string" && t.id ? t.id : null,
2765
+ log: t.log,
2766
+ gated: (Array.isArray(t.outcomes) ? t.outcomes : []).filter((o) => o && o.action === "gated" && typeof o.gateName === "string").map((o) => ({
2767
+ rule: o.ruleId,
2768
+ op: typeof o.op === "string" ? o.op : null,
2769
+ gate: o.gateName,
2770
+ url: approvalLink(o.url) ?? null
2771
+ }))
2772
+ };
2773
+ });
2774
+ }
2775
+ function mergeOperatorLogs(answers, limit) {
2776
+ const malformed = answers.reduce(
2777
+ (acc, a) => ({ converge: acc.converge + a.malformed.converge, gates: acc.gates + a.malformed.gates }),
2778
+ { converge: 0, gates: 0 }
2779
+ );
2780
+ if (answers.length === 1) {
2781
+ const only = answers[0].entries;
2782
+ return { entries: only.length > limit ? only.slice(-limit) : only, malformed };
2573
2783
  }
2574
- return out;
2784
+ const all = answers.flatMap((a) => a.entries).sort((a, b) => {
2785
+ const delta = new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
2786
+ return delta !== 0 ? delta : a.kind === b.kind ? 0 : a.kind === "tick" ? -1 : 1;
2787
+ });
2788
+ return { entries: all.length > limit ? all.slice(-limit) : all, malformed };
2575
2789
  }
2576
- function deriveK8sEdges(nodes) {
2577
- const k8s = nodes.filter((n) => n.lexicon === "k8s");
2578
- if (k8s.length === 0) return [];
2579
- const out = [];
2580
- const add = (from, to, viaAttr) => {
2581
- if (from !== to) out.push({ from, to, kind: "ref", viaAttr, inferred: true });
2582
- };
2583
- const workloads = k8s.filter((n) => WORKLOAD_KINDS.has(n.kind));
2584
- const byKindNsName = /* @__PURE__ */ new Map();
2585
- const appProjectsByName = /* @__PURE__ */ new Map();
2586
- for (const n of k8s) {
2587
- const name = nameOf(n);
2588
- if (!name) continue;
2589
- byKindNsName.set(`${n.kind}\0${namespaceOf(n)}\0${name}`, n);
2590
- if (n.kind === "K8s::Argo::AppProject") {
2591
- const same = appProjectsByName.get(name) ?? [];
2592
- same.push(n);
2593
- appProjectsByName.set(name, same);
2594
- }
2790
+ var initialOperatorState = {
2791
+ declared: [],
2792
+ strip: null,
2793
+ gates: [],
2794
+ verdicts: [],
2795
+ note: null,
2796
+ code: null,
2797
+ readAt: null
2798
+ };
2799
+ function operatorRead(state, result, at, now = Date.now()) {
2800
+ if (!result.ok) {
2801
+ return { ...state, strip: null, gates: [], verdicts: [], note: result.refusal.error, code: result.refusal.code, readAt: at };
2595
2802
  }
2596
- const lookup = (kind, namespace, name) => byKindNsName.get(`${kind}\0${namespace}\0${name}`);
2597
- const addFluxRef = (n, refObj, viaAttr, allowed, defaultKind) => {
2598
- const ref = sourceRefOf(refObj, n, defaultKind);
2599
- if (!ref || ref.name === "*") return;
2600
- const kind = `K8s::Flux::${ref.kind}`;
2601
- if (allowed && !allowed.has(kind)) return;
2602
- const target = lookup(kind, ref.namespace, ref.name);
2603
- if (target) add(n.id, target.id, viaAttr);
2604
- };
2605
- const addSourceRef = (n, refObj, viaAttr, defaultKind) => addFluxRef(n, refObj, viaAttr, FLUX_SOURCE_KINDS, defaultKind);
2606
- const addNamedRef = (n, refObj, kind, viaAttr) => {
2607
- const ref = namedRefOf(refObj, n);
2608
- if (!ref) return;
2609
- const target = lookup(kind, ref.namespace, ref.name);
2610
- if (target) add(n.id, target.id, viaAttr);
2611
- };
2612
- const addProject = (n, project, viaAttr) => {
2613
- const candidates = project ? appProjectsByName.get(project) ?? [] : [];
2614
- const target = candidates.length === 1 ? candidates[0] : candidates.find((p) => namespaceOf(p) === namespaceOf(n));
2615
- if (target) add(n.id, target.id, viaAttr);
2803
+ return {
2804
+ ...state,
2805
+ strip: operatorStrip(result.rows, now),
2806
+ gates: convergeGateCards(result.rows),
2807
+ verdicts: tickComponentVerdicts(result.rows),
2808
+ note: null,
2809
+ code: null,
2810
+ readAt: at
2616
2811
  };
2617
- const addSelector = (n, raw, viaAttr) => {
2618
- const selector = labelRecord(raw);
2619
- if (!selector) return;
2620
- for (const w of workloads) {
2621
- if (namespaceOf(w) !== namespaceOf(n)) continue;
2622
- const labels = templateLabels(w);
2623
- if (labels && selects(selector, labels)) add(n.id, w.id, viaAttr);
2624
- }
2812
+ }
2813
+ function operatorNote(state, now = Date.now()) {
2814
+ if (!state.declared.length) {
2815
+ return "No operating loop: this project declares no ConvergeOp, so there is no operator strip.";
2816
+ }
2817
+ const loops = `${state.declared.length} ConvergeOp${state.declared.length === 1 ? "" : "s"}`;
2818
+ if (!state.strip) {
2819
+ return state.note ? `Operating loop: ${loops} declared \u2014 status unavailable: ${state.note}` : `Operating loop: ${loops} declared \u2014 not read yet.`;
2820
+ }
2821
+ const bits = [];
2822
+ const ticked = state.strip.rows.filter((r) => r.at).length;
2823
+ bits.push(
2824
+ ticked ? `last tick per loop \u2014 the status surface answers with one tick each, and the history is its own read (chant#2029) reached from the strip` : `no tick recorded yet on any loop`
2825
+ );
2826
+ const named = state.verdicts.reduce((n, t) => n + t.verdicts.length, 0);
2827
+ if (named) {
2828
+ const stale = state.verdicts.filter((t) => t.verdicts.length && !tickFreshness(t.at, now).fresh);
2829
+ const painted = named - stale.reduce((n, t) => n + t.verdicts.length, 0);
2830
+ bits.push(
2831
+ painted ? `${painted} component verdict${painted === 1 ? "" : "s"} from that tick \u2014 joined onto the component DAG by component name, under the live read (never over it) and only while the tick is younger than ${Math.round(TICK_VERDICT_TTL_MS / 6e4)}m` : `${named} component verdict${named === 1 ? "" : "s"} from that tick, all older than ${Math.round(TICK_VERDICT_TTL_MS / 6e4)}m \u2014 named on the node, not painted: a colour would read as the estate's state now`
2832
+ );
2833
+ }
2834
+ const held = state.strip.rows.filter((r) => r.lease === "held").length;
2835
+ if (held) bits.push(`${held} lease${held === 1 ? "" : "s"} held`);
2836
+ const expired = state.strip.rows.filter((r) => r.lease === "expired").length;
2837
+ if (expired) bits.push(`${expired} lease${expired === 1 ? "" : "s"} expired \u2014 the next round reclaims it`);
2838
+ if (state.strip.pendingGates) {
2839
+ const addressed = state.gates.filter((g) => g.url).length;
2840
+ bits.push(
2841
+ `${state.strip.pendingGates} converge gate${state.strip.pendingGates === 1 ? "" : "s"} pending \u2014 approving one records a fact for the next tick, it does not release a workflow (that is the run gate, a different card)` + (addressed ? `, and ${addressed} name${addressed === 1 ? "s" : ""} where approval happens (chant#2028)` : "")
2842
+ );
2843
+ }
2844
+ return `Operating loop: ${loops} \u2014 ${bits.join("; ")}.`;
2845
+ }
2846
+
2847
+ // src/component-status.ts
2848
+ function componentStatusColor(row) {
2849
+ if (row.stack && row.stack.healthy === false) {
2850
+ if (row.stack.status && /ROLLBACK|FAILED/i.test(row.stack.status)) return "warn";
2851
+ return "accent";
2852
+ }
2853
+ const rollup = row.resources;
2854
+ if (rollup && rollup.total > 0) {
2855
+ if (rollup.unobserved > 0) return "accent";
2856
+ if (rollup.present === rollup.total) return "good";
2857
+ if (rollup.present === 0) return "neutral";
2858
+ return "warn";
2859
+ }
2860
+ if (row.live !== void 0) return row.live ? "good" : "neutral";
2861
+ switch (row.reconciliation) {
2862
+ case "reconciled":
2863
+ return "good";
2864
+ case "unrecorded":
2865
+ return row.detail.startsWith("live") ? "good" : "neutral";
2866
+ case "stale":
2867
+ case "drifted":
2868
+ return "warn";
2869
+ case "unknown":
2870
+ default:
2871
+ return "neutral";
2872
+ }
2873
+ }
2874
+ function joinComponentStatus(ir, rows) {
2875
+ const byComponent = new Map(rows.map((r) => [r.component, r]));
2876
+ return {
2877
+ ...ir,
2878
+ nodes: ir.nodes.map((n) => {
2879
+ const row = byComponent.get(n.id);
2880
+ if (!row) return n;
2881
+ return {
2882
+ ...n,
2883
+ attrs: {
2884
+ ...n.attrs,
2885
+ _status: componentStatusColor(row),
2886
+ _liveStatus: {
2887
+ reconciliation: row.reconciliation,
2888
+ detail: row.detail,
2889
+ ...row.live !== void 0 ? { live: row.live } : {},
2890
+ ...row.stack ? { stack: row.stack } : {},
2891
+ ...row.resources ? { resources: row.resources } : {}
2892
+ }
2893
+ }
2894
+ };
2895
+ })
2625
2896
  };
2626
- for (const n of k8s) {
2627
- const carrier = SELECTOR_CARRIERS.find((c) => c.kind === n.kind);
2628
- if (carrier) {
2629
- addSelector(n, carrier.read(rec(n.attrs?.spec)), carrier.viaAttr);
2630
- continue;
2631
- }
2632
- if (n.kind === "K8s::Networking::Ingress") {
2633
- for (const svcName of ingressBackendServices(n)) {
2634
- const svc = lookup("K8s::Core::Service", namespaceOf(n), svcName);
2635
- if (svc) add(n.id, svc.id, "ingress backend");
2636
- }
2637
- continue;
2638
- }
2639
- if (n.kind === "K8s::Autoscaling::HorizontalPodAutoscaler") {
2640
- const ref = rec(rec(n.attrs?.spec)?.scaleTargetRef);
2641
- const refKind = str(ref?.kind);
2642
- const refName = str(ref?.name);
2643
- if (!refKind || !refName) continue;
2644
- for (const w of workloads) {
2645
- if (namespaceOf(w) !== namespaceOf(n)) continue;
2646
- if (shortKind(w.kind) === refKind && nameOf(w) === refName) add(n.id, w.id, "scaleTargetRef");
2647
- }
2648
- continue;
2649
- }
2650
- if (n.kind === "K8s::Flux::Kustomization") {
2651
- addSourceRef(n, rec(n.attrs?.spec)?.sourceRef, "sourceRef");
2652
- for (const dep of list(rec(n.attrs?.spec)?.dependsOn)) {
2653
- addNamedRef(n, dep, "K8s::Flux::Kustomization", "dependsOn");
2897
+ }
2898
+ function joinTickVerdicts(ir, tick, now = Date.now()) {
2899
+ if (!tick || !tick.verdicts.length) return ir;
2900
+ const { fresh } = tickFreshness(tick.at, now);
2901
+ const byComponent = new Map(tick.verdicts.map((v) => [v.component, v]));
2902
+ let joined = false;
2903
+ const nodes = ir.nodes.map((n) => {
2904
+ const v = byComponent.get(n.id);
2905
+ if (!v) return n;
2906
+ joined = true;
2907
+ const status = {
2908
+ reconciliation: v.reconciliation,
2909
+ detail: v.detail,
2910
+ ...v.live !== void 0 ? { live: v.live } : {},
2911
+ ...v.unobserved ? { unobserved: v.unobserved } : {},
2912
+ op: tick.op,
2913
+ env: tick.env,
2914
+ at: tick.at,
2915
+ ...tick.tickId ? { tickId: tick.tickId } : {},
2916
+ stale: !fresh
2917
+ };
2918
+ const paint = fresh && n.attrs?._status === void 0;
2919
+ return {
2920
+ ...n,
2921
+ attrs: {
2922
+ ...n.attrs,
2923
+ // The reconciliation tier and nothing else — `live`/`stack`/`resources`
2924
+ // are deliberately not passed, so this can only ever reach
2925
+ // `componentStatusColor`'s last branch.
2926
+ ...paint ? { _status: componentStatusColor({ reconciliation: v.reconciliation, detail: v.detail }) } : {},
2927
+ _tickStatus: status
2654
2928
  }
2655
- continue;
2656
- }
2657
- if (n.kind === "K8s::Flux::HelmRelease") {
2658
- addSourceRef(n, helmReleaseSourceRef(n), "chart sourceRef");
2659
- continue;
2660
- }
2661
- if (n.kind === "K8s::Flux::ImagePolicy") {
2662
- addNamedRef(n, rec(n.attrs?.spec)?.imageRepositoryRef, "K8s::Flux::ImageRepository", "imageRepositoryRef");
2663
- continue;
2664
- }
2665
- if (n.kind === "K8s::Flux::ImageUpdateAutomation") {
2666
- addSourceRef(n, rec(n.attrs?.spec)?.sourceRef, "sourceRef", "GitRepository");
2667
- continue;
2929
+ };
2930
+ });
2931
+ return joined ? { ...ir, nodes } : ir;
2932
+ }
2933
+
2934
+ // src/overlay.ts
2935
+ function pruneImports(ir) {
2936
+ const drop = new Set((ir.imports ?? []).map((i) => i.node));
2937
+ if (drop.size === 0) return ir;
2938
+ ir.nodes = ir.nodes.filter((n) => !drop.has(n.id));
2939
+ if (ir.edges) ir.edges = ir.edges.filter((e) => !drop.has(e.from) && !drop.has(e.to));
2940
+ return ir;
2941
+ }
2942
+ function componentDir(node) {
2943
+ const parts = node.sourceLoc?.file?.split("/") ?? [];
2944
+ if (parts[0] !== "src" || parts[1] === "examples" || parts.length < 3) return void 0;
2945
+ return parts[1];
2946
+ }
2947
+ function reclassifyOverlay(ir) {
2948
+ const deployedComponents = /* @__PURE__ */ new Set();
2949
+ for (const n of ir.nodes) {
2950
+ if (n.attrs?._status === "good") {
2951
+ const c = componentDir(n);
2952
+ if (c) deployedComponents.add(c);
2668
2953
  }
2669
- if (n.kind === "K8s::Flux::Alert") {
2670
- addNamedRef(n, rec(n.attrs?.spec)?.providerRef, "K8s::Flux::Provider", "providerRef");
2671
- for (const src of list(rec(n.attrs?.spec)?.eventSources)) {
2672
- addFluxRef(n, src, "eventSource");
2954
+ }
2955
+ for (const n of ir.nodes) {
2956
+ const file = n.sourceLoc?.file ?? "";
2957
+ if (file.startsWith("src/examples/")) {
2958
+ n.attrs = { ...n.attrs, _byo: true };
2959
+ delete n.attrs._status;
2960
+ } else if (n.kind === "AWS::CloudFormation::Parameter") {
2961
+ const c = componentDir(n);
2962
+ if (c && deployedComponents.has(c)) {
2963
+ n.attrs = { ...n.attrs, _status: "good" };
2673
2964
  }
2674
- continue;
2675
- }
2676
- if (n.kind === "K8s::Argo::Application") {
2677
- addProject(n, str(rec(n.attrs?.spec)?.project), "project");
2678
- continue;
2679
- }
2680
- if (n.kind === "K8s::Argo::ApplicationSet") {
2681
- addProject(n, str(rec(rec(rec(n.attrs?.spec)?.template)?.spec)?.project), "template project");
2682
2965
  }
2683
2966
  }
2684
- return out;
2967
+ return ir;
2685
2968
  }
2686
- function addK8sDeclaredEdges(ir) {
2687
- const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
2688
- for (const e of deriveK8sEdges(ir.nodes)) {
2689
- const key = `${e.from}\0${e.to}`;
2690
- if (existing.has(key)) continue;
2691
- existing.add(key);
2692
- ir.edges.push(e);
2969
+ function pruneRuntimeChildren(ir) {
2970
+ const runtime = new Set(ir.nodes.filter((n) => n.runtimeOwner).map((n) => n.id));
2971
+ if (runtime.size === 0) return ir;
2972
+ ir.nodes = ir.nodes.filter((n) => !runtime.has(n.id));
2973
+ ir.edges = ir.edges.filter((e) => !runtime.has(e.from) && !runtime.has(e.to));
2974
+ return ir;
2975
+ }
2976
+ function attachRuntimeContainment(ir) {
2977
+ const byOwner = /* @__PURE__ */ new Map();
2978
+ for (const n of ir.nodes) {
2979
+ if (!n.runtimeOwner) continue;
2980
+ if (!byOwner.has(n.runtimeOwner)) byOwner.set(n.runtimeOwner, /* @__PURE__ */ new Set());
2981
+ byOwner.get(n.runtimeOwner).add(n.id);
2982
+ }
2983
+ if (byOwner.size === 0) return ir;
2984
+ const byContainer = { ...ir.groups.byContainer ?? {} };
2985
+ for (const [owner, children] of byOwner) {
2986
+ byContainer[owner] = [.../* @__PURE__ */ new Set([owner, ...byContainer[owner] ?? [], ...children])].sort();
2693
2987
  }
2988
+ ir.groups.byContainer = byContainer;
2694
2989
  return ir;
2695
2990
  }
2696
2991
 
2697
- // src/helm-artifacts.ts
2698
- function chartNameOf(node) {
2699
- const name = node.attrs?.name;
2700
- return typeof name === "string" && name.length > 0 ? name : void 0;
2701
- }
2702
- function releaseMatchesChart(observed, chartName) {
2703
- const chart = observed.attributes?.chart;
2704
- if (typeof chart !== "string") return false;
2705
- return chart === chartName || chart.startsWith(`${chartName}-`);
2992
+ // src/value-match.ts
2993
+ function typeSegment(kind) {
2994
+ const parts = kind.split(kind.includes("::") ? "::" : "/");
2995
+ return parts[parts.length - 1] ?? kind;
2706
2996
  }
2707
- function applyHelmArtifacts(ir, observed) {
2708
- let charts = 0;
2709
- let installed = 0;
2710
- for (const node of ir.nodes) {
2711
- if (node.lexicon !== "helm" || node.kind !== "Helm::Chart") continue;
2712
- charts++;
2713
- const attrs = node.attrs ??= {};
2714
- if (observed === void 0) {
2715
- attrs._status = "neutral";
2716
- attrs._unobserved = "artifact-observation-unavailable";
2717
- continue;
2718
- }
2719
- const chartName = chartNameOf(node);
2720
- const entry = chartName ? Object.entries(observed).find(([key2, o2]) => key2.startsWith("release/") && releaseMatchesChart(o2, chartName)) : void 0;
2721
- if (!entry) {
2722
- attrs._status = "accent";
2723
- continue;
2724
- }
2725
- const [key, o] = entry;
2726
- const match = {
2727
- release: key.replace(/^release\//, ""),
2728
- ...o.status ? { status: o.status } : {},
2729
- ...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
2730
- ...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {}
2731
- };
2732
- attrs._artifact = match;
2733
- attrs._status = o.status === "deployed" ? "good" : "warn";
2734
- if (o.status === "deployed") installed++;
2735
- }
2736
- return { charts, installed };
2997
+ function isOwnNameAttr(kind, key) {
2998
+ if (!/name$/i.test(key)) return false;
2999
+ const prefix = key.slice(0, -4).toLowerCase();
3000
+ if (prefix === "") return true;
3001
+ return typeSegment(kind).toLowerCase().includes(prefix);
2737
3002
  }
2738
-
2739
- // src/estate.ts
2740
- import { statSync as statSync3 } from "node:fs";
2741
- import { availableParallelism } from "node:os";
2742
- import { join as join7, resolve as resolve3, sep } from "node:path";
2743
- import { composeStacks, shortStackNames } from "@intentius/pinhole";
2744
-
2745
- // src/member-ir.ts
2746
- import { createHash } from "node:crypto";
2747
- import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
2748
- import { join as join6, relative, resolve as resolve2 } from "node:path";
2749
- var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
2750
- function memberIrCacheSize(env = process.env) {
2751
- const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
2752
- return Number.isInteger(override) && override >= 0 ? override : 64;
3003
+ var MIN_NAME_LEN = 6;
3004
+ var MAX_NAME_LEN = 200;
3005
+ var MAX_DEPTH = 12;
3006
+ var SKIP_KEYS = /* @__PURE__ */ new Set([
3007
+ "status",
3008
+ "annotations",
3009
+ "labels",
3010
+ "matchLabels",
3011
+ "managedFields",
3012
+ "ownerReferences",
3013
+ "creationTimestamp",
3014
+ "resourceVersion",
3015
+ "uid",
3016
+ "selfLink",
3017
+ "generation",
3018
+ "finalizers"
3019
+ ]);
3020
+ function joinable(v) {
3021
+ return v.length >= MIN_NAME_LEN && v.length <= MAX_NAME_LEN && !/\s/.test(v);
2753
3022
  }
2754
- function memberSourceStamp(dir) {
2755
- const root = resolve2(dir);
2756
- const h = createHash("sha1");
2757
- let any = false;
2758
- const walk = (at) => {
2759
- const entries = readdirSync3(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
2760
- for (const e of entries) {
2761
- if (SKIP.has(e.name)) continue;
2762
- const path = join6(at, e.name);
2763
- if (e.isDirectory()) {
2764
- walk(path);
2765
- continue;
3023
+ function stringLeaves(attrs) {
3024
+ const out = [];
3025
+ const onPath = /* @__PURE__ */ new Set();
3026
+ const step = (v, key, path, depth) => {
3027
+ if (typeof v === "string") {
3028
+ out.push({ path, key, value: v });
3029
+ return;
3030
+ }
3031
+ if (depth >= MAX_DEPTH || !v || typeof v !== "object") return;
3032
+ if (onPath.has(v)) return;
3033
+ onPath.add(v);
3034
+ if (Array.isArray(v)) {
3035
+ for (const item of v) step(item, key, path, depth + 1);
3036
+ } else {
3037
+ for (const [k, val] of Object.entries(v)) {
3038
+ if (k.startsWith("_") || SKIP_KEYS.has(k)) continue;
3039
+ step(val, k, path ? `${path}.${k}` : k, depth + 1);
2766
3040
  }
2767
- if (!e.isFile()) continue;
2768
- const st = statSync2(path);
2769
- h.update(`${relative(root, path)}\0${st.mtimeMs}\0${st.size}
2770
- `);
2771
- any = true;
2772
3041
  }
3042
+ onPath.delete(v);
2773
3043
  };
2774
- try {
2775
- walk(root);
2776
- } catch {
2777
- return void 0;
2778
- }
2779
- return any ? h.digest("hex") : void 0;
2780
- }
2781
- function cacheKey(dir, opts) {
2782
- const chant = resolveChant(dir);
2783
- const canonical = JSON.stringify(opts, Object.keys(opts).sort());
2784
- return `${resolve2(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
2785
- }
2786
- var cache = /* @__PURE__ */ new Map();
2787
- var hits = 0;
2788
- var misses = 0;
2789
- function invalidateMember(dir) {
2790
- if (dir === void 0) {
2791
- cache.clear();
2792
- return;
3044
+ for (const [k, v] of Object.entries(attrs ?? {})) {
3045
+ if (k.startsWith("_") || SKIP_KEYS.has(k)) continue;
3046
+ step(v, k, k, 0);
2793
3047
  }
2794
- const root = resolve2(dir);
2795
- for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
3048
+ return out;
2796
3049
  }
2797
- async function memberIr(dir, opts = {}) {
2798
- const size = memberIrCacheSize();
2799
- if (opts.live || opts.overlay || size === 0) return graphIr(dir, opts);
2800
- const stamp = memberSourceStamp(dir);
2801
- const key = cacheKey(dir, opts);
2802
- const hit = stamp !== void 0 ? cache.get(key) : void 0;
2803
- if (hit && hit.stamp === stamp) {
2804
- hits++;
2805
- cache.delete(key);
2806
- cache.set(key, hit);
2807
- return structuredClone(hit.ir);
3050
+ function addValueMatchEdges(ir) {
3051
+ const nodes = ir.nodes;
3052
+ const owner = /* @__PURE__ */ new Map();
3053
+ for (const n of nodes) {
3054
+ for (const { path, key, value } of stringLeaves(n.attrs)) {
3055
+ if (path !== key && path !== `metadata.${key}`) continue;
3056
+ if (!joinable(value) || !isOwnNameAttr(n.kind, key)) continue;
3057
+ owner.set(value, owner.has(value) ? null : n.id);
3058
+ }
2808
3059
  }
2809
- misses++;
2810
- const ir = await graphIr(dir, opts);
2811
- if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
2812
- cache.delete(key);
2813
- cache.set(key, { dir: resolve2(dir), stamp, ir });
2814
- for (const oldest of cache.keys()) {
2815
- if (cache.size <= size) break;
2816
- cache.delete(oldest);
3060
+ if (owner.size === 0) return ir;
3061
+ const declared = /* @__PURE__ */ new Set();
3062
+ for (const e of ir.edges) declared.add(`${e.from}\0${e.to}`);
3063
+ const added = /* @__PURE__ */ new Set();
3064
+ for (const a of nodes) {
3065
+ for (const { path, value } of stringLeaves(a.attrs)) {
3066
+ if (!joinable(value)) continue;
3067
+ const b = owner.get(value);
3068
+ if (!b || b === a.id) continue;
3069
+ const key = `${a.id}\0${b}`;
3070
+ if (declared.has(key) || added.has(key)) continue;
3071
+ added.add(key);
3072
+ ir.edges.push({ from: a.id, to: b, kind: "ref", viaAttr: path, inferred: true });
3073
+ }
2817
3074
  }
2818
- return structuredClone(ir);
3075
+ return ir;
2819
3076
  }
2820
3077
 
2821
- // src/zoom-notes.ts
2822
- function zoomNote(zoom, ir, compositeEdgesAttached) {
2823
- const empty = ir.nodes.length === 0;
2824
- if (zoom === "components") {
2825
- return empty ? "no components discovered \u2014 nothing declares one in this project" : void 0;
2826
- }
2827
- if (zoom === "logical") {
2828
- return empty ? "logical is an AWS projection \u2014 no AWS resources in this estate" : void 0;
2829
- }
2830
- if (zoom === "composites") {
2831
- return compositeEdgesAttached === 0 ? "no component ownership to join \u2014 showing the resource graph unchanged" : void 0;
2832
- }
2833
- if (zoom === "runtime") {
2834
- const children = ir.nodes.filter((n) => n.runtimeOwner).length;
2835
- return children === 0 ? "nothing below the declaration boundary \u2014 no owner-referenced children on this substrate" : void 0;
2836
- }
2837
- return void 0;
3078
+ // src/k8s-edges.ts
3079
+ var WORKLOAD_KINDS = /* @__PURE__ */ new Set([
3080
+ "K8s::Apps::Deployment",
3081
+ "K8s::Apps::StatefulSet",
3082
+ "K8s::Apps::DaemonSet",
3083
+ "K8s::Apps::ReplicaSet"
3084
+ ]);
3085
+ var SELECTOR_CARRIERS = [
3086
+ { kind: "K8s::Core::Service", read: (spec) => spec?.selector, viaAttr: "selector" },
3087
+ { kind: "K8s::Policy::PodDisruptionBudget", read: (spec) => labelSelector(spec?.selector), viaAttr: "selector" },
3088
+ { kind: "K8s::Networking::NetworkPolicy", read: (spec) => labelSelector(spec?.podSelector), viaAttr: "podSelector" }
3089
+ ];
3090
+ function rec2(v) {
3091
+ return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
2838
3092
  }
2839
- function logicalKept(before, after) {
2840
- if (after > 0 && after * 3 >= before) return void 0;
2841
- return after === 0 ? `logical projected nothing from ${before} resources \u2014 it is a cloud-topology lens, and this estate declares none of the kinds it nests (behold#74)` : `logical kept ${after} of ${before} resources \u2014 it is a cloud-topology lens, and the rest are kinds it does not nest (behold#74)`;
3093
+ function str2(v) {
3094
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2842
3095
  }
2843
- function edgelessNote(zoom, ir, detail) {
2844
- if (zoom === "components" || zoom === "logical") return void 0;
2845
- if (ir.nodes.length === 0 || ir.edges.length > 0) return void 0;
2846
- if (detail !== void 0 && detail < 3) {
2847
- return "no edges at this detail \u2014 sourceRef/dependsOn and other attrs-derived references only appear at detail 3 (\u2318K \u2192 attributes, or add &detail=3)";
3096
+ function namespaceOf(n) {
3097
+ return str2(rec2(n.attrs?.metadata)?.namespace) ?? "default";
3098
+ }
3099
+ function nameOf(n) {
3100
+ return str2(rec2(n.attrs?.metadata)?.name);
3101
+ }
3102
+ function labelRecord(v) {
3103
+ const r = rec2(v);
3104
+ if (!r) return void 0;
3105
+ const out = {};
3106
+ for (const [k, val] of Object.entries(r)) {
3107
+ if (typeof val !== "string") return void 0;
3108
+ out[k] = val;
2848
3109
  }
2849
- return "no edges \u2014 nothing in this estate references anything else";
3110
+ return Object.keys(out).length > 0 ? out : void 0;
2850
3111
  }
2851
- var CLUSTER_SCOPED = /* @__PURE__ */ new Set([
2852
- "K8s::Core::Namespace",
2853
- "K8s::Rbac::ClusterRole",
2854
- "K8s::Rbac::ClusterRoleBinding",
2855
- "K8s::Storage::StorageClass",
2856
- "K8s::Apiextensions::CustomResourceDefinition"
2857
- ]);
2858
- function namespaceMismatchNote(nodes) {
2859
- const k8s = nodes.filter((n) => n?.lexicon === "k8s");
2860
- if (k8s.length === 0) return void 0;
2861
- const pending = k8s.filter((n) => n.attrs?._status === "accent" && !CLUSTER_SCOPED.has(n.kind ?? ""));
2862
- if (pending.length < 2) return void 0;
2863
- if (pending.some((n) => typeof n.attrs?.metadata?.namespace === "string" && n.attrs.metadata.namespace)) return void 0;
2864
- if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
2865
- return `${pending.length} pending k8s objects declare no metadata.namespace \u2014 if a controller stamps it at apply time (e.g. Flux's targetNamespace), the live read looked in "default", not where they run`;
3112
+ function labelSelector(v) {
3113
+ const sel = rec2(v);
3114
+ if (!sel) return void 0;
3115
+ const expressions = sel.matchExpressions;
3116
+ if (Array.isArray(expressions) && expressions.length > 0) return void 0;
3117
+ return sel.matchLabels;
2866
3118
  }
2867
- function namespaceJoinNote(joined) {
2868
- if (joined.length === 0) return void 0;
2869
- const each = joined.map((j) => `${j.name} in "${j.namespace}"`).join(", ");
2870
- return `read ${each} \u2014 the namespace the estate's own Kustomization targetNamespace binds, not the app project's own declaration`;
3119
+ function selects(selector, labels) {
3120
+ return Object.entries(selector).every(([k, v]) => labels[k] === v);
2871
3121
  }
2872
- function notesFor(zoom, ir, compositeEdgesAttached, logicalBefore, detail) {
2873
- const primary = zoom === "logical" && logicalBefore !== void 0 ? logicalKept(logicalBefore, ir.nodes.length) : zoomNote(zoom, ir, compositeEdgesAttached);
2874
- const notes = [primary, edgelessNote(zoom, ir, detail)].filter((n) => n !== void 0);
2875
- return notes.length ? notes.join(" \xB7 ") : void 0;
3122
+ function templateLabels(n) {
3123
+ const template = rec2(rec2(n.attrs?.spec)?.template);
3124
+ return labelRecord(rec2(template?.metadata)?.labels);
2876
3125
  }
2877
- function tierMismatchNote(ir, tiers, currentTier) {
2878
- const values = tiers?.values ?? [];
2879
- if (values.length < 2) return void 0;
2880
- const byLexicon = /* @__PURE__ */ new Map();
2881
- for (const n of ir.nodes) {
2882
- const status = n.attrs?._status;
2883
- if (status !== "good" && status !== "accent") continue;
2884
- const lexicon = n.lexicon ?? "?";
2885
- const c = byLexicon.get(lexicon) ?? { good: 0, accent: 0 };
2886
- c[status === "good" ? "good" : "accent"]++;
2887
- byLexicon.set(lexicon, c);
2888
- }
2889
- for (const [lexicon, { good, accent }] of [...byLexicon].sort()) {
2890
- if (accent < 2 || accent * 2 < good + accent) continue;
2891
- const here = currentTier ? `tier "${currentTier}"` : "the default tier";
2892
- const others = values.filter((v) => v !== currentTier).join(", ");
2893
- return `${accent} of ${good + accent} ${lexicon} resources read "declared, not deployed" at ${here} \u2014 if the estate runs another declared tier (${others}), pick it (\u2318K \u2192 tier)`;
2894
- }
2895
- return void 0;
2896
- }
2897
-
2898
- // src/estate.ts
2899
- function estateReadPool(members, env = process.env) {
2900
- const override = Number.parseInt(env.BEHOLD_ESTATE_CONCURRENCY ?? "", 10);
2901
- const cap = Number.isInteger(override) && override >= 1 ? override : Math.min(4, availableParallelism());
2902
- return Math.max(1, Math.min(cap, members));
3126
+ function shortKind(kind) {
3127
+ const parts = kind.split("::");
3128
+ return parts[parts.length - 1] ?? kind;
2903
3129
  }
2904
- async function mapPool(items, width, fn) {
2905
- const results = new Array(items.length);
2906
- let next = 0;
2907
- const worker = async () => {
2908
- for (let i = next++; i < items.length; i = next++) {
2909
- results[i] = await fn(items[i], i);
2910
- }
2911
- };
2912
- await Promise.all(Array.from({ length: Math.max(1, Math.min(width, items.length)) }, () => worker()));
2913
- return results;
3130
+ var FLUX_SOURCE_KINDS = /* @__PURE__ */ new Set([
3131
+ "K8s::Flux::GitRepository",
3132
+ "K8s::Flux::OCIRepository",
3133
+ "K8s::Flux::HelmRepository",
3134
+ "K8s::Flux::HelmChart",
3135
+ "K8s::Flux::Bucket"
3136
+ ]);
3137
+ function sourceRefOf(refObj, referrer, defaultKind) {
3138
+ const ref = rec2(refObj);
3139
+ const kind = str2(ref?.kind) ?? defaultKind;
3140
+ const name = str2(ref?.name);
3141
+ if (!kind || !name) return void 0;
3142
+ return { kind, name, namespace: str2(ref?.namespace) ?? namespaceOf(referrer) };
2914
3143
  }
2915
- function estateMembers(projectDirs) {
2916
- const names = shortStackNames([...projectDirs]);
2917
- return projectDirs.map((dir, i) => ({ name: names[i], dir }));
3144
+ function namedRefOf(refObj, referrer) {
3145
+ const ref = rec2(refObj);
3146
+ const name = str2(ref?.name);
3147
+ if (!name) return void 0;
3148
+ return { name, namespace: str2(ref?.namespace) ?? namespaceOf(referrer) };
2918
3149
  }
2919
- async function composeEstate(projectDirs, opts = {}) {
2920
- const names = shortStackNames(projectDirs);
2921
- const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
2922
- name: names[i],
2923
- ir: await memberIr(dir, opts)
2924
- }));
2925
- return composeStacks(stacks);
3150
+ function list(v) {
3151
+ return Array.isArray(v) ? v : [];
2926
3152
  }
2927
- var firstLine2 = (e) => {
2928
- const msg = e instanceof Error ? e.message : String(e);
2929
- const m = msg.match(/exited \d+:\s*([\s\S]*)/);
2930
- return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
2931
- };
2932
- function namespaceRuntimeOwners(name, ir) {
2933
- const own = new Set(ir.nodes.map((n) => n.id));
2934
- for (const n of ir.nodes) {
2935
- if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
2936
- }
2937
- return ir;
3153
+ function helmReleaseSourceRef(n) {
3154
+ return rec2(rec2(rec2(n.attrs?.spec)?.chart)?.spec)?.sourceRef;
2938
3155
  }
2939
- var NAMESPACE_JOIN_FLOOR = "0.44.5";
2940
- var rec2 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
2941
- var str2 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2942
- var realIsDir = (p) => {
2943
- try {
2944
- return statSync3(p).isDirectory();
2945
- } catch {
2946
- return false;
2947
- }
2948
- };
2949
- function declaredNamespaceBindings(ir) {
3156
+ function ingressBackendServices(n) {
3157
+ const spec = rec2(n.attrs?.spec);
2950
3158
  const out = [];
2951
- for (const n of ir.nodes) {
2952
- if (n.kind !== "K8s::Flux::Kustomization") continue;
2953
- const spec = rec2(n.attrs?.spec);
2954
- const namespace = str2(spec?.targetNamespace);
2955
- const path = str2(spec?.path);
2956
- if (namespace && path) out.push({ path, namespace });
3159
+ const backendName = (b) => str2(rec2(rec2(b)?.service)?.name);
3160
+ const fromDefault = backendName(spec?.defaultBackend);
3161
+ if (fromDefault) out.push(fromDefault);
3162
+ const rules = Array.isArray(spec?.rules) ? spec.rules : [];
3163
+ for (const rule of rules) {
3164
+ const paths = rec2(rec2(rule)?.http)?.paths;
3165
+ for (const p of Array.isArray(paths) ? paths : []) {
3166
+ const name = backendName(rec2(p)?.backend);
3167
+ if (name) out.push(name);
3168
+ }
2957
3169
  }
2958
3170
  return out;
2959
3171
  }
2960
- function awaitsNamespaceBinding(ir) {
2961
- const namespaced = ir.nodes.filter((n) => n.lexicon === "k8s" && !CLUSTER_SCOPED.has(n.kind ?? ""));
2962
- if (namespaced.length === 0) return false;
2963
- return namespaced.every((n) => !str2(rec2(n.attrs?.metadata)?.namespace));
2964
- }
2965
- function declaredSegments(path) {
2966
- if (path.startsWith("/")) return void 0;
2967
- const segs = path.split("/").filter((s) => s !== "" && s !== ".");
2968
- if (segs.length === 0 || segs.includes("..")) return void 0;
2969
- return segs;
2970
- }
2971
- function pathAlignment(declared, dir, isDir = realIsDir) {
2972
- const want = declaredSegments(declared);
2973
- if (!want) return 0;
2974
- const root = resolve3(dir);
2975
- const have = root.split(sep).filter(Boolean);
2976
- for (let k = Math.min(want.length, have.length); k >= 1; k--) {
2977
- if (!have.slice(-k).every((s, i) => s === want[i])) continue;
2978
- if (!isDir(join7(root, ...want.slice(k)))) continue;
2979
- return k;
3172
+ function deriveK8sEdges(nodes) {
3173
+ const k8s = nodes.filter((n) => n.lexicon === "k8s");
3174
+ if (k8s.length === 0) return [];
3175
+ const out = [];
3176
+ const add = (from, to, viaAttr) => {
3177
+ if (from !== to) out.push({ from, to, kind: "ref", viaAttr, inferred: true });
3178
+ };
3179
+ const workloads = k8s.filter((n) => WORKLOAD_KINDS.has(n.kind));
3180
+ const byKindNsName = /* @__PURE__ */ new Map();
3181
+ const appProjectsByName = /* @__PURE__ */ new Map();
3182
+ for (const n of k8s) {
3183
+ const name = nameOf(n);
3184
+ if (!name) continue;
3185
+ byKindNsName.set(`${n.kind}\0${namespaceOf(n)}\0${name}`, n);
3186
+ if (n.kind === "K8s::Argo::AppProject") {
3187
+ const same = appProjectsByName.get(name) ?? [];
3188
+ same.push(n);
3189
+ appProjectsByName.set(name, same);
3190
+ }
2980
3191
  }
2981
- return 0;
2982
- }
2983
- function joinNamespaceBindings(members, isDir = realIsDir) {
2984
- const bindings = members.flatMap(
2985
- (m) => m.ir ? declaredNamespaceBindings(m.ir).map((b) => ({ ...b, declaredBy: m.dir })) : []
2986
- );
2987
- if (bindings.length === 0) return [];
2988
- const awaiting = members.filter((m) => m.ir && awaitsNamespaceBinding(m.ir));
2989
- if (awaiting.length === 0) return [];
2990
- const best = /* @__PURE__ */ new Map();
2991
- for (const b of bindings) {
2992
- let winner;
2993
- let tied = false;
2994
- for (const m of awaiting) {
2995
- if (m.dir === b.declaredBy) continue;
2996
- const score = pathAlignment(b.path, m.dir, isDir);
2997
- if (score === 0) continue;
2998
- if (!winner || score > winner.score) {
2999
- winner = { dir: m.dir, score };
3000
- tied = false;
3001
- } else if (score === winner.score) {
3002
- tied = true;
3192
+ const lookup = (kind, namespace, name) => byKindNsName.get(`${kind}\0${namespace}\0${name}`);
3193
+ const addFluxRef = (n, refObj, viaAttr, allowed, defaultKind) => {
3194
+ const ref = sourceRefOf(refObj, n, defaultKind);
3195
+ if (!ref || ref.name === "*") return;
3196
+ const kind = `K8s::Flux::${ref.kind}`;
3197
+ if (allowed && !allowed.has(kind)) return;
3198
+ const target = lookup(kind, ref.namespace, ref.name);
3199
+ if (target) add(n.id, target.id, viaAttr);
3200
+ };
3201
+ const addSourceRef = (n, refObj, viaAttr, defaultKind) => addFluxRef(n, refObj, viaAttr, FLUX_SOURCE_KINDS, defaultKind);
3202
+ const addNamedRef = (n, refObj, kind, viaAttr) => {
3203
+ const ref = namedRefOf(refObj, n);
3204
+ if (!ref) return;
3205
+ const target = lookup(kind, ref.namespace, ref.name);
3206
+ if (target) add(n.id, target.id, viaAttr);
3207
+ };
3208
+ const addProject = (n, project, viaAttr) => {
3209
+ const candidates = project ? appProjectsByName.get(project) ?? [] : [];
3210
+ const target = candidates.length === 1 ? candidates[0] : candidates.find((p) => namespaceOf(p) === namespaceOf(n));
3211
+ if (target) add(n.id, target.id, viaAttr);
3212
+ };
3213
+ const addSelector = (n, raw, viaAttr) => {
3214
+ const selector = labelRecord(raw);
3215
+ if (!selector) return;
3216
+ for (const w of workloads) {
3217
+ if (namespaceOf(w) !== namespaceOf(n)) continue;
3218
+ const labels = templateLabels(w);
3219
+ if (labels && selects(selector, labels)) add(n.id, w.id, viaAttr);
3220
+ }
3221
+ };
3222
+ for (const n of k8s) {
3223
+ const carrier = SELECTOR_CARRIERS.find((c) => c.kind === n.kind);
3224
+ if (carrier) {
3225
+ addSelector(n, carrier.read(rec2(n.attrs?.spec)), carrier.viaAttr);
3226
+ continue;
3227
+ }
3228
+ if (n.kind === "K8s::Networking::Ingress") {
3229
+ for (const svcName of ingressBackendServices(n)) {
3230
+ const svc = lookup("K8s::Core::Service", namespaceOf(n), svcName);
3231
+ if (svc) add(n.id, svc.id, "ingress backend");
3003
3232
  }
3233
+ continue;
3234
+ }
3235
+ if (n.kind === "K8s::Autoscaling::HorizontalPodAutoscaler") {
3236
+ const ref = rec2(rec2(n.attrs?.spec)?.scaleTargetRef);
3237
+ const refKind = str2(ref?.kind);
3238
+ const refName = str2(ref?.name);
3239
+ if (!refKind || !refName) continue;
3240
+ for (const w of workloads) {
3241
+ if (namespaceOf(w) !== namespaceOf(n)) continue;
3242
+ if (shortKind(w.kind) === refKind && nameOf(w) === refName) add(n.id, w.id, "scaleTargetRef");
3243
+ }
3244
+ continue;
3245
+ }
3246
+ if (n.kind === "K8s::Flux::Kustomization") {
3247
+ addSourceRef(n, rec2(n.attrs?.spec)?.sourceRef, "sourceRef");
3248
+ for (const dep of list(rec2(n.attrs?.spec)?.dependsOn)) {
3249
+ addNamedRef(n, dep, "K8s::Flux::Kustomization", "dependsOn");
3250
+ }
3251
+ continue;
3252
+ }
3253
+ if (n.kind === "K8s::Flux::HelmRelease") {
3254
+ addSourceRef(n, helmReleaseSourceRef(n), "chart sourceRef");
3255
+ continue;
3256
+ }
3257
+ if (n.kind === "K8s::Flux::ImagePolicy") {
3258
+ addNamedRef(n, rec2(n.attrs?.spec)?.imageRepositoryRef, "K8s::Flux::ImageRepository", "imageRepositoryRef");
3259
+ continue;
3260
+ }
3261
+ if (n.kind === "K8s::Flux::ImageUpdateAutomation") {
3262
+ addSourceRef(n, rec2(n.attrs?.spec)?.sourceRef, "sourceRef", "GitRepository");
3263
+ continue;
3264
+ }
3265
+ if (n.kind === "K8s::Flux::Alert") {
3266
+ addNamedRef(n, rec2(n.attrs?.spec)?.providerRef, "K8s::Flux::Provider", "providerRef");
3267
+ for (const src of list(rec2(n.attrs?.spec)?.eventSources)) {
3268
+ addFluxRef(n, src, "eventSource");
3269
+ }
3270
+ continue;
3271
+ }
3272
+ if (n.kind === "K8s::Argo::Application") {
3273
+ addProject(n, str2(rec2(n.attrs?.spec)?.project), "project");
3274
+ continue;
3275
+ }
3276
+ if (n.kind === "K8s::Argo::ApplicationSet") {
3277
+ addProject(n, str2(rec2(rec2(rec2(n.attrs?.spec)?.template)?.spec)?.project), "template project");
3004
3278
  }
3005
- if (!winner || tied) continue;
3006
- const prev = best.get(winner.dir);
3007
- if (prev === "ambiguous") continue;
3008
- const join21 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
3009
- if (!prev) best.set(winner.dir, { score: winner.score, join: join21 });
3010
- else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
3011
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join21 });
3012
3279
  }
3013
- return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
3280
+ return out;
3014
3281
  }
3015
- async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
3016
- if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
3017
- const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
3018
- const irs = await mapPool(
3019
- projectDirs,
3020
- estateReadPool(projectDirs.length),
3021
- (dir) => memberIr(dir, { ...src, detail: 3 }).catch(() => void 0)
3022
- );
3023
- const scopes = /* @__PURE__ */ new Map();
3024
- for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
3025
- if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
3026
- scopes.set(j.dir, j.namespace);
3282
+ function addK8sDeclaredEdges(ir) {
3283
+ const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
3284
+ for (const e of deriveK8sEdges(ir.nodes)) {
3285
+ const key = `${e.from}\0${e.to}`;
3286
+ if (existing.has(key)) continue;
3287
+ existing.add(key);
3288
+ ir.edges.push(e);
3027
3289
  }
3028
- return scopes;
3029
- }
3030
- function withoutJoinedMembers(nodes, joined) {
3031
- if (joined.length === 0) return [...nodes];
3032
- return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
3033
- }
3034
- async function composeEstateOverlay(projectDirs, opts, classify) {
3035
- const names = shortStackNames(projectDirs);
3036
- const unobserved = [];
3037
- const dropped = [];
3038
- const joined = [];
3039
- const stacks = new Array(projectDirs.length);
3040
- const scopes = await estateNamespaceScopes(projectDirs, opts);
3041
- await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
3042
- const name = names[i];
3043
- const namespace = scopes.get(dir);
3044
- try {
3045
- const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
3046
- stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
3047
- if (namespace) joined.push({ name, namespace });
3048
- } catch (err) {
3049
- const reason = firstLine2(err);
3050
- try {
3051
- const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
3052
- const src = await memberIr(dir, srcOpts);
3053
- for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
3054
- stacks[i] = { name, ir: src };
3055
- unobserved.push({ name, reason });
3056
- } catch (err2) {
3057
- dropped.push({ name, reason: firstLine2(err2) });
3058
- }
3059
- }
3060
- });
3061
- const present = stacks.filter((s) => !!s);
3062
- return {
3063
- ir: composeStacks(present),
3064
- observed: present.length - unobserved.length,
3065
- total: projectDirs.length,
3066
- unobserved,
3067
- dropped,
3068
- // Concurrent reads finish in whatever order the clusters answer; the note
3069
- // this feeds should read the same twice.
3070
- joined: joined.sort((a, b) => a.name.localeCompare(b.name))
3071
- };
3290
+ return ir;
3072
3291
  }
3073
3292
 
3074
- // src/helm-drift.ts
3075
- function countChanges(docs) {
3076
- return docs.reduce((n, d) => n + d.changes.length, 0);
3077
- }
3078
- function provenanceOf(live) {
3079
- const m = live.manifest;
3080
- return {
3081
- contentDigest: m.contentDigest,
3082
- inputDigest: m.inputDigest,
3083
- valuesDigest: m.valuesDigest,
3084
- chart: m.chart,
3085
- chartVersion: m.chartVersion,
3086
- repo: m.repo,
3087
- releaseName: m.releaseName,
3088
- namespace: m.namespace,
3089
- profile: m.capabilityProfile?.cluster ?? "",
3090
- kubeVersion: m.capabilityProfile?.kubeVersion ?? "",
3091
- renderedAt: m.renderedAt,
3092
- helmVersion: m.helmVersion,
3093
- chantVersion: m.chantVersion,
3094
- sourceRef: m.sourceRef
3095
- };
3096
- }
3097
- function helmRenderReport(record, live) {
3098
- const base = { render: record.name, chart: record.chart };
3099
- if (!live) {
3100
- return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
3101
- }
3102
- const d = live.diff;
3103
- const counts = {
3104
- drifted: d.drifted.length,
3105
- changes: countChanges(d.drifted),
3106
- unchanged: d.unchanged.length,
3107
- unobserved: d.unobserved.length,
3108
- undeclared: d.undeclaredEntities.length
3109
- };
3110
- const provenance = provenanceOf(live);
3111
- if (d.drifted.length > 0) {
3112
- return { ...base, verdict: "drifted", counts, provenance, drifted: d.drifted };
3113
- }
3114
- if (d.drifted.length + d.accepted.length + d.unchanged.length === 0) {
3115
- return { ...base, verdict: "unobserved", reason: "nothing-observed", counts, provenance };
3116
- }
3117
- if (d.unobserved.length > 0) {
3118
- return { ...base, verdict: "unobserved", reason: "partly-observed", counts, provenance };
3119
- }
3120
- return { ...base, verdict: "in-sync", counts, provenance };
3121
- }
3122
- async function readHelmRenderDrift(projectDir, env, opts = {}) {
3123
- const report = await helmRenders(projectDir, opts).catch(() => void 0);
3124
- const records = report?.records ?? [];
3125
- if (records.length === 0) return [];
3126
- return mapPool(records, estateReadPool(records.length), async (record) => {
3127
- if (!record.contentDigest) return helmRenderReport(record, void 0);
3128
- const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
3129
- return helmRenderReport(record, live);
3130
- });
3131
- }
3132
- function chartNameOf2(node) {
3293
+ // src/helm-artifacts.ts
3294
+ function chartNameOf(node) {
3133
3295
  const name = node.attrs?.name;
3134
3296
  return typeof name === "string" && name.length > 0 ? name : void 0;
3135
3297
  }
3136
- var SEVERITY = { drifted: 2, unobserved: 1, "in-sync": 0 };
3137
- function applyHelmRenderDrift(ir, reports) {
3138
- if (reports.length === 0) return { charts: 0, drifted: 0 };
3139
- const byChart = /* @__PURE__ */ new Map();
3140
- for (const r of reports) {
3141
- const list3 = byChart.get(r.chart);
3142
- if (list3) list3.push(r);
3143
- else byChart.set(r.chart, [r]);
3144
- }
3298
+ function releaseMatchesChart(observed, chartName) {
3299
+ const chart = observed.attributes?.chart;
3300
+ if (typeof chart !== "string") return false;
3301
+ return chart === chartName || chart.startsWith(`${chartName}-`);
3302
+ }
3303
+ function applyHelmArtifacts(ir, observed) {
3145
3304
  let charts = 0;
3146
- let drifted = 0;
3305
+ let installed = 0;
3147
3306
  for (const node of ir.nodes) {
3148
3307
  if (node.lexicon !== "helm" || node.kind !== "Helm::Chart") continue;
3149
- const chartName = chartNameOf2(node);
3150
- const matched = chartName ? byChart.get(chartName) : void 0;
3151
- if (!matched || matched.length === 0) continue;
3152
- const worst = matched.reduce((a, b) => SEVERITY[b.verdict] > SEVERITY[a.verdict] ? b : a);
3153
- const attrs = node.attrs ??= {};
3154
- attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
3155
3308
  charts++;
3156
- if (worst.verdict === "drifted") {
3157
- attrs._status = "warn";
3158
- drifted++;
3309
+ const attrs = node.attrs ??= {};
3310
+ if (observed === void 0) {
3311
+ attrs._status = "neutral";
3312
+ attrs._unobserved = "artifact-observation-unavailable";
3313
+ continue;
3314
+ }
3315
+ const chartName = chartNameOf(node);
3316
+ const entry = chartName ? Object.entries(observed).find(([key2, o2]) => key2.startsWith("release/") && releaseMatchesChart(o2, chartName)) : void 0;
3317
+ if (!entry) {
3318
+ attrs._status = "accent";
3319
+ continue;
3159
3320
  }
3321
+ const [key, o] = entry;
3322
+ const match = {
3323
+ release: key.replace(/^release\//, ""),
3324
+ ...o.status ? { status: o.status } : {},
3325
+ ...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
3326
+ ...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {}
3327
+ };
3328
+ attrs._artifact = match;
3329
+ attrs._status = o.status === "deployed" ? "good" : "warn";
3330
+ if (o.status === "deployed") installed++;
3160
3331
  }
3161
- return { charts, drifted };
3332
+ return { charts, installed };
3162
3333
  }
3163
3334
 
3164
- // src/cluster-anchor.ts
3165
- var MANAGED_CLUSTER_KINDS = /* @__PURE__ */ new Set([
3166
- "AWS::EKS::Cluster",
3167
- "GCP::Container::Cluster",
3168
- "Microsoft.ContainerService/managedClusters",
3169
- "K3d::Cluster"
3170
- ]);
3171
- var ANCHOR_VIA = "runs-on";
3172
- function metadata(node) {
3173
- const m = node.attrs?.metadata;
3174
- return m && typeof m === "object" && !Array.isArray(m) ? m : void 0;
3335
+ // src/estate.ts
3336
+ import { statSync as statSync3 } from "node:fs";
3337
+ import { availableParallelism } from "node:os";
3338
+ import { join as join7, resolve as resolve3, sep } from "node:path";
3339
+ import { composeStacks, shortStackNames } from "@intentius/pinhole";
3340
+
3341
+ // src/member-ir.ts
3342
+ import { createHash } from "node:crypto";
3343
+ import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
3344
+ import { join as join6, relative, resolve as resolve2 } from "node:path";
3345
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
3346
+ function memberIrCacheSize(env = process.env) {
3347
+ const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
3348
+ return Number.isInteger(override) && override >= 0 ? override : 64;
3175
3349
  }
3176
- function metaString(node, key) {
3177
- const v = metadata(node)?.[key];
3178
- return typeof v === "string" && v.length > 0 ? v : void 0;
3350
+ function memberSourceStamp(dir) {
3351
+ const root = resolve2(dir);
3352
+ const h = createHash("sha1");
3353
+ let any = false;
3354
+ const walk = (at) => {
3355
+ const entries = readdirSync3(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
3356
+ for (const e of entries) {
3357
+ if (SKIP.has(e.name)) continue;
3358
+ const path = join6(at, e.name);
3359
+ if (e.isDirectory()) {
3360
+ walk(path);
3361
+ continue;
3362
+ }
3363
+ if (!e.isFile()) continue;
3364
+ const st = statSync2(path);
3365
+ h.update(`${relative(root, path)}\0${st.mtimeMs}\0${st.size}
3366
+ `);
3367
+ any = true;
3368
+ }
3369
+ };
3370
+ try {
3371
+ walk(root);
3372
+ } catch {
3373
+ return void 0;
3374
+ }
3375
+ return any ? h.digest("hex") : void 0;
3179
3376
  }
3180
- function isK8s(node) {
3181
- return node.lexicon === "k8s";
3377
+ function cacheKey(dir, opts) {
3378
+ const chant = resolveChant(dir);
3379
+ const canonical = JSON.stringify(opts, Object.keys(opts).sort());
3380
+ return `${resolve2(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
3182
3381
  }
3183
- function isNamespace(node) {
3184
- return node.kind === "K8s::Core::Namespace";
3382
+ var cache = /* @__PURE__ */ new Map();
3383
+ var hits = 0;
3384
+ var misses = 0;
3385
+ function invalidateMember(dir) {
3386
+ if (dir === void 0) {
3387
+ cache.clear();
3388
+ return;
3389
+ }
3390
+ const root = resolve2(dir);
3391
+ for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
3185
3392
  }
3186
- function managedClusterName(node) {
3187
- const metaName = metaString(node, "name");
3188
- if (metaName) return metaName;
3189
- for (const key of ["name", "clusterName"]) {
3190
- const v = node.attrs?.[key];
3191
- if (typeof v === "string" && v.length > 0) return v;
3393
+ async function memberIr(dir, opts = {}) {
3394
+ const size = memberIrCacheSize();
3395
+ if (opts.live || opts.overlay || size === 0) return graphIr(dir, opts);
3396
+ const stamp = memberSourceStamp(dir);
3397
+ const key = cacheKey(dir, opts);
3398
+ const hit = stamp !== void 0 ? cache.get(key) : void 0;
3399
+ if (hit && hit.stamp === stamp) {
3400
+ hits++;
3401
+ cache.delete(key);
3402
+ cache.set(key, hit);
3403
+ return structuredClone(hit.ir);
3192
3404
  }
3193
- return node.id;
3405
+ misses++;
3406
+ const ir = await graphIr(dir, opts);
3407
+ if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
3408
+ cache.delete(key);
3409
+ cache.set(key, { dir: resolve2(dir), stamp, ir });
3410
+ for (const oldest of cache.keys()) {
3411
+ if (cache.size <= size) break;
3412
+ cache.delete(oldest);
3413
+ }
3414
+ return structuredClone(ir);
3194
3415
  }
3195
- function boundManagedCluster(nodes, boundContext) {
3196
- const clusters = nodes.filter((n) => MANAGED_CLUSTER_KINDS.has(n.kind));
3197
- if (clusters.length === 1) return clusters[0];
3198
- if (clusters.length === 0 || !boundContext) return void 0;
3199
- const bound = clusters.filter((n) => contextBindsCluster(boundContext, managedClusterName(n)));
3200
- return bound.length === 1 ? bound[0] : void 0;
3201
- }
3202
- function addClusterAnchorEdges(ir, boundContext) {
3203
- const nodes = ir.nodes;
3204
- const cluster = boundManagedCluster(ir.nodes, boundContext);
3205
- if (!cluster) return ir;
3206
- const k8sNodes = nodes.filter(isK8s);
3207
- if (k8sNodes.length === 0) return ir;
3208
- const namespaceByName = /* @__PURE__ */ new Map();
3209
- for (const n of k8sNodes) {
3210
- if (!isNamespace(n)) continue;
3211
- const name = metaString(n, "name");
3212
- if (name && !namespaceByName.has(name)) namespaceByName.set(name, n.id);
3416
+
3417
+ // src/zoom-notes.ts
3418
+ function zoomNote(zoom, ir, compositeEdgesAttached) {
3419
+ const empty = ir.nodes.length === 0;
3420
+ if (zoom === "components") {
3421
+ return empty ? "no components discovered \u2014 nothing declares one in this project" : void 0;
3213
3422
  }
3214
- const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
3215
- const add = (from, to) => {
3216
- if (from === to) return;
3217
- const key = `${from}\0${to}`;
3218
- if (existing.has(key)) return;
3219
- existing.add(key);
3220
- ir.edges.push({ from, to, kind: "ref", viaAttr: ANCHOR_VIA, inferred: true });
3221
- };
3222
- for (const n of k8sNodes) {
3223
- if (isNamespace(n)) {
3224
- add(cluster.id, n.id);
3225
- continue;
3226
- }
3227
- const ns = metaString(n, "namespace");
3228
- const nsNode = ns ? namespaceByName.get(ns) : void 0;
3229
- add(nsNode ?? cluster.id, n.id);
3423
+ if (zoom === "logical") {
3424
+ return empty ? "logical is an AWS projection \u2014 no AWS resources in this estate" : void 0;
3230
3425
  }
3231
- return ir;
3232
- }
3233
-
3234
- // src/logical-azure.ts
3235
- var AZURE_HEADLINE_KINDS = /* @__PURE__ */ new Set([
3236
- // compute / workloads
3237
- "Microsoft.Compute/virtualMachines",
3238
- "Microsoft.Compute/virtualMachineScaleSets",
3239
- "Microsoft.ContainerService/managedClusters",
3240
- "Microsoft.ContainerInstance/containerGroups",
3241
- "Microsoft.Web/sites",
3242
- // network edge
3243
- "Microsoft.Network/publicIPAddresses",
3244
- "Microsoft.Network/loadBalancers",
3245
- "Microsoft.Network/applicationGateways",
3246
- "Microsoft.Network/natGateways",
3247
- "Microsoft.Network/dnsZones",
3248
- // data stores
3249
- "Microsoft.Storage/storageAccounts",
3250
- "Microsoft.Sql/servers",
3251
- "Microsoft.DocumentDB/databaseAccounts",
3252
- "Microsoft.Cache/redis",
3253
- "Microsoft.DBforPostgreSQL/flexibleServers",
3254
- // registry / identity edge
3255
- "Microsoft.ContainerRegistry/registries"
3256
- ]);
3257
- var AZURE_FABRIC_KINDS = /* @__PURE__ */ new Set([
3258
- "Microsoft.Network/virtualNetworks",
3259
- "Microsoft.Network/virtualNetworks_subnets",
3260
- "Microsoft.Network/networkSecurityGroups",
3261
- "Microsoft.Network/routeTables",
3262
- "Microsoft.Network/networkInterfaces",
3263
- // identity/authorization hubs — a role assignment or managed identity is
3264
- // referenced by half the estate, so contracting through one invents edges
3265
- // between unrelated resources.
3266
- "Microsoft.Authorization/roleAssignments",
3267
- "Microsoft.ManagedIdentity/userAssignedIdentities",
3268
- "Microsoft.KeyVault/vaults"
3269
- ]);
3270
- var VNET_KIND = "Microsoft.Network/virtualNetworks";
3271
- var SUBNET_KIND = "Microsoft.Network/virtualNetworks_subnets";
3272
- var NSG_KIND = "Microsoft.Network/networkSecurityGroups";
3273
- var RESOURCE_ID_RE = /resourceId\(\s*([^)]*)\)/g;
3274
- var QUOTED_RE = /'([^']*)'/g;
3275
- function resourceIdNames(value) {
3276
- if (typeof value !== "string" || !value.includes("resourceId(")) return [];
3277
- const out = [];
3278
- for (const call of value.matchAll(RESOURCE_ID_RE)) {
3279
- const args = [...call[1].matchAll(QUOTED_RE)].map((m) => m[1]);
3280
- if (args.length >= 2) out.push(args[args.length - 1]);
3426
+ if (zoom === "composites") {
3427
+ return compositeEdgesAttached === 0 ? "no component ownership to join \u2014 showing the resource graph unchanged" : void 0;
3281
3428
  }
3282
- return out;
3283
- }
3284
- function collectAzureRefs(value, out) {
3285
- if (value === null || typeof value !== "object") {
3286
- for (const name of resourceIdNames(value)) out.add(name);
3287
- return;
3429
+ if (zoom === "runtime") {
3430
+ const children = ir.nodes.filter((n) => n.runtimeOwner).length;
3431
+ return children === 0 ? "nothing below the declaration boundary \u2014 no owner-referenced children on this substrate" : void 0;
3288
3432
  }
3289
- const ref = value.$ref;
3290
- if (typeof ref === "string") out.add(ref.split(".")[0]);
3291
- for (const v of Object.values(value)) collectAzureRefs(v, out);
3433
+ return void 0;
3292
3434
  }
3293
- function strAttr(node, key) {
3294
- const v = node.attrs?.[key];
3295
- return typeof v === "string" ? v : void 0;
3435
+ function logicalKept(before, after) {
3436
+ if (after > 0 && after * 3 >= before) return void 0;
3437
+ return after === 0 ? `logical projected nothing from ${before} resources \u2014 it is a cloud-topology lens, and this estate declares none of the kinds it nests (behold#74)` : `logical kept ${after} of ${before} resources \u2014 it is a cloud-topology lens, and the rest are kinds it does not nest (behold#74)`;
3296
3438
  }
3297
- function resourceName(node) {
3298
- return strAttr(node, "name");
3439
+ function edgelessNote(zoom, ir, detail) {
3440
+ if (zoom === "components" || zoom === "logical") return void 0;
3441
+ if (ir.nodes.length === 0 || ir.edges.length > 0) return void 0;
3442
+ if (detail !== void 0 && detail < 3) {
3443
+ return "no edges at this detail \u2014 sourceRef/dependsOn and other attrs-derived references only appear at detail 3 (\u2318K \u2192 attributes, or add &detail=3)";
3444
+ }
3445
+ return "no edges \u2014 nothing in this estate references anything else";
3299
3446
  }
3300
- function subnetVnetName(node) {
3301
- const name = resourceName(node);
3302
- if (!name || !name.includes("/")) return void 0;
3303
- return name.split("/")[0];
3447
+ var CLUSTER_SCOPED = /* @__PURE__ */ new Set([
3448
+ "K8s::Core::Namespace",
3449
+ "K8s::Rbac::ClusterRole",
3450
+ "K8s::Rbac::ClusterRoleBinding",
3451
+ "K8s::Storage::StorageClass",
3452
+ "K8s::Apiextensions::CustomResourceDefinition"
3453
+ ]);
3454
+ function namespaceMismatchNote(nodes) {
3455
+ const k8s = nodes.filter((n) => n?.lexicon === "k8s");
3456
+ if (k8s.length === 0) return void 0;
3457
+ const pending = k8s.filter((n) => n.attrs?._status === "accent" && !CLUSTER_SCOPED.has(n.kind ?? ""));
3458
+ if (pending.length < 2) return void 0;
3459
+ if (pending.some((n) => typeof n.attrs?.metadata?.namespace === "string" && n.attrs.metadata.namespace)) return void 0;
3460
+ if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
3461
+ return `${pending.length} pending k8s objects declare no metadata.namespace \u2014 if a controller stamps it at apply time (e.g. Flux's targetNamespace), the live read looked in "default", not where they run`;
3304
3462
  }
3305
- function subnetShortName(node) {
3306
- const name = resourceName(node);
3307
- return name?.includes("/") ? name.split("/").slice(1).join("/") : name;
3463
+ function namespaceJoinNote(joined) {
3464
+ if (joined.length === 0) return void 0;
3465
+ const each = joined.map((j) => `${j.name} in "${j.namespace}"`).join(", ");
3466
+ return `read ${each} \u2014 the namespace the estate's own Kustomization targetNamespace binds, not the app project's own declaration`;
3308
3467
  }
3309
- function vnetCidr(node) {
3310
- const space = node.attrs?.addressSpace;
3311
- const prefixes = space?.addressPrefixes;
3312
- return Array.isArray(prefixes) && typeof prefixes[0] === "string" ? prefixes[0] : void 0;
3468
+ function notesFor(zoom, ir, compositeEdgesAttached, logicalBefore, detail) {
3469
+ const primary = zoom === "logical" && logicalBefore !== void 0 ? logicalKept(logicalBefore, ir.nodes.length) : zoomNote(zoom, ir, compositeEdgesAttached);
3470
+ const notes = [primary, edgelessNote(zoom, ir, detail)].filter((n) => n !== void 0);
3471
+ return notes.length ? notes.join(" \xB7 ") : void 0;
3313
3472
  }
3314
- function projectAzureLogical(ir, env) {
3315
- const azure = ir.nodes.filter((n) => n.lexicon === "azure");
3316
- if (azure.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
3317
- const componentOf = componentNamer(azure);
3318
- const byId = new Map(azure.map((n) => [n.id, n]));
3319
- const byName = /* @__PURE__ */ new Map();
3320
- for (const n of azure) {
3321
- const name = resourceName(n);
3322
- if (name && !byName.has(name)) byName.set(name, n);
3323
- }
3324
- const vnets = azure.filter((n) => n.kind === VNET_KIND);
3325
- const subnets = azure.filter((n) => n.kind === SUBNET_KIND);
3326
- const vnetByName = new Map(vnets.map((v) => [resourceName(v) ?? v.id, v]));
3327
- const refOut = /* @__PURE__ */ new Map();
3328
- for (const n of azure) {
3329
- const names = /* @__PURE__ */ new Set();
3330
- collectAzureRefs(n.attrs, names);
3331
- const targets = /* @__PURE__ */ new Set();
3332
- for (const name of names) {
3333
- const target = byName.get(name) ?? byId.get(name);
3334
- if (target && target.id !== n.id) targets.add(target.id);
3335
- }
3336
- if (targets.size) refOut.set(n.id, targets);
3337
- }
3338
- for (const e of ir.edges) {
3339
- if (!byId.has(e.from) || !byId.has(e.to)) continue;
3340
- (refOut.get(e.from) ?? refOut.set(e.from, /* @__PURE__ */ new Set()).get(e.from)).add(e.to);
3473
+ function tierMismatchNote(ir, tiers, currentTier) {
3474
+ const values = tiers?.values ?? [];
3475
+ if (values.length < 2) return void 0;
3476
+ const byLexicon = /* @__PURE__ */ new Map();
3477
+ for (const n of ir.nodes) {
3478
+ const status = n.attrs?._status;
3479
+ if (status !== "good" && status !== "accent") continue;
3480
+ const lexicon = n.lexicon ?? "?";
3481
+ const c = byLexicon.get(lexicon) ?? { good: 0, accent: 0 };
3482
+ c[status === "good" ? "good" : "accent"]++;
3483
+ byLexicon.set(lexicon, c);
3341
3484
  }
3342
- const RG_TITLE = env ? `resource group ${env}` : "resource group";
3343
- const vnetTitle = (v) => `VNet ${vnetCidr(v) ?? resourceName(v) ?? v.id}`;
3344
- const subnetTitle = (s) => `subnet ${strAttr(s, "addressPrefix") ?? subnetShortName(s) ?? s.id}`;
3345
- const guardedBy = /* @__PURE__ */ new Map();
3346
- for (const s of subnets) {
3347
- const names = /* @__PURE__ */ new Set();
3348
- collectAzureRefs(s.attrs?.networkSecurityGroup, names);
3349
- for (const name of names) {
3350
- if (byName.get(name)?.kind === NSG_KIND) guardedBy.set(s.id, name);
3351
- }
3485
+ for (const [lexicon, { good, accent }] of [...byLexicon].sort()) {
3486
+ if (accent < 2 || accent * 2 < good + accent) continue;
3487
+ const here = currentTier ? `tier "${currentTier}"` : "the default tier";
3488
+ const others = values.filter((v) => v !== currentTier).join(", ");
3489
+ return `${accent} of ${good + accent} ${lexicon} resources read "declared, not deployed" at ${here} \u2014 if the estate runs another declared tier (${others}), pick it (\u2318K \u2192 tier)`;
3352
3490
  }
3353
- const headline = azure.filter((n) => AZURE_HEADLINE_KINDS.has(n.kind));
3354
- const subnetIds = new Set(subnets.map((s) => s.id));
3355
- const vnetIds = new Set(vnets.map((v) => v.id));
3356
- const nearest = (start, want, maxDepth = 6) => {
3357
- const seen = /* @__PURE__ */ new Set([start]);
3358
- let frontier = [...refOut.get(start) ?? []];
3359
- for (let depth = 0; depth < maxDepth && frontier.length; depth++) {
3360
- const next = [];
3361
- for (const id of frontier) {
3362
- if (seen.has(id)) continue;
3363
- seen.add(id);
3364
- if (want.has(id)) return id;
3365
- for (const r of refOut.get(id) ?? []) if (!seen.has(r)) next.push(r);
3366
- }
3367
- frontier = next;
3491
+ return void 0;
3492
+ }
3493
+
3494
+ // src/estate.ts
3495
+ function estateReadPool(members, env = process.env) {
3496
+ const override = Number.parseInt(env.BEHOLD_ESTATE_CONCURRENCY ?? "", 10);
3497
+ const cap = Number.isInteger(override) && override >= 1 ? override : Math.min(4, availableParallelism());
3498
+ return Math.max(1, Math.min(cap, members));
3499
+ }
3500
+ async function mapPool(items, width, fn) {
3501
+ const results = new Array(items.length);
3502
+ let next = 0;
3503
+ const worker = async () => {
3504
+ for (let i = next++; i < items.length; i = next++) {
3505
+ results[i] = await fn(items[i], i);
3368
3506
  }
3369
- return void 0;
3370
- };
3371
- const byContainer = {};
3372
- const child = (parent, c) => {
3373
- const arr = byContainer[parent] ?? (byContainer[parent] = []);
3374
- if (!arr.includes(c)) arr.push(c);
3375
3507
  };
3376
- for (const v of vnets) child(RG_TITLE, vnetTitle(v));
3377
- for (const s of subnets) {
3378
- const parentVnet = subnetVnetName(s);
3379
- const v = parentVnet ? vnetByName.get(parentVnet) : void 0;
3380
- child(v ? vnetTitle(v) : RG_TITLE, subnetTitle(s));
3381
- }
3382
- const placeOf = /* @__PURE__ */ new Map();
3383
- for (const n of headline) {
3384
- const subnetId = nearest(n.id, subnetIds);
3385
- if (subnetId) {
3386
- placeOf.set(n.id, subnetTitle(byId.get(subnetId)));
3387
- continue;
3388
- }
3389
- const vnetId = nearest(n.id, vnetIds);
3508
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(width, items.length)) }, () => worker()));
3509
+ return results;
3510
+ }
3511
+ function estateMembers(projectDirs) {
3512
+ const names = shortStackNames([...projectDirs]);
3513
+ return projectDirs.map((dir, i) => ({ name: names[i], dir }));
3514
+ }
3515
+ async function composeEstate(projectDirs, opts = {}) {
3516
+ const names = shortStackNames(projectDirs);
3517
+ const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
3518
+ name: names[i],
3519
+ ir: await memberIr(dir, opts)
3520
+ }));
3521
+ return composeStacks(stacks);
3522
+ }
3523
+ var firstLine2 = (e) => {
3524
+ const msg = e instanceof Error ? e.message : String(e);
3525
+ const m = msg.match(/exited \d+:\s*([\s\S]*)/);
3526
+ return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
3527
+ };
3528
+ function namespaceRuntimeOwners(name, ir) {
3529
+ const own = new Set(ir.nodes.map((n) => n.id));
3530
+ for (const n of ir.nodes) {
3531
+ if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
3532
+ }
3533
+ return ir;
3534
+ }
3535
+ var NAMESPACE_JOIN_FLOOR = "0.44.5";
3536
+ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
3537
+ var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
3538
+ var realIsDir = (p) => {
3539
+ try {
3540
+ return statSync3(p).isDirectory();
3541
+ } catch {
3542
+ return false;
3543
+ }
3544
+ };
3545
+ function declaredNamespaceBindings(ir) {
3546
+ const out = [];
3547
+ for (const n of ir.nodes) {
3548
+ if (n.kind !== "K8s::Flux::Kustomization") continue;
3549
+ const spec = rec3(n.attrs?.spec);
3550
+ const namespace = str3(spec?.targetNamespace);
3551
+ const path = str3(spec?.path);
3552
+ if (namespace && path) out.push({ path, namespace });
3553
+ }
3554
+ return out;
3555
+ }
3556
+ function awaitsNamespaceBinding(ir) {
3557
+ const namespaced = ir.nodes.filter((n) => n.lexicon === "k8s" && !CLUSTER_SCOPED.has(n.kind ?? ""));
3558
+ if (namespaced.length === 0) return false;
3559
+ return namespaced.every((n) => !str3(rec3(n.attrs?.metadata)?.namespace));
3560
+ }
3561
+ function declaredSegments(path) {
3562
+ if (path.startsWith("/")) return void 0;
3563
+ const segs = path.split("/").filter((s) => s !== "" && s !== ".");
3564
+ if (segs.length === 0 || segs.includes("..")) return void 0;
3565
+ return segs;
3566
+ }
3567
+ function pathAlignment(declared, dir, isDir = realIsDir) {
3568
+ const want = declaredSegments(declared);
3569
+ if (!want) return 0;
3570
+ const root = resolve3(dir);
3571
+ const have = root.split(sep).filter(Boolean);
3572
+ for (let k = Math.min(want.length, have.length); k >= 1; k--) {
3573
+ if (!have.slice(-k).every((s, i) => s === want[i])) continue;
3574
+ if (!isDir(join7(root, ...want.slice(k)))) continue;
3575
+ return k;
3576
+ }
3577
+ return 0;
3578
+ }
3579
+ function joinNamespaceBindings(members, isDir = realIsDir) {
3580
+ const bindings = members.flatMap(
3581
+ (m) => m.ir ? declaredNamespaceBindings(m.ir).map((b) => ({ ...b, declaredBy: m.dir })) : []
3582
+ );
3583
+ if (bindings.length === 0) return [];
3584
+ const awaiting = members.filter((m) => m.ir && awaitsNamespaceBinding(m.ir));
3585
+ if (awaiting.length === 0) return [];
3586
+ const best = /* @__PURE__ */ new Map();
3587
+ for (const b of bindings) {
3588
+ let winner;
3589
+ let tied = false;
3590
+ for (const m of awaiting) {
3591
+ if (m.dir === b.declaredBy) continue;
3592
+ const score = pathAlignment(b.path, m.dir, isDir);
3593
+ if (score === 0) continue;
3594
+ if (!winner || score > winner.score) {
3595
+ winner = { dir: m.dir, score };
3596
+ tied = false;
3597
+ } else if (score === winner.score) {
3598
+ tied = true;
3599
+ }
3600
+ }
3601
+ if (!winner || tied) continue;
3602
+ const prev = best.get(winner.dir);
3603
+ if (prev === "ambiguous") continue;
3604
+ const join21 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
3605
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join21 });
3606
+ else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
3607
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join21 });
3608
+ }
3609
+ return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
3610
+ }
3611
+ async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
3612
+ if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
3613
+ const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
3614
+ const irs = await mapPool(
3615
+ projectDirs,
3616
+ estateReadPool(projectDirs.length),
3617
+ (dir) => memberIr(dir, { ...src, detail: 3 }).catch(() => void 0)
3618
+ );
3619
+ const scopes = /* @__PURE__ */ new Map();
3620
+ for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
3621
+ if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
3622
+ scopes.set(j.dir, j.namespace);
3623
+ }
3624
+ return scopes;
3625
+ }
3626
+ function withoutJoinedMembers(nodes, joined) {
3627
+ if (joined.length === 0) return [...nodes];
3628
+ return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
3629
+ }
3630
+ async function composeEstateOverlay(projectDirs, opts, classify) {
3631
+ const names = shortStackNames(projectDirs);
3632
+ const unobserved = [];
3633
+ const dropped = [];
3634
+ const joined = [];
3635
+ const stacks = new Array(projectDirs.length);
3636
+ const scopes = await estateNamespaceScopes(projectDirs, opts);
3637
+ await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
3638
+ const name = names[i];
3639
+ const namespace = scopes.get(dir);
3640
+ try {
3641
+ const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
3642
+ stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
3643
+ if (namespace) joined.push({ name, namespace });
3644
+ } catch (err) {
3645
+ const reason = firstLine2(err);
3646
+ try {
3647
+ const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
3648
+ const src = await memberIr(dir, srcOpts);
3649
+ for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
3650
+ stacks[i] = { name, ir: src };
3651
+ unobserved.push({ name, reason });
3652
+ } catch (err2) {
3653
+ dropped.push({ name, reason: firstLine2(err2) });
3654
+ }
3655
+ }
3656
+ });
3657
+ const present = stacks.filter((s) => !!s);
3658
+ return {
3659
+ ir: composeStacks(present),
3660
+ observed: present.length - unobserved.length,
3661
+ total: projectDirs.length,
3662
+ unobserved,
3663
+ dropped,
3664
+ // Concurrent reads finish in whatever order the clusters answer; the note
3665
+ // this feeds should read the same twice.
3666
+ joined: joined.sort((a, b) => a.name.localeCompare(b.name))
3667
+ };
3668
+ }
3669
+
3670
+ // src/helm-drift.ts
3671
+ function countChanges(docs) {
3672
+ return docs.reduce((n, d) => n + d.changes.length, 0);
3673
+ }
3674
+ function provenanceOf(live) {
3675
+ const m = live.manifest;
3676
+ return {
3677
+ contentDigest: m.contentDigest,
3678
+ inputDigest: m.inputDigest,
3679
+ valuesDigest: m.valuesDigest,
3680
+ chart: m.chart,
3681
+ chartVersion: m.chartVersion,
3682
+ repo: m.repo,
3683
+ releaseName: m.releaseName,
3684
+ namespace: m.namespace,
3685
+ profile: m.capabilityProfile?.cluster ?? "",
3686
+ kubeVersion: m.capabilityProfile?.kubeVersion ?? "",
3687
+ renderedAt: m.renderedAt,
3688
+ helmVersion: m.helmVersion,
3689
+ chantVersion: m.chantVersion,
3690
+ sourceRef: m.sourceRef
3691
+ };
3692
+ }
3693
+ function helmRenderReport(record, live) {
3694
+ const base = { render: record.name, chart: record.chart };
3695
+ if (!live) {
3696
+ return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
3697
+ }
3698
+ const d = live.diff;
3699
+ const counts = {
3700
+ drifted: d.drifted.length,
3701
+ changes: countChanges(d.drifted),
3702
+ unchanged: d.unchanged.length,
3703
+ unobserved: d.unobserved.length,
3704
+ undeclared: d.undeclaredEntities.length
3705
+ };
3706
+ const provenance = provenanceOf(live);
3707
+ if (d.drifted.length > 0) {
3708
+ return { ...base, verdict: "drifted", counts, provenance, drifted: d.drifted };
3709
+ }
3710
+ if (d.drifted.length + d.accepted.length + d.unchanged.length === 0) {
3711
+ return { ...base, verdict: "unobserved", reason: "nothing-observed", counts, provenance };
3712
+ }
3713
+ if (d.unobserved.length > 0) {
3714
+ return { ...base, verdict: "unobserved", reason: "partly-observed", counts, provenance };
3715
+ }
3716
+ return { ...base, verdict: "in-sync", counts, provenance };
3717
+ }
3718
+ async function readHelmRenderDrift(projectDir, env, opts = {}) {
3719
+ const report = await helmRenders(projectDir, opts).catch(() => void 0);
3720
+ const records = report?.records ?? [];
3721
+ if (records.length === 0) return [];
3722
+ return mapPool(records, estateReadPool(records.length), async (record) => {
3723
+ if (!record.contentDigest) return helmRenderReport(record, void 0);
3724
+ const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
3725
+ return helmRenderReport(record, live);
3726
+ });
3727
+ }
3728
+ function chartNameOf2(node) {
3729
+ const name = node.attrs?.name;
3730
+ return typeof name === "string" && name.length > 0 ? name : void 0;
3731
+ }
3732
+ var SEVERITY = { drifted: 2, unobserved: 1, "in-sync": 0 };
3733
+ function applyHelmRenderDrift(ir, reports) {
3734
+ if (reports.length === 0) return { charts: 0, drifted: 0 };
3735
+ const byChart = /* @__PURE__ */ new Map();
3736
+ for (const r of reports) {
3737
+ const list3 = byChart.get(r.chart);
3738
+ if (list3) list3.push(r);
3739
+ else byChart.set(r.chart, [r]);
3740
+ }
3741
+ let charts = 0;
3742
+ let drifted = 0;
3743
+ for (const node of ir.nodes) {
3744
+ if (node.lexicon !== "helm" || node.kind !== "Helm::Chart") continue;
3745
+ const chartName = chartNameOf2(node);
3746
+ const matched = chartName ? byChart.get(chartName) : void 0;
3747
+ if (!matched || matched.length === 0) continue;
3748
+ const worst = matched.reduce((a, b) => SEVERITY[b.verdict] > SEVERITY[a.verdict] ? b : a);
3749
+ const attrs = node.attrs ??= {};
3750
+ attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
3751
+ charts++;
3752
+ if (worst.verdict === "drifted") {
3753
+ attrs._status = "warn";
3754
+ drifted++;
3755
+ }
3756
+ }
3757
+ return { charts, drifted };
3758
+ }
3759
+
3760
+ // src/cluster-anchor.ts
3761
+ var MANAGED_CLUSTER_KINDS = /* @__PURE__ */ new Set([
3762
+ "AWS::EKS::Cluster",
3763
+ "GCP::Container::Cluster",
3764
+ "Microsoft.ContainerService/managedClusters",
3765
+ "K3d::Cluster"
3766
+ ]);
3767
+ var ANCHOR_VIA = "runs-on";
3768
+ function metadata(node) {
3769
+ const m = node.attrs?.metadata;
3770
+ return m && typeof m === "object" && !Array.isArray(m) ? m : void 0;
3771
+ }
3772
+ function metaString(node, key) {
3773
+ const v = metadata(node)?.[key];
3774
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3775
+ }
3776
+ function isK8s(node) {
3777
+ return node.lexicon === "k8s";
3778
+ }
3779
+ function isNamespace(node) {
3780
+ return node.kind === "K8s::Core::Namespace";
3781
+ }
3782
+ function managedClusterName(node) {
3783
+ const metaName = metaString(node, "name");
3784
+ if (metaName) return metaName;
3785
+ for (const key of ["name", "clusterName"]) {
3786
+ const v = node.attrs?.[key];
3787
+ if (typeof v === "string" && v.length > 0) return v;
3788
+ }
3789
+ return node.id;
3790
+ }
3791
+ function boundManagedCluster(nodes, boundContext) {
3792
+ const clusters = nodes.filter((n) => MANAGED_CLUSTER_KINDS.has(n.kind));
3793
+ if (clusters.length === 1) return clusters[0];
3794
+ if (clusters.length === 0 || !boundContext) return void 0;
3795
+ const bound = clusters.filter((n) => contextBindsCluster(boundContext, managedClusterName(n)));
3796
+ return bound.length === 1 ? bound[0] : void 0;
3797
+ }
3798
+ function addClusterAnchorEdges(ir, boundContext) {
3799
+ const nodes = ir.nodes;
3800
+ const cluster = boundManagedCluster(ir.nodes, boundContext);
3801
+ if (!cluster) return ir;
3802
+ const k8sNodes = nodes.filter(isK8s);
3803
+ if (k8sNodes.length === 0) return ir;
3804
+ const namespaceByName = /* @__PURE__ */ new Map();
3805
+ for (const n of k8sNodes) {
3806
+ if (!isNamespace(n)) continue;
3807
+ const name = metaString(n, "name");
3808
+ if (name && !namespaceByName.has(name)) namespaceByName.set(name, n.id);
3809
+ }
3810
+ const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
3811
+ const add = (from, to) => {
3812
+ if (from === to) return;
3813
+ const key = `${from}\0${to}`;
3814
+ if (existing.has(key)) return;
3815
+ existing.add(key);
3816
+ ir.edges.push({ from, to, kind: "ref", viaAttr: ANCHOR_VIA, inferred: true });
3817
+ };
3818
+ for (const n of k8sNodes) {
3819
+ if (isNamespace(n)) {
3820
+ add(cluster.id, n.id);
3821
+ continue;
3822
+ }
3823
+ const ns = metaString(n, "namespace");
3824
+ const nsNode = ns ? namespaceByName.get(ns) : void 0;
3825
+ add(nsNode ?? cluster.id, n.id);
3826
+ }
3827
+ return ir;
3828
+ }
3829
+
3830
+ // src/logical-azure.ts
3831
+ var AZURE_HEADLINE_KINDS = /* @__PURE__ */ new Set([
3832
+ // compute / workloads
3833
+ "Microsoft.Compute/virtualMachines",
3834
+ "Microsoft.Compute/virtualMachineScaleSets",
3835
+ "Microsoft.ContainerService/managedClusters",
3836
+ "Microsoft.ContainerInstance/containerGroups",
3837
+ "Microsoft.Web/sites",
3838
+ // network edge
3839
+ "Microsoft.Network/publicIPAddresses",
3840
+ "Microsoft.Network/loadBalancers",
3841
+ "Microsoft.Network/applicationGateways",
3842
+ "Microsoft.Network/natGateways",
3843
+ "Microsoft.Network/dnsZones",
3844
+ // data stores
3845
+ "Microsoft.Storage/storageAccounts",
3846
+ "Microsoft.Sql/servers",
3847
+ "Microsoft.DocumentDB/databaseAccounts",
3848
+ "Microsoft.Cache/redis",
3849
+ "Microsoft.DBforPostgreSQL/flexibleServers",
3850
+ // registry / identity edge
3851
+ "Microsoft.ContainerRegistry/registries"
3852
+ ]);
3853
+ var AZURE_FABRIC_KINDS = /* @__PURE__ */ new Set([
3854
+ "Microsoft.Network/virtualNetworks",
3855
+ "Microsoft.Network/virtualNetworks_subnets",
3856
+ "Microsoft.Network/networkSecurityGroups",
3857
+ "Microsoft.Network/routeTables",
3858
+ "Microsoft.Network/networkInterfaces",
3859
+ // identity/authorization hubs — a role assignment or managed identity is
3860
+ // referenced by half the estate, so contracting through one invents edges
3861
+ // between unrelated resources.
3862
+ "Microsoft.Authorization/roleAssignments",
3863
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
3864
+ "Microsoft.KeyVault/vaults"
3865
+ ]);
3866
+ var VNET_KIND = "Microsoft.Network/virtualNetworks";
3867
+ var SUBNET_KIND = "Microsoft.Network/virtualNetworks_subnets";
3868
+ var NSG_KIND = "Microsoft.Network/networkSecurityGroups";
3869
+ var RESOURCE_ID_RE = /resourceId\(\s*([^)]*)\)/g;
3870
+ var QUOTED_RE = /'([^']*)'/g;
3871
+ function resourceIdNames(value) {
3872
+ if (typeof value !== "string" || !value.includes("resourceId(")) return [];
3873
+ const out = [];
3874
+ for (const call of value.matchAll(RESOURCE_ID_RE)) {
3875
+ const args = [...call[1].matchAll(QUOTED_RE)].map((m) => m[1]);
3876
+ if (args.length >= 2) out.push(args[args.length - 1]);
3877
+ }
3878
+ return out;
3879
+ }
3880
+ function collectAzureRefs(value, out) {
3881
+ if (value === null || typeof value !== "object") {
3882
+ for (const name of resourceIdNames(value)) out.add(name);
3883
+ return;
3884
+ }
3885
+ const ref = value.$ref;
3886
+ if (typeof ref === "string") out.add(ref.split(".")[0]);
3887
+ for (const v of Object.values(value)) collectAzureRefs(v, out);
3888
+ }
3889
+ function strAttr(node, key) {
3890
+ const v = node.attrs?.[key];
3891
+ return typeof v === "string" ? v : void 0;
3892
+ }
3893
+ function resourceName(node) {
3894
+ return strAttr(node, "name");
3895
+ }
3896
+ function subnetVnetName(node) {
3897
+ const name = resourceName(node);
3898
+ if (!name || !name.includes("/")) return void 0;
3899
+ return name.split("/")[0];
3900
+ }
3901
+ function subnetShortName(node) {
3902
+ const name = resourceName(node);
3903
+ return name?.includes("/") ? name.split("/").slice(1).join("/") : name;
3904
+ }
3905
+ function vnetCidr(node) {
3906
+ const space = node.attrs?.addressSpace;
3907
+ const prefixes = space?.addressPrefixes;
3908
+ return Array.isArray(prefixes) && typeof prefixes[0] === "string" ? prefixes[0] : void 0;
3909
+ }
3910
+ function projectAzureLogical(ir, env) {
3911
+ const azure = ir.nodes.filter((n) => n.lexicon === "azure");
3912
+ if (azure.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
3913
+ const componentOf = componentNamer(azure);
3914
+ const byId = new Map(azure.map((n) => [n.id, n]));
3915
+ const byName = /* @__PURE__ */ new Map();
3916
+ for (const n of azure) {
3917
+ const name = resourceName(n);
3918
+ if (name && !byName.has(name)) byName.set(name, n);
3919
+ }
3920
+ const vnets = azure.filter((n) => n.kind === VNET_KIND);
3921
+ const subnets = azure.filter((n) => n.kind === SUBNET_KIND);
3922
+ const vnetByName = new Map(vnets.map((v) => [resourceName(v) ?? v.id, v]));
3923
+ const refOut = /* @__PURE__ */ new Map();
3924
+ for (const n of azure) {
3925
+ const names = /* @__PURE__ */ new Set();
3926
+ collectAzureRefs(n.attrs, names);
3927
+ const targets = /* @__PURE__ */ new Set();
3928
+ for (const name of names) {
3929
+ const target = byName.get(name) ?? byId.get(name);
3930
+ if (target && target.id !== n.id) targets.add(target.id);
3931
+ }
3932
+ if (targets.size) refOut.set(n.id, targets);
3933
+ }
3934
+ for (const e of ir.edges) {
3935
+ if (!byId.has(e.from) || !byId.has(e.to)) continue;
3936
+ (refOut.get(e.from) ?? refOut.set(e.from, /* @__PURE__ */ new Set()).get(e.from)).add(e.to);
3937
+ }
3938
+ const RG_TITLE = env ? `resource group ${env}` : "resource group";
3939
+ const vnetTitle = (v) => `VNet ${vnetCidr(v) ?? resourceName(v) ?? v.id}`;
3940
+ const subnetTitle = (s) => `subnet ${strAttr(s, "addressPrefix") ?? subnetShortName(s) ?? s.id}`;
3941
+ const guardedBy = /* @__PURE__ */ new Map();
3942
+ for (const s of subnets) {
3943
+ const names = /* @__PURE__ */ new Set();
3944
+ collectAzureRefs(s.attrs?.networkSecurityGroup, names);
3945
+ for (const name of names) {
3946
+ if (byName.get(name)?.kind === NSG_KIND) guardedBy.set(s.id, name);
3947
+ }
3948
+ }
3949
+ const headline = azure.filter((n) => AZURE_HEADLINE_KINDS.has(n.kind));
3950
+ const subnetIds = new Set(subnets.map((s) => s.id));
3951
+ const vnetIds = new Set(vnets.map((v) => v.id));
3952
+ const nearest = (start, want, maxDepth = 6) => {
3953
+ const seen = /* @__PURE__ */ new Set([start]);
3954
+ let frontier = [...refOut.get(start) ?? []];
3955
+ for (let depth = 0; depth < maxDepth && frontier.length; depth++) {
3956
+ const next = [];
3957
+ for (const id of frontier) {
3958
+ if (seen.has(id)) continue;
3959
+ seen.add(id);
3960
+ if (want.has(id)) return id;
3961
+ for (const r of refOut.get(id) ?? []) if (!seen.has(r)) next.push(r);
3962
+ }
3963
+ frontier = next;
3964
+ }
3965
+ return void 0;
3966
+ };
3967
+ const byContainer = {};
3968
+ const child = (parent, c) => {
3969
+ const arr = byContainer[parent] ?? (byContainer[parent] = []);
3970
+ if (!arr.includes(c)) arr.push(c);
3971
+ };
3972
+ for (const v of vnets) child(RG_TITLE, vnetTitle(v));
3973
+ for (const s of subnets) {
3974
+ const parentVnet = subnetVnetName(s);
3975
+ const v = parentVnet ? vnetByName.get(parentVnet) : void 0;
3976
+ child(v ? vnetTitle(v) : RG_TITLE, subnetTitle(s));
3977
+ }
3978
+ const placeOf = /* @__PURE__ */ new Map();
3979
+ for (const n of headline) {
3980
+ const subnetId = nearest(n.id, subnetIds);
3981
+ if (subnetId) {
3982
+ placeOf.set(n.id, subnetTitle(byId.get(subnetId)));
3983
+ continue;
3984
+ }
3985
+ const vnetId = nearest(n.id, vnetIds);
3390
3986
  placeOf.set(n.id, vnetId ? vnetTitle(byId.get(vnetId)) : RG_TITLE);
3391
3987
  }
3392
3988
  const byComponent = /* @__PURE__ */ new Map();
@@ -4534,9 +5130,9 @@ function helmIconFor(kind) {
4534
5130
  var SUPPORTED_MAJOR = 1;
4535
5131
  var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
4536
5132
  var HOW_TO_GET_ONE = "Generate one with `chant carve advise --from <terraform-dir> --report report.json` (chant's read-only Terraform peelability advisor), then `behold carve report.json`.";
4537
- var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5133
+ var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4538
5134
  function parseCarveReport(value) {
4539
- if (!isRecord(value)) {
5135
+ if (!isRecord2(value)) {
4540
5136
  return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
4541
5137
  }
4542
5138
  if (!Array.isArray(value.resources)) {
@@ -4546,7 +5142,7 @@ function parseCarveReport(value) {
4546
5142
  );
4547
5143
  }
4548
5144
  const bad = value.resources.findIndex(
4549
- (r) => !isRecord(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
5145
+ (r) => !isRecord2(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
4550
5146
  );
4551
5147
  if (bad >= 0) {
4552
5148
  return refuse(
@@ -4612,7 +5208,7 @@ function boundaryEdgesOf(r) {
4612
5208
  const b = r.boundary;
4613
5209
  if (!b) return [];
4614
5210
  const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
4615
- return list3.filter((e) => isRecord(e) && typeof e.survivor === "string" && typeof e.carved === "string");
5211
+ return list3.filter((e) => isRecord2(e) && typeof e.survivor === "string" && typeof e.carved === "string");
4616
5212
  }
4617
5213
  function scoreArithmetic(r) {
4618
5214
  const b = r.breakdown;
@@ -4728,10 +5324,10 @@ import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as
4728
5324
  import { dirname as dirname4, join as join10 } from "node:path";
4729
5325
  var CARVE_MANIFEST_SUFFIX = ".carve.json";
4730
5326
  var APPLY_IS_HUMAN = "`chant carve apply` graduates ownership \u2014 it resolves the marker that makes chant the owner of a live resource. behold shows what the manifest records and echoes the command; it has no apply endpoint and no apply button, by design. Read the graduation plan, then run it yourself.";
4731
- var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5327
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4732
5328
  var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
4733
5329
  function parseCarveManifest(value) {
4734
- if (!isRecord2(value)) return null;
5330
+ if (!isRecord3(value)) return null;
4735
5331
  if (value.version !== 1) return null;
4736
5332
  if (typeof value.target !== "string" || !value.target) return null;
4737
5333
  return value;
@@ -4975,9 +5571,9 @@ function carveStateNote(progress, states) {
4975
5571
  var SUPPORTED_MAJOR2 = 1;
4976
5572
  var refuse2 = (error, remedy) => ({ ok: false, refusal: { error, code: "op-ir", remedy } });
4977
5573
  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.";
4978
- var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5574
+ var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4979
5575
  function parseOpIr(value) {
4980
- if (!isRecord3(value)) {
5576
+ if (!isRecord4(value)) {
4981
5577
  return refuse2("That file isn't an op.json \u2014 its top level isn't a JSON object.", HOW_TO_EMIT);
4982
5578
  }
4983
5579
  if (typeof value.name !== "string" || !Array.isArray(value.phases)) {
@@ -4986,7 +5582,7 @@ function parseOpIr(value) {
4986
5582
  HOW_TO_EMIT
4987
5583
  );
4988
5584
  }
4989
- const bad = value.phases.findIndex((p) => !isRecord3(p) || typeof p.name !== "string" || !Array.isArray(p.steps));
5585
+ const bad = value.phases.findIndex((p) => !isRecord4(p) || typeof p.name !== "string" || !Array.isArray(p.steps));
4990
5586
  if (bad >= 0) {
4991
5587
  return refuse2(
4992
5588
  `That JSON has a \`phases\` array, but entry ${bad} isn't a phase (needs a string \`name\` and a \`steps\` array).`,
@@ -5083,14 +5679,14 @@ function opPlacedSteps(op) {
5083
5679
  }
5084
5680
  var RULE_BOX = (opName) => `${opName} \xB7 rule table`;
5085
5681
  var ruleId = (opName, rule) => `${opName}/rules/${rule.id}`;
5086
- var isPredicate = (v) => isRecord3(v) && typeof v.kind === "string";
5682
+ var isPredicate = (v) => isRecord4(v) && typeof v.kind === "string";
5087
5683
  function isConvergeRule(v) {
5088
- if (!isRecord3(v)) return false;
5684
+ if (!isRecord4(v)) return false;
5089
5685
  if (typeof v.id !== "string" || !v.id) return false;
5090
5686
  if (typeof v.why !== "string" || !v.why) return false;
5091
5687
  if (!isPredicate(v.when)) return false;
5092
5688
  const then = v.then;
5093
- if (!isRecord3(then)) return false;
5689
+ if (!isRecord4(then)) return false;
5094
5690
  return then.kind === "run" && typeof then.op === "string" || then.kind === "report" && typeof then.reason === "string";
5095
5691
  }
5096
5692
  function convergeTable(op) {
@@ -6029,973 +6625,691 @@ function capture(cmd, args, cwd) {
6029
6625
  child.on("error", () => res({ code: -1, stdout: out }));
6030
6626
  child.on("close", (code) => res({ code: code ?? 1, stdout: out }));
6031
6627
  });
6032
- }
6033
- async function bootScratchFloci() {
6034
- const ps = await capture("docker", ["ps", "-a", "--format", "{{.Names}}"], process.cwd());
6035
- if (ps.code !== 0) return "docker isn't answering \u2014 is the daemon up?";
6036
- if (ps.stdout.split("\n").includes(LIVE_CONTAINER)) {
6037
- return `container ${LIVE_CONTAINER} already exists \u2014 a previous run didn't tear down. \`docker rm -f ${LIVE_CONTAINER}\` and re-run.`;
6038
- }
6039
- const run3 = await capture(
6040
- "docker",
6041
- ["run", "-d", "--rm", "-p", `${LIVE_PORT}:4566`, "--name", LIVE_CONTAINER, LIVE_IMAGE],
6042
- process.cwd()
6043
- );
6044
- if (run3.code !== 0) return `docker run ${LIVE_IMAGE} failed (${run3.code})`;
6045
- for (let i = 0; i < 30; i++) {
6046
- try {
6047
- const health = await fetch(`http://localhost:${LIVE_PORT}/_localstack/health`);
6048
- if (health.ok && (await health.text()).includes("cloudformation")) return void 0;
6049
- } catch {
6050
- }
6051
- await new Promise((r) => setTimeout(r, 1e3));
6052
- }
6053
- await teardownScratchFloci();
6054
- return `Floci never reported healthy on :${LIVE_PORT} after 30s`;
6055
- }
6056
- async function teardownScratchFloci() {
6057
- await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
6058
- }
6059
- async function applyIntoFloci(fromDir, step) {
6060
- const template = join12(fromDir, "floci-override.tf.disabled");
6061
- if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
6062
- writeFileSync3(join12(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
6063
- const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
6064
- if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
6065
- const apply = await step(
6066
- "terraform",
6067
- ["apply", "-input=false", "-auto-approve", "-no-color", ...LIVE_TARGETS.map((t) => `-target=${t}`)],
6068
- fromDir
6069
- );
6070
- if (apply !== 0) return `terraform apply exited ${apply}`;
6071
- return void 0;
6072
- }
6073
- async function runLivePlan(fromDir) {
6074
- const r = await capture("terraform", ["plan", "-input=false", "-no-color", "-detailed-exitcode", ...LIVE_TARGETS.map((t) => `-target=${t}`)], fromDir);
6075
- if (r.code !== 0 && r.code !== 2) {
6076
- const tail = r.stdout.trim().split("\n").slice(-8).join("\n");
6077
- return { error: `terraform plan failed (exit ${r.code}):
6078
- ${tail}`, exitCode: r.code };
6079
- }
6080
- return { ...parsePlanOutput(r.stdout, r.code), exitCode: r.code };
6081
- }
6082
-
6083
- // src/carve-actions.ts
6084
- async function runCarvePlan(demo) {
6085
- if (!demo?.live) {
6086
- return refuse3(
6087
- "carve-action",
6088
- "terraform plan is the live tier's beat, and this server isn't serving one",
6089
- "run `behold demo carve --live` (needs docker + terraform)"
6090
- );
6091
- }
6092
- const r = await runLivePlan(demo.from);
6093
- if ("error" in r) {
6094
- return refuse3("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
6095
- }
6096
- return {
6097
- ok: true,
6098
- command: "terraform plan -detailed-exitcode (targeted, in the demo copy)",
6099
- planLine: r.planLine,
6100
- changes: r.changes,
6101
- noDestroy: r.noDestroy
6102
- };
6103
- }
6104
- async function runCarveObserve(demo, select) {
6105
- if (!demo.live) {
6106
- return refuse3(
6107
- "carve-action",
6108
- "the observe beat reads the scratch Floci, and this server isn't serving one",
6109
- "run `behold demo carve --live` (needs docker + terraform)"
6110
- );
6111
- }
6112
- if (!existsSync8(join13(demo.out, "src"))) {
6113
- return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
6114
- }
6115
- const args = ["lifecycle", "diff", "prod", "--live", "--json"];
6116
- const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
6117
- code: 127,
6118
- stdout: "",
6119
- stderr: err instanceof Error ? err.message : String(err)
6120
- }));
6121
- if (run3.code !== 0) {
6122
- return refuse3(
6123
- "carve-action",
6124
- `chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
6125
- "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
6126
- );
6127
- }
6128
- let parsed;
6129
- try {
6130
- parsed = JSON.parse(run3.stdout);
6131
- } catch {
6132
- return refuse3("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
6133
- }
6134
- if (parsed.stacks && !parsed.lexicons) {
6135
- return refuse3(
6136
- "carve-action",
6137
- "chant lifecycle diff answered a stacked shape ({stacks}) \u2014 observe only reads the stack-less {lexicons} shape",
6138
- "point the target at a stack-less carveout, or file a shape-read gap against observe for the stacked case"
6139
- );
6140
- }
6141
- const aws = parsed.lexicons?.aws ?? {};
6142
- const entity = select.split(".").pop() ?? select;
6143
- const command = `AWS_ENDPOINT_URL=${demo.live.endpoint} chant ${args.join(" ")} # in ${shortenIn(demo.out, demo.root)}`;
6144
- const queried = aws.resources?.queried?.[entity];
6145
- const meta = aws.observed?.[entity];
6146
- if (meta) {
6147
- return {
6148
- ok: true,
6149
- select,
6150
- command,
6151
- entity,
6152
- verdict: "observed",
6153
- ...meta.status ? { status: meta.status } : {},
6154
- ...meta.ownership ? { ownership: meta.ownership } : {},
6155
- ...meta.physicalId ? { physicalId: meta.physicalId } : {},
6156
- ...queried ? { queried } : {}
6157
- };
6158
- }
6159
- const hole = aws.resources?.unobserved?.find((u) => u.name === entity);
6160
- if (hole) {
6161
- return {
6162
- ok: true,
6163
- select,
6164
- command,
6165
- entity,
6166
- verdict: "unobserved",
6167
- ...hole.detail || hole.reason ? { detail: [hole.reason, hole.detail].filter(Boolean).join(": ") } : {},
6168
- ...queried ? { queried } : {}
6169
- };
6170
- }
6171
- return { ok: true, select, command, entity, verdict: "missing", ...queried ? { queried } : {} };
6172
- }
6173
- var MAX_ARTIFACT_BYTES = 64 * 1024;
6174
- var MAX_ARTIFACTS = 24;
6175
- var MAX_OUTPUT_BYTES = 32 * 1024;
6176
- var refuse3 = (code, error, remedy) => ({
6177
- ok: false,
6178
- refusal: { error, code, remedy }
6179
- });
6180
- 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.";
6181
- function insideDemo(root, p) {
6182
- const rel = relative2(resolve4(root), resolve4(p));
6183
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
6184
- }
6185
- function selectFromReport(report, raw) {
6186
- if (typeof raw !== "string" || !raw || raw.length > 512) return null;
6187
- return (report?.resources ?? []).some((r) => r.address === raw) ? raw : null;
6188
- }
6189
- function carveWriteBlock(demo) {
6190
- if (!demo) {
6191
- return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
6192
- }
6193
- if (!existsSync8(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
6194
- if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
6195
- return unwritableReason(demo.root);
6196
- }
6197
- function readArtifacts(demo, dir, filter) {
6198
- const out = [];
6199
- const walk = (d, depth) => {
6200
- if (out.length >= MAX_ARTIFACTS || depth > 2) return;
6201
- let entries;
6628
+ }
6629
+ async function bootScratchFloci() {
6630
+ const ps = await capture("docker", ["ps", "-a", "--format", "{{.Names}}"], process.cwd());
6631
+ if (ps.code !== 0) return "docker isn't answering \u2014 is the daemon up?";
6632
+ if (ps.stdout.split("\n").includes(LIVE_CONTAINER)) {
6633
+ return `container ${LIVE_CONTAINER} already exists \u2014 a previous run didn't tear down. \`docker rm -f ${LIVE_CONTAINER}\` and re-run.`;
6634
+ }
6635
+ const run3 = await capture(
6636
+ "docker",
6637
+ ["run", "-d", "--rm", "-p", `${LIVE_PORT}:4566`, "--name", LIVE_CONTAINER, LIVE_IMAGE],
6638
+ process.cwd()
6639
+ );
6640
+ if (run3.code !== 0) return `docker run ${LIVE_IMAGE} failed (${run3.code})`;
6641
+ for (let i = 0; i < 30; i++) {
6202
6642
  try {
6203
- entries = readdirSync5(d).sort();
6643
+ const health = await fetch(`http://localhost:${LIVE_PORT}/_localstack/health`);
6644
+ if (health.ok && (await health.text()).includes("cloudformation")) return void 0;
6204
6645
  } catch {
6205
- return;
6206
- }
6207
- for (const name of entries) {
6208
- if (out.length >= MAX_ARTIFACTS) return;
6209
- if (name === "node_modules" || name.startsWith(".")) continue;
6210
- const full = join13(d, name);
6211
- let st;
6212
- try {
6213
- st = statSync5(full);
6214
- } catch {
6215
- continue;
6216
- }
6217
- if (st.isDirectory()) {
6218
- walk(full, depth + 1);
6219
- continue;
6220
- }
6221
- const rel = relative2(demo.root, full).split(sep2).join("/");
6222
- if (!filter(rel)) continue;
6223
- let text = "";
6224
- try {
6225
- text = readFileSync10(full, "utf8");
6226
- } catch {
6227
- continue;
6228
- }
6229
- const truncated = text.length > MAX_ARTIFACT_BYTES;
6230
- out.push({
6231
- path: rel,
6232
- kind: (name.split(".").pop() ?? "").toLowerCase(),
6233
- bytes: st.size,
6234
- text: truncated ? text.slice(0, MAX_ARTIFACT_BYTES) : text,
6235
- truncated
6236
- });
6237
6646
  }
6238
- };
6239
- walk(dir, 0);
6240
- return out;
6647
+ await new Promise((r) => setTimeout(r, 1e3));
6648
+ }
6649
+ await teardownScratchFloci();
6650
+ return `Floci never reported healthy on :${LIVE_PORT} after 30s`;
6241
6651
  }
6242
- function carveSlug(select) {
6243
- return select.replace(/[^A-Za-z0-9_]+/g, "-");
6652
+ async function teardownScratchFloci() {
6653
+ await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
6244
6654
  }
6245
- var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
6246
- function shortenIn(arg, root) {
6247
- return arg.startsWith(root + sep2) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
6655
+ async function applyIntoFloci(fromDir, step) {
6656
+ const template = join12(fromDir, "floci-override.tf.disabled");
6657
+ if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
6658
+ writeFileSync3(join12(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
6659
+ const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
6660
+ if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
6661
+ const apply = await step(
6662
+ "terraform",
6663
+ ["apply", "-input=false", "-auto-approve", "-no-color", ...LIVE_TARGETS.map((t) => `-target=${t}`)],
6664
+ fromDir
6665
+ );
6666
+ if (apply !== 0) return `terraform apply exited ${apply}`;
6667
+ return void 0;
6248
6668
  }
6249
- function merge(r, root) {
6250
- const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
6251
- return clip(joined.split(root + sep2).join("").split(root).join("."));
6669
+ async function runLivePlan(fromDir) {
6670
+ const r = await capture("terraform", ["plan", "-input=false", "-no-color", "-detailed-exitcode", ...LIVE_TARGETS.map((t) => `-target=${t}`)], fromDir);
6671
+ if (r.code !== 0 && r.code !== 2) {
6672
+ const tail = r.stdout.trim().split("\n").slice(-8).join("\n");
6673
+ return { error: `terraform plan failed (exit ${r.code}):
6674
+ ${tail}`, exitCode: r.code };
6675
+ }
6676
+ return { ...parsePlanOutput(r.stdout, r.code), exitCode: r.code };
6252
6677
  }
6253
- async function runCarveEmit(demo, select) {
6254
- const block = carveWriteBlock(demo);
6255
- if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6256
- mkdirSync3(demo.out, { recursive: true });
6257
- const reportFile = join13(demo.out, `${carveSlug(select)}-boundary.json`);
6258
- const args = [
6259
- "carve",
6260
- "emit",
6261
- "--from",
6262
- demo.from,
6263
- ...demo.state ? ["--state", demo.state] : [],
6264
- "--select",
6265
- select,
6266
- "--output",
6267
- demo.out,
6268
- "--report",
6269
- reportFile
6270
- ];
6271
- const run3 = await runChantRaw(args, demo.project).catch((err) => ({
6272
- code: 127,
6273
- stdout: "",
6274
- stderr: err instanceof Error ? err.message : String(err)
6275
- }));
6276
- const output = merge(run3, demo.root);
6277
- if (run3.code !== 0) {
6678
+
6679
+ // src/carve-actions.ts
6680
+ async function runCarvePlan(demo) {
6681
+ if (!demo?.live) {
6278
6682
  return refuse3(
6279
6683
  "carve-action",
6280
- `chant carve emit exited ${run3.code}: ${output || "(no output)"}`,
6281
- "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`."
6684
+ "terraform plan is the live tier's beat, and this server isn't serving one",
6685
+ "run `behold demo carve --live` (needs docker + terraform)"
6282
6686
  );
6283
6687
  }
6284
- let boundary = null;
6285
- try {
6286
- boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
6287
- } catch {
6688
+ const r = await runLivePlan(demo.from);
6689
+ if ("error" in r) {
6690
+ return refuse3("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
6288
6691
  }
6289
- const lintPath = relative2(demo.project, join13(demo.out, "src")).split(sep2).join("/");
6290
- const lintArgs = ["lint", lintPath];
6291
- const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
6292
- code: 127,
6293
- stdout: "",
6294
- stderr: err instanceof Error ? err.message : String(err)
6295
- }));
6296
6692
  return {
6297
6693
  ok: true,
6298
- select,
6299
- command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
6300
- output,
6301
- // Emitted source first, scaffolding after: `src/assets.ts` is the thing the
6302
- // step is about, and a package.json sorting above it buries the answer.
6303
- artifacts: readArtifacts(demo, demo.out, (rel) => /\.(ts|json)$/.test(rel) && !rel.endsWith("tsconfig.json")).sort(
6304
- (a, b) => Number(!/\/src\//.test(a.path)) - Number(!/\/src\//.test(b.path))
6305
- ),
6306
- boundary,
6307
- lint: { ok: lint.code === 0, code: lint.code, command: `chant ${lintArgs.join(" ")}`, output: merge(lint, demo.root) },
6308
- buildCaveat: BUILD_CAVEAT
6694
+ command: "terraform plan -detailed-exitcode (targeted, in the demo copy)",
6695
+ planLine: r.planLine,
6696
+ changes: r.changes,
6697
+ noDestroy: r.noDestroy
6309
6698
  };
6310
6699
  }
6311
- async function runCarveBridge(demo, select) {
6312
- const block = carveWriteBlock(demo);
6313
- if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6314
- mkdirSync3(demo.out, { recursive: true });
6315
- const args = [
6316
- "carve",
6317
- "bridge",
6318
- "--from",
6319
- demo.from,
6320
- ...demo.state ? ["--state", demo.state] : [],
6321
- "--select",
6322
- select,
6323
- "--output",
6324
- demo.out
6325
- ];
6326
- const run3 = await runChantRaw(args, demo.project).catch((err) => ({
6327
- code: 127,
6328
- stdout: "",
6329
- stderr: err instanceof Error ? err.message : String(err)
6330
- }));
6331
- const output = merge(run3, demo.root);
6332
- if (run3.code !== 0) {
6700
+ async function runCarveObserve(demo, select) {
6701
+ if (!demo.live) {
6333
6702
  return refuse3(
6334
6703
  "carve-action",
6335
- `chant carve bridge exited ${run3.code}: ${output || "(no output)"}`,
6336
- "Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
6704
+ "the observe beat reads the scratch Floci, and this server isn't serving one",
6705
+ "run `behold demo carve --live` (needs docker + terraform)"
6337
6706
  );
6338
6707
  }
6339
- const slug3 = carveSlug(select);
6340
- const all = readArtifacts(demo, demo.out, (rel) => /\.(tf|md|patch)$/.test(rel));
6341
- return {
6342
- ok: true,
6343
- select,
6344
- command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
6345
- output,
6346
- runbook: all.find((a) => a.path.endsWith(`${slug3}-runbook.md`)) ?? null,
6347
- proposals: all.filter((a) => !a.path.endsWith(`${slug3}-runbook.md`))
6348
- };
6349
- }
6350
-
6351
- // src/ops.ts
6352
- import { readdirSync as readdirSync6, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
6353
- import { join as join14 } from "node:path";
6354
- var APPLY_TARGET_LEXICON = {
6355
- cloudformation: "aws",
6356
- kubectl: "k8s",
6357
- // kustomize renders then applies through the same k8s pipeline
6358
- // (chant#1548), so it scopes to the same lexicon. Note the auto-sync
6359
- // wrinkle this creates on purpose: a kustomize Op AND a kubectl Op in one
6360
- // project both exact-match `k8s`, and two exact matches decline — two Ops
6361
- // claiming the k8s half genuinely is ambiguous.
6362
- kustomize: "k8s",
6363
- arm: "azure"
6364
- };
6365
- function kindOf(content) {
6366
- if (/\bApplyOp\b/.test(content)) return "apply";
6367
- if (/\bReconcileOp\b/.test(content)) return "reconcile";
6368
- if (/AuditOp\b/.test(content)) return "audit";
6369
- return "op";
6370
- }
6371
- function discoverOps(projectDir) {
6372
- const seen = /* @__PURE__ */ new Set();
6373
- const out = [];
6374
- for (const sub of ["ops", "src", "."]) {
6375
- const dir = join14(projectDir, sub);
6376
- if (!existsSync9(dir)) continue;
6377
- for (const f of readdirSync6(dir)) {
6378
- if (!f.endsWith(".op.ts")) continue;
6379
- const content = readFileSync11(join14(dir, f), "utf8");
6380
- const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
6381
- if (!name || seen.has(name)) continue;
6382
- seen.add(name);
6383
- const gate = content.match(/signalName:\s*["'`]([^"'`]+)["'`]/)?.[1];
6384
- const env = content.match(/\benv:\s*["'`]([^"'`]+)["'`]/)?.[1];
6385
- const target = content.match(/\btarget:\s*["'`]([^"'`]+)["'`]/)?.[1];
6386
- const substrate = target ? APPLY_TARGET_LEXICON[target] : void 0;
6387
- const irPath = opIrPath(projectDir, name);
6388
- out.push({
6389
- name,
6390
- kind: kindOf(content),
6391
- dir: projectDir,
6392
- ...gate ? { gate } : {},
6393
- ...env ? { env } : {},
6394
- ...substrate ? { substrate } : {},
6395
- ...existsSync9(irPath) ? { irPath } : {}
6396
- });
6397
- }
6398
- }
6399
- return out.sort((a, b) => a.name.localeCompare(b.name));
6400
- }
6401
- function opIrPath(projectDir, name) {
6402
- return join14(projectDir, "dist", "ops", name, "op.json");
6403
- }
6404
- function discoverOpIrs(projectDirs) {
6405
- const seen = /* @__PURE__ */ new Set();
6406
- const out = [];
6407
- for (const dir of projectDirs) {
6408
- const root = join14(dir, "dist", "ops");
6409
- if (!existsSync9(root)) continue;
6410
- for (const name of readdirSync6(root)) {
6411
- if (seen.has(name)) continue;
6412
- const path = opIrPath(dir, name);
6413
- if (!existsSync9(path)) continue;
6414
- seen.add(name);
6415
- out.push({ name, dir, path });
6416
- }
6417
- }
6418
- return out.sort((a, b) => a.name.localeCompare(b.name));
6419
- }
6420
- function discoverEstateOps(projectDirs) {
6421
- const seen = /* @__PURE__ */ new Set();
6422
- const out = [];
6423
- for (const dir of projectDirs) {
6424
- for (const op of discoverOps(dir)) {
6425
- if (seen.has(op.name)) continue;
6426
- seen.add(op.name);
6427
- out.push(op);
6428
- }
6708
+ if (!existsSync8(join13(demo.out, "src"))) {
6709
+ return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
6710
+ }
6711
+ const args = ["lifecycle", "diff", "prod", "--live", "--json"];
6712
+ const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
6713
+ code: 127,
6714
+ stdout: "",
6715
+ stderr: err instanceof Error ? err.message : String(err)
6716
+ }));
6717
+ if (run3.code !== 0) {
6718
+ return refuse3(
6719
+ "carve-action",
6720
+ `chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
6721
+ "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
6722
+ );
6429
6723
  }
6430
- return out.sort((a, b) => a.name.localeCompare(b.name));
6431
- }
6432
-
6433
- // src/run-playhead.ts
6434
- var RECEIPT_READ_FN = "receiptRead";
6435
- var RECEIPT_WRITE_FN = "receiptWrite";
6436
- var STEP_STATUSES = /* @__PURE__ */ new Set(["ok", "fail", "skipped"]);
6437
- var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6438
- function asStepRecord(value) {
6439
- if (!isRecord4(value)) return null;
6440
- if (typeof value.phase !== "string" || typeof value.fn !== "string") return null;
6441
- if (typeof value.status !== "string" || !STEP_STATUSES.has(value.status)) return null;
6442
- if (typeof value.durationMs !== "number") return null;
6443
- return value;
6444
- }
6445
- function parseRunProgressLine(line) {
6446
6724
  let parsed;
6447
6725
  try {
6448
- parsed = JSON.parse(line);
6726
+ parsed = JSON.parse(run3.stdout);
6449
6727
  } catch {
6450
- return null;
6728
+ return refuse3("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
6451
6729
  }
6452
- const one = asStepRecord(parsed);
6453
- if (one) return [one];
6454
- if (isRecord4(parsed) && typeof parsed.op === "string" && Array.isArray(parsed.records)) {
6455
- const records = parsed.records.map(asStepRecord).filter((r) => r !== null);
6456
- return records.length ? records : null;
6730
+ if (parsed.stacks && !parsed.lexicons) {
6731
+ return refuse3(
6732
+ "carve-action",
6733
+ "chant lifecycle diff answered a stacked shape ({stacks}) \u2014 observe only reads the stack-less {lexicons} shape",
6734
+ "point the target at a stack-less carveout, or file a shape-read gap against observe for the stacked case"
6735
+ );
6457
6736
  }
6458
- return null;
6459
- }
6460
- function recordLine(record) {
6461
- const mark = record.status === "ok" ? "\u2713" : record.status === "fail" ? "\u2717" : "\u2022";
6462
- const took = record.status === "skipped" ? "skipped" : record.durationMs < 1e3 ? `${record.durationMs}ms` : `${(record.durationMs / 1e3).toFixed(1)}s`;
6463
- return `${mark} ${record.phase} \xB7 ${record.fn} \u2014 ${took}${record.error ? ` \u2014 ${record.error}` : ""}`;
6464
- }
6465
- var ANSI = /\x1b\[[0-9;]*m/g;
6466
- var strip = (s) => s.replace(ANSI, "");
6467
- function statusFields(stdout) {
6468
- const out = /* @__PURE__ */ new Map();
6469
- for (const raw of strip(stdout).split("\n")) {
6470
- const m = /^\s{2,}([A-Za-z][A-Za-z ]*?)\s*:\s*(.*)$/.exec(raw);
6471
- if (m) out.set(m[1].trim(), m[2].trim());
6737
+ const aws = parsed.lexicons?.aws ?? {};
6738
+ const entity = select.split(".").pop() ?? select;
6739
+ const command = `AWS_ENDPOINT_URL=${demo.live.endpoint} chant ${args.join(" ")} # in ${shortenIn(demo.out, demo.root)}`;
6740
+ const queried = aws.resources?.queried?.[entity];
6741
+ const meta = aws.observed?.[entity];
6742
+ if (meta) {
6743
+ return {
6744
+ ok: true,
6745
+ select,
6746
+ command,
6747
+ entity,
6748
+ verdict: "observed",
6749
+ ...meta.status ? { status: meta.status } : {},
6750
+ ...meta.ownership ? { ownership: meta.ownership } : {},
6751
+ ...meta.physicalId ? { physicalId: meta.physicalId } : {},
6752
+ ...queried ? { queried } : {}
6753
+ };
6472
6754
  }
6473
- return out;
6474
- }
6475
- function parseGateLine(line) {
6476
- const m = /^(.*?)(?: — (.*?))? \(waiting since (.+)\)$/.exec(line.trim());
6477
- if (!m) return null;
6478
- const signalName = m[1].trim();
6479
- if (!signalName) return null;
6480
- return { signalName, ...m[2] ? { description: m[2].trim() } : {}, since: m[3].trim() };
6481
- }
6482
- function readRunStatus(op, run3) {
6483
- if (run3.code !== 0) {
6484
- const message = strip(run3.stderr).trim() || strip(run3.stdout).trim() || `chant run status exited ${run3.code}`;
6485
- if (/not available in local mode|@temporalio\/client is not installed/.test(message)) {
6486
- return {
6487
- ok: false,
6488
- refusal: {
6489
- error: `No Temporal run state for "${op}" \u2014 ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
6490
- code: "no-temporal",
6491
- remedy: "Gate state and per-step run records come from a durable run. Configure a Temporal profile in chant.config.ts (and `npm install @temporalio/client`), then run the Op with --temporal."
6492
- }
6493
- };
6494
- }
6495
- if (/not found|NotFound/.test(message)) {
6496
- return {
6497
- ok: false,
6498
- refusal: {
6499
- error: `No run of "${op}" to report \u2014 ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
6500
- code: "no-run",
6501
- remedy: "Start the Op (Run) and the playhead fills in as each step settles."
6502
- }
6503
- };
6504
- }
6755
+ const hole = aws.resources?.unobserved?.find((u) => u.name === entity);
6756
+ if (hole) {
6505
6757
  return {
6506
- ok: false,
6507
- refusal: {
6508
- error: `chant run status ${op} failed: ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
6509
- code: "run-status",
6510
- remedy: "Run `chant run status " + op + " --temporal` in the project to see the whole error."
6511
- }
6758
+ ok: true,
6759
+ select,
6760
+ command,
6761
+ entity,
6762
+ verdict: "unobserved",
6763
+ ...hole.detail || hole.reason ? { detail: [hole.reason, hole.detail].filter(Boolean).join(": ") } : {},
6764
+ ...queried ? { queried } : {}
6512
6765
  };
6513
6766
  }
6514
- const f = statusFields(run3.stdout);
6515
- const activities = /^(\d+)\/(\d+)/.exec(f.get("Activities") ?? "");
6516
- const gateLine = f.get("Gate");
6517
- return {
6518
- ok: true,
6519
- read: {
6520
- op,
6521
- ...f.get("Workflow ID") ? { workflowId: f.get("Workflow ID") } : {},
6522
- ...f.get("Run ID") ? { runId: f.get("Run ID") } : {},
6523
- ...f.get("Status") ? { status: f.get("Status") } : {},
6524
- ...f.get("Started") ? { startedAt: f.get("Started") } : {},
6525
- ...f.get("Closed") ? { closedAt: f.get("Closed") } : {},
6526
- ...activities ? { activities: { completed: Number(activities[1]), scheduled: Number(activities[2]) } } : {},
6527
- // No Gate line means chant printed none, which it does both when the
6528
- // query says "nothing pending" and when the Op registers no handler.
6529
- // Either way: no gate is waiting on a human right now.
6530
- gate: gateLine ? parseGateLine(gateLine) : null
6531
- }
6532
- };
6533
- }
6534
- var initialRunState = {
6535
- op: null,
6536
- mode: null,
6537
- status: "idle",
6538
- records: [],
6539
- gate: null,
6540
- gateNote: null,
6541
- startedAt: null,
6542
- endedAt: null,
6543
- lost: null
6544
- };
6545
- function runStarted(op, mode, at) {
6546
- return { ...initialRunState, op, mode, status: "running", startedAt: at };
6767
+ return { ok: true, select, command, entity, verdict: "missing", ...queried ? { queried } : {} };
6547
6768
  }
6548
- function runRecord(state, record) {
6549
- return { ...state, records: [...state.records, record] };
6769
+ var MAX_ARTIFACT_BYTES = 64 * 1024;
6770
+ var MAX_ARTIFACTS = 24;
6771
+ var MAX_OUTPUT_BYTES = 32 * 1024;
6772
+ var refuse3 = (code, error, remedy) => ({
6773
+ ok: false,
6774
+ refusal: { error, code, remedy }
6775
+ });
6776
+ var BUILD_CAVEAT = "The gate shown is `chant lint`, not `chant build`. chant#1637's fold is applied now: the bucket's versioning and public-access-block sub-resources come out as native props, not just folded-in mentions. `chant build` still fails, but on one rule only \u2014 WAW042, a TLS-deny bucket policy the source Terraform never declared. That's real drift the estate inherited, not a gap in the carve. example-carve/README.md, \"Why build still fails\", has the full statement.";
6777
+ function insideDemo(root, p) {
6778
+ const rel = relative2(resolve4(root), resolve4(p));
6779
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
6550
6780
  }
6551
- function runGate(state, result) {
6552
- return result.ok ? { ...state, gate: result.read.gate, gateNote: null } : { ...state, gate: null, gateNote: result.refusal.error };
6781
+ function selectFromReport(report, raw) {
6782
+ if (typeof raw !== "string" || !raw || raw.length > 512) return null;
6783
+ return (report?.resources ?? []).some((r) => r.address === raw) ? raw : null;
6553
6784
  }
6554
- function runEnded(state, exitCode, at) {
6555
- if (state.status !== "running") return state;
6556
- if (exitCode === 0) return { ...state, status: "ok", endedAt: at };
6557
- if (state.records.some((r) => r.status === "fail")) return { ...state, status: "failed", endedAt: at };
6558
- return {
6559
- ...state,
6560
- status: "lost",
6561
- endedAt: at,
6562
- lost: `the progress stream ended (exit ${exitCode}) after ${state.records.length} settled step${state.records.length === 1 ? "" : "s"} without a failed step \u2014 behold does not know whether the Op failed or the stream broke, so the playhead stays at the last settled step.`
6563
- };
6785
+ function carveWriteBlock(demo) {
6786
+ if (!demo) {
6787
+ return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
6788
+ }
6789
+ if (!existsSync8(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
6790
+ if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
6791
+ return unwritableReason(demo.root);
6564
6792
  }
6565
- function slotsFor(placed) {
6793
+ function readArtifacts(demo, dir, filter) {
6566
6794
  const out = [];
6567
- for (const p of placed) {
6568
- if (p.step.kind === "activity") {
6569
- out.push({ id: p.id, phase: p.phase, fn: p.step.fn });
6570
- } else if (p.step.kind === "effect") {
6571
- out.push({ id: p.id, phase: p.phase, fn: RECEIPT_READ_FN });
6572
- for (const nested of p.step.steps ?? []) {
6573
- if (nested.kind === "activity") out.push({ id: p.id, phase: p.phase, fn: nested.fn });
6574
- }
6575
- out.push({ id: p.id, phase: p.phase, fn: RECEIPT_WRITE_FN });
6795
+ const walk = (d, depth) => {
6796
+ if (out.length >= MAX_ARTIFACTS || depth > 2) return;
6797
+ let entries;
6798
+ try {
6799
+ entries = readdirSync5(d).sort();
6800
+ } catch {
6801
+ return;
6802
+ }
6803
+ for (const name of entries) {
6804
+ if (out.length >= MAX_ARTIFACTS) return;
6805
+ if (name === "node_modules" || name.startsWith(".")) continue;
6806
+ const full = join13(d, name);
6807
+ let st;
6808
+ try {
6809
+ st = statSync5(full);
6810
+ } catch {
6811
+ continue;
6812
+ }
6813
+ if (st.isDirectory()) {
6814
+ walk(full, depth + 1);
6815
+ continue;
6816
+ }
6817
+ const rel = relative2(demo.root, full).split(sep2).join("/");
6818
+ if (!filter(rel)) continue;
6819
+ let text = "";
6820
+ try {
6821
+ text = readFileSync10(full, "utf8");
6822
+ } catch {
6823
+ continue;
6824
+ }
6825
+ const truncated = text.length > MAX_ARTIFACT_BYTES;
6826
+ out.push({
6827
+ path: rel,
6828
+ kind: (name.split(".").pop() ?? "").toLowerCase(),
6829
+ bytes: st.size,
6830
+ text: truncated ? text.slice(0, MAX_ARTIFACT_BYTES) : text,
6831
+ truncated
6832
+ });
6576
6833
  }
6577
- }
6578
- return out;
6579
- }
6580
- function mergeRun(prev, record) {
6581
- const rank = { fail: 3, ok: 2, skipped: 1 };
6582
- const keepPrev = prev && rank[prev.status] >= rank[record.status];
6583
- return {
6584
- status: keepPrev ? prev.status : record.status,
6585
- durationMs: (prev?.durationMs ?? 0) + record.durationMs,
6586
- ...keepPrev && prev.error ? { error: prev.error } : record.error ? { error: record.error } : {},
6587
- ...keepPrev && prev.outcome ? { outcome: prev.outcome } : record.outcome ? { outcome: `${record.outcome.name}=${String(record.outcome.value)}` } : {},
6588
- records: (prev?.records ?? 0) + 1
6589
6834
  };
6835
+ walk(dir, 0);
6836
+ return out;
6590
6837
  }
6591
- function joinRun(op, records, gate) {
6592
- const placed = opPlacedSteps(op);
6593
- const slots = slotsFor(placed);
6594
- const byKey = /* @__PURE__ */ new Map();
6595
- for (const s of slots) {
6596
- const key = `${s.phase}\0${s.fn}`;
6597
- if (!byKey.has(key)) byKey.set(key, []);
6598
- byKey.get(key).push({ id: s.id });
6838
+ function carveSlug(select) {
6839
+ return select.replace(/[^A-Za-z0-9_]+/g, "-");
6840
+ }
6841
+ var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
6842
+ function shortenIn(arg, root) {
6843
+ return arg.startsWith(root + sep2) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
6844
+ }
6845
+ function merge(r, root) {
6846
+ const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
6847
+ return clip(joined.split(root + sep2).join("").split(root).join("."));
6848
+ }
6849
+ async function runCarveEmit(demo, select) {
6850
+ const block = carveWriteBlock(demo);
6851
+ if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6852
+ mkdirSync3(demo.out, { recursive: true });
6853
+ const reportFile = join13(demo.out, `${carveSlug(select)}-boundary.json`);
6854
+ const args = [
6855
+ "carve",
6856
+ "emit",
6857
+ "--from",
6858
+ demo.from,
6859
+ ...demo.state ? ["--state", demo.state] : [],
6860
+ "--select",
6861
+ select,
6862
+ "--output",
6863
+ demo.out,
6864
+ "--report",
6865
+ reportFile
6866
+ ];
6867
+ const run3 = await runChantRaw(args, demo.project).catch((err) => ({
6868
+ code: 127,
6869
+ stdout: "",
6870
+ stderr: err instanceof Error ? err.message : String(err)
6871
+ }));
6872
+ const output = merge(run3, demo.root);
6873
+ if (run3.code !== 0) {
6874
+ return refuse3(
6875
+ "carve-action",
6876
+ `chant carve emit exited ${run3.code}: ${output || "(no output)"}`,
6877
+ "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`."
6878
+ );
6599
6879
  }
6600
- const cursor = /* @__PURE__ */ new Map();
6601
- const byNode = /* @__PURE__ */ new Map();
6602
- const unplaced = [];
6603
- let lastSettledIndex = -1;
6604
- const indexOf = new Map(placed.map((p, i) => [p.id, i]));
6605
- for (const record of records) {
6606
- const key = `${record.phase}\0${record.fn}`;
6607
- const list3 = byKey.get(key);
6608
- const i = cursor.get(key) ?? 0;
6609
- const slot = list3?.[i];
6610
- if (!slot) {
6611
- unplaced.push(record);
6612
- continue;
6613
- }
6614
- cursor.set(key, i + 1);
6615
- byNode.set(slot.id, mergeRun(byNode.get(slot.id), record));
6616
- if (record.status !== "skipped") {
6617
- lastSettledIndex = Math.max(lastSettledIndex, indexOf.get(slot.id) ?? -1);
6618
- }
6880
+ let boundary = null;
6881
+ try {
6882
+ boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
6883
+ } catch {
6619
6884
  }
6620
- if (gate) {
6621
- const gateNode = placed.find((p) => p.step.kind === "gate" && p.step.signalName === gate.signalName);
6622
- if (gateNode) {
6623
- return {
6624
- byNode,
6625
- at: gateNode.id,
6626
- atReason: `waiting for ${gate.signalName} since ${gate.since}`,
6627
- unplaced
6628
- };
6629
- }
6885
+ const lintPath = relative2(demo.project, join13(demo.out, "src")).split(sep2).join("/");
6886
+ const lintArgs = ["lint", lintPath];
6887
+ const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
6888
+ code: 127,
6889
+ stdout: "",
6890
+ stderr: err instanceof Error ? err.message : String(err)
6891
+ }));
6892
+ return {
6893
+ ok: true,
6894
+ select,
6895
+ command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
6896
+ output,
6897
+ // Emitted source first, scaffolding after: `src/assets.ts` is the thing the
6898
+ // step is about, and a package.json sorting above it buries the answer.
6899
+ artifacts: readArtifacts(demo, demo.out, (rel) => /\.(ts|json)$/.test(rel) && !rel.endsWith("tsconfig.json")).sort(
6900
+ (a, b) => Number(!/\/src\//.test(a.path)) - Number(!/\/src\//.test(b.path))
6901
+ ),
6902
+ boundary,
6903
+ lint: { ok: lint.code === 0, code: lint.code, command: `chant ${lintArgs.join(" ")}`, output: merge(lint, demo.root) },
6904
+ buildCaveat: BUILD_CAVEAT
6905
+ };
6906
+ }
6907
+ async function runCarveBridge(demo, select) {
6908
+ const block = carveWriteBlock(demo);
6909
+ if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
6910
+ mkdirSync3(demo.out, { recursive: true });
6911
+ const args = [
6912
+ "carve",
6913
+ "bridge",
6914
+ "--from",
6915
+ demo.from,
6916
+ ...demo.state ? ["--state", demo.state] : [],
6917
+ "--select",
6918
+ select,
6919
+ "--output",
6920
+ demo.out
6921
+ ];
6922
+ const run3 = await runChantRaw(args, demo.project).catch((err) => ({
6923
+ code: 127,
6924
+ stdout: "",
6925
+ stderr: err instanceof Error ? err.message : String(err)
6926
+ }));
6927
+ const output = merge(run3, demo.root);
6928
+ if (run3.code !== 0) {
6929
+ return refuse3(
6930
+ "carve-action",
6931
+ `chant carve bridge exited ${run3.code}: ${output || "(no output)"}`,
6932
+ "Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
6933
+ );
6630
6934
  }
6631
- const next = placed.find((p, i) => i > lastSettledIndex && p.when === "phases");
6935
+ const slug3 = carveSlug(select);
6936
+ const all = readArtifacts(demo, demo.out, (rel) => /\.(tf|md|patch)$/.test(rel));
6632
6937
  return {
6633
- byNode,
6634
- at: lastSettledIndex >= 0 && next ? next.id : null,
6635
- atReason: lastSettledIndex >= 0 && next ? "reached \u2014 not settled yet" : null,
6636
- unplaced
6938
+ ok: true,
6939
+ select,
6940
+ command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
6941
+ output,
6942
+ runbook: all.find((a) => a.path.endsWith(`${slug3}-runbook.md`)) ?? null,
6943
+ proposals: all.filter((a) => !a.path.endsWith(`${slug3}-runbook.md`))
6637
6944
  };
6638
6945
  }
6639
- var TONE = {
6640
- ok: "good",
6641
- fail: "warn",
6642
- skipped: "neutral"
6946
+
6947
+ // src/ops.ts
6948
+ import { readdirSync as readdirSync6, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
6949
+ import { join as join14 } from "node:path";
6950
+ var APPLY_TARGET_LEXICON = {
6951
+ cloudformation: "aws",
6952
+ kubectl: "k8s",
6953
+ // kustomize renders then applies through the same k8s pipeline
6954
+ // (chant#1548), so it scopes to the same lexicon. Note the auto-sync
6955
+ // wrinkle this creates on purpose: a kustomize Op AND a kubectl Op in one
6956
+ // project both exact-match `k8s`, and two exact matches decline — two Ops
6957
+ // claiming the k8s half genuinely is ambiguous.
6958
+ kustomize: "k8s",
6959
+ arm: "azure"
6643
6960
  };
6644
- var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
6645
- function tookLabel(ms) {
6646
- return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
6961
+ function kindOf(content) {
6962
+ if (/\bApplyOp\b/.test(content)) return "apply";
6963
+ if (/\bReconcileOp\b/.test(content)) return "reconcile";
6964
+ if (/AuditOp\b/.test(content)) return "audit";
6965
+ return "op";
6647
6966
  }
6648
- function paintPlayhead(ir, join21, gate) {
6649
- if (!join21.byNode.size && !join21.at) return ir;
6650
- const nodes = ir.nodes.map((node) => {
6651
- const run3 = join21.byNode.get(node.id);
6652
- const here = join21.at === node.id;
6653
- if (!run3 && !here) return node;
6654
- const isGate = node.attrs?._step === "gate";
6655
- const attrs = { ...node.attrs };
6656
- if (run3) {
6657
- attrs._status = TONE[run3.status];
6658
- attrs.run = run3.status === "skipped" ? "skipped" : `${RAN[run3.status]} \xB7 ${tookLabel(run3.durationMs)}`;
6659
- attrs.ran = tookLabel(run3.durationMs);
6660
- if (run3.error) attrs.error = run3.error;
6661
- if (run3.outcome) attrs.outcome = run3.outcome;
6662
- if (run3.records > 1) attrs.runRecords = run3.records;
6663
- }
6664
- if (here) {
6665
- attrs._playhead = true;
6666
- attrs._status = isGate && gate ? "warn" : "accent";
6667
- attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join21.atReason ?? "here";
6668
- if (isGate && gate?.description) attrs.gateDescription = gate.description;
6967
+ function discoverOps(projectDir) {
6968
+ const seen = /* @__PURE__ */ new Set();
6969
+ const out = [];
6970
+ for (const sub of ["ops", "src", "."]) {
6971
+ const dir = join14(projectDir, sub);
6972
+ if (!existsSync9(dir)) continue;
6973
+ for (const f of readdirSync6(dir)) {
6974
+ if (!f.endsWith(".op.ts")) continue;
6975
+ const content = readFileSync11(join14(dir, f), "utf8");
6976
+ const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
6977
+ if (!name || seen.has(name)) continue;
6978
+ seen.add(name);
6979
+ const gate = content.match(/signalName:\s*["'`]([^"'`]+)["'`]/)?.[1];
6980
+ const env = content.match(/\benv:\s*["'`]([^"'`]+)["'`]/)?.[1];
6981
+ const target = content.match(/\btarget:\s*["'`]([^"'`]+)["'`]/)?.[1];
6982
+ const substrate = target ? APPLY_TARGET_LEXICON[target] : void 0;
6983
+ const irPath = opIrPath(projectDir, name);
6984
+ out.push({
6985
+ name,
6986
+ kind: kindOf(content),
6987
+ dir: projectDir,
6988
+ ...gate ? { gate } : {},
6989
+ ...env ? { env } : {},
6990
+ ...substrate ? { substrate } : {},
6991
+ ...existsSync9(irPath) ? { irPath } : {}
6992
+ });
6669
6993
  }
6670
- return { ...node, attrs };
6671
- });
6672
- return { ...ir, nodes };
6994
+ }
6995
+ return out.sort((a, b) => a.name.localeCompare(b.name));
6673
6996
  }
6674
- function pendingGateCard(op, gate, ir) {
6675
- const node = ir.nodes.find((n) => n.attrs?._step === "gate" && n.attrs?.gate === gate.signalName && n.attrs?.op === op);
6676
- return {
6677
- op,
6678
- signalName: gate.signalName,
6679
- ...gate.description ? { description: gate.description } : {},
6680
- since: gate.since,
6681
- ...node ? { node: node.id } : {},
6682
- ...node && typeof node.attrs?.guards === "string" ? { guards: node.attrs.guards } : {},
6683
- approve: {
6684
- method: "POST",
6685
- path: `/api/ops/${encodeURIComponent(op)}/signal/${encodeURIComponent(gate.signalName)}`
6997
+ function opIrPath(projectDir, name) {
6998
+ return join14(projectDir, "dist", "ops", name, "op.json");
6999
+ }
7000
+ function discoverOpIrs(projectDirs) {
7001
+ const seen = /* @__PURE__ */ new Set();
7002
+ const out = [];
7003
+ for (const dir of projectDirs) {
7004
+ const root = join14(dir, "dist", "ops");
7005
+ if (!existsSync9(root)) continue;
7006
+ for (const name of readdirSync6(root)) {
7007
+ if (seen.has(name)) continue;
7008
+ const path = opIrPath(dir, name);
7009
+ if (!existsSync9(path)) continue;
7010
+ seen.add(name);
7011
+ out.push({ name, dir, path });
6686
7012
  }
6687
- };
6688
- }
6689
- function playheadNote(state, join21) {
6690
- if (!state.op) {
6691
- return "No run state: nothing has been run from this behold session, so every step paints as declared.";
6692
7013
  }
6693
- const settled = join21 ? join21.byNode.size : state.records.length;
6694
- const bits = [];
6695
- 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`;
6696
- if (state.status === "running") {
6697
- bits.push(
6698
- state.mode === "local" ? "still running; the local executor reports every step at once when it ends, so the track fills in on completion" : "still running; only settled steps are painted"
6699
- );
7014
+ return out.sort((a, b) => a.name.localeCompare(b.name));
7015
+ }
7016
+ function discoverEstateOps(projectDirs) {
7017
+ const seen = /* @__PURE__ */ new Set();
7018
+ const out = [];
7019
+ for (const dir of projectDirs) {
7020
+ for (const op of discoverOps(dir)) {
7021
+ if (seen.has(op.name)) continue;
7022
+ seen.add(op.name);
7023
+ out.push(op);
7024
+ }
6700
7025
  }
6701
- if (state.status === "ok") bits.push("the run exited cleanly");
6702
- if (state.status === "failed") bits.push("a step failed");
6703
- if (state.status === "lost") bits.push(`stream lost \u2014 ${state.lost ?? "the progress stream ended without a verdict"}`);
6704
- if (state.gate) {
6705
- bits.push(`waiting on gate ${state.gate.signalName} since ${state.gate.since} \u2014 approve is a delegated signal, behold never signals on its own`);
6706
- } else if (state.gateNote) {
6707
- bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
7026
+ return out.sort((a, b) => a.name.localeCompare(b.name));
7027
+ }
7028
+
7029
+ // src/run-playhead.ts
7030
+ var RECEIPT_READ_FN = "receiptRead";
7031
+ var RECEIPT_WRITE_FN = "receiptWrite";
7032
+ var STEP_STATUSES = /* @__PURE__ */ new Set(["ok", "fail", "skipped"]);
7033
+ var isRecord5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7034
+ function asStepRecord(value) {
7035
+ if (!isRecord5(value)) return null;
7036
+ if (typeof value.phase !== "string" || typeof value.fn !== "string") return null;
7037
+ if (typeof value.status !== "string" || !STEP_STATUSES.has(value.status)) return null;
7038
+ if (typeof value.durationMs !== "number") return null;
7039
+ return value;
7040
+ }
7041
+ function parseRunProgressLine(line) {
7042
+ let parsed;
7043
+ try {
7044
+ parsed = JSON.parse(line);
7045
+ } catch {
7046
+ return null;
6708
7047
  }
6709
- if (join21?.unplaced.length) {
6710
- bits.push(
6711
- `${join21.unplaced.length} record${join21.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join21.unplaced.length === 1 ? "is" : "are"} not painted`
6712
- );
7048
+ const one = asStepRecord(parsed);
7049
+ if (one) return [one];
7050
+ if (isRecord5(parsed) && typeof parsed.op === "string" && Array.isArray(parsed.records)) {
7051
+ const records = parsed.records.map(asStepRecord).filter((r) => r !== null);
7052
+ return records.length ? records : null;
6713
7053
  }
6714
- if (!bits.length) return `${head}.`;
6715
- const rest = bits.join("; ");
6716
- return `${head} \u2014 ${rest}.`;
7054
+ return null;
7055
+ }
7056
+ function recordLine(record) {
7057
+ const mark = record.status === "ok" ? "\u2713" : record.status === "fail" ? "\u2717" : "\u2022";
7058
+ const took = record.status === "skipped" ? "skipped" : record.durationMs < 1e3 ? `${record.durationMs}ms` : `${(record.durationMs / 1e3).toFixed(1)}s`;
7059
+ return `${mark} ${record.phase} \xB7 ${record.fn} \u2014 ${took}${record.error ? ` \u2014 ${record.error}` : ""}`;
6717
7060
  }
6718
-
6719
- // src/operator.ts
6720
7061
  var ANSI2 = /\x1b\[[0-9;]*m/g;
6721
7062
  var strip2 = (s) => s.replace(ANSI2, "");
6722
- var isRecord5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6723
- var HOW_TO_DECLARE = "An operating loop is a ConvergeOp: declare one in a `*.op.ts` (`ConvergeOp({ name, env, dial, rules })` from @intentius/chant-lexicon-temporal), build the project, and tick it with `chant operator` (or a cron / CronJob invoking it).";
6724
- function readRow(value) {
6725
- if (!isRecord5(value)) return null;
6726
- if (typeof value.op !== "string" || typeof value.env !== "string") return null;
6727
- if (!Array.isArray(value.pendingGates)) return null;
6728
- const gates = [];
6729
- for (const g of value.pendingGates) {
6730
- if (!isRecord5(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
6731
- gates.push({ rule: g.rule, gate: g.gate, ...typeof g.op === "string" ? { op: g.op } : {} });
7063
+ function statusFields(stdout) {
7064
+ const out = /* @__PURE__ */ new Map();
7065
+ for (const raw of strip2(stdout).split("\n")) {
7066
+ const m = /^\s{2,}([A-Za-z][A-Za-z ]*?)\s*:\s*(.*)$/.exec(raw);
7067
+ if (m) out.set(m[1].trim(), m[2].trim());
6732
7068
  }
6733
- const tick = isRecord5(value.lastTick) ? value.lastTick : void 0;
6734
- const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
6735
- const lease = isRecord5(value.lease) ? value.lease : void 0;
6736
- return {
6737
- op: value.op,
6738
- env: value.env,
6739
- ...usable ? { lastTick: tick } : {},
6740
- pendingGates: gates,
6741
- ...lease && typeof lease.holder === "string" && typeof lease.expiresAt === "string" ? { lease: { holder: lease.holder, expiresAt: lease.expiresAt } } : {}
6742
- };
7069
+ return out;
6743
7070
  }
6744
- function readOperatorStatus(run3) {
6745
- const err = strip2(run3.stderr);
6746
- const out = strip2(run3.stdout).trim();
7071
+ function parseGateLine(line) {
7072
+ const m = /^(.*?)(?: — (.*?))? \(waiting since (.+)\)$/.exec(line.trim());
7073
+ if (!m) return null;
7074
+ const signalName = m[1].trim();
7075
+ if (!signalName) return null;
7076
+ return { signalName, ...m[2] ? { description: m[2].trim() } : {}, since: m[3].trim() };
7077
+ }
7078
+ function readRunStatus(op, run3) {
6747
7079
  if (run3.code !== 0) {
6748
- const message = err.trim() || out || `chant operator status exited ${run3.code}`;
6749
- const first = message.split("\n")[0].replace(/^error:\s*/i, "");
6750
- if (/Unknown command|Unknown operator subcommand/i.test(message)) {
7080
+ const message = strip2(run3.stderr).trim() || strip2(run3.stdout).trim() || `chant run status exited ${run3.code}`;
7081
+ if (/not available in local mode|@temporalio\/client is not installed/.test(message)) {
6751
7082
  return {
6752
7083
  ok: false,
6753
7084
  refusal: {
6754
- error: `This chant has no \`operator status\` \u2014 ${first}`,
6755
- code: "no-operator-cli",
6756
- remedy: "The operating loop's status surface landed in chant 0.52 (chant#1485). Upgrade chant to read it."
7085
+ error: `No Temporal run state for "${op}" \u2014 ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
7086
+ code: "no-temporal",
7087
+ remedy: "Gate state and per-step run records come from a durable run. Configure a Temporal profile in chant.config.ts (and `npm install @temporalio/client`), then run the Op with --temporal."
6757
7088
  }
6758
7089
  };
6759
7090
  }
6760
- return {
6761
- ok: false,
6762
- refusal: {
6763
- error: `chant operator status failed: ${first}`,
6764
- code: "operator-status",
6765
- remedy: "Run `chant operator status --json` in the project to see the whole error."
6766
- }
6767
- };
6768
- }
6769
- if (!out) {
6770
- return {
6771
- ok: false,
6772
- refusal: {
6773
- error: "This project declares no operating loop \u2014 chant found no ConvergeOp.",
6774
- code: "no-operator",
6775
- remedy: HOW_TO_DECLARE
6776
- }
6777
- };
6778
- }
6779
- let parsed;
6780
- try {
6781
- parsed = JSON.parse(out);
6782
- } catch (e) {
6783
- return {
6784
- ok: false,
6785
- refusal: {
6786
- error: `chant operator status --json didn't print JSON: ${e instanceof Error ? e.message : String(e)}`,
6787
- code: "operator-status",
6788
- remedy: "Run `chant operator status --json` in the project and check what it printed."
6789
- }
6790
- };
6791
- }
6792
- if (!Array.isArray(parsed)) {
6793
- return {
6794
- ok: false,
6795
- refusal: {
6796
- error: "chant operator status --json answered with something that isn't an array of ConvergeOp rows.",
6797
- code: "operator-status",
6798
- remedy: "behold reads one row per ConvergeOp: `{op, env, lastTick?, pendingGates[], lease?}`."
6799
- }
6800
- };
6801
- }
6802
- const rows = [];
6803
- for (const value of parsed) {
6804
- const row = readRow(value);
6805
- if (!row) {
7091
+ if (/not found|NotFound/.test(message)) {
6806
7092
  return {
6807
7093
  ok: false,
6808
7094
  refusal: {
6809
- error: "chant operator status --json answered with a row behold couldn't read (needs `op`, `env`, `pendingGates[]`).",
6810
- code: "operator-status",
6811
- remedy: "Check `chant operator status --json` in the project \u2014 and file the shape change if chant's is the newer one."
7095
+ error: `No run of "${op}" to report \u2014 ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
7096
+ code: "no-run",
7097
+ remedy: "Start the Op (Run) and the playhead fills in as each step settles."
6812
7098
  }
6813
7099
  };
6814
7100
  }
6815
- rows.push(row);
7101
+ return {
7102
+ ok: false,
7103
+ refusal: {
7104
+ error: `chant run status ${op} failed: ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
7105
+ code: "run-status",
7106
+ remedy: "Run `chant run status " + op + " --temporal` in the project to see the whole error."
7107
+ }
7108
+ };
6816
7109
  }
6817
- return { ok: true, rows };
7110
+ const f = statusFields(run3.stdout);
7111
+ const activities = /^(\d+)\/(\d+)/.exec(f.get("Activities") ?? "");
7112
+ const gateLine = f.get("Gate");
7113
+ return {
7114
+ ok: true,
7115
+ read: {
7116
+ op,
7117
+ ...f.get("Workflow ID") ? { workflowId: f.get("Workflow ID") } : {},
7118
+ ...f.get("Run ID") ? { runId: f.get("Run ID") } : {},
7119
+ ...f.get("Status") ? { status: f.get("Status") } : {},
7120
+ ...f.get("Started") ? { startedAt: f.get("Started") } : {},
7121
+ ...f.get("Closed") ? { closedAt: f.get("Closed") } : {},
7122
+ ...activities ? { activities: { completed: Number(activities[1]), scheduled: Number(activities[2]) } } : {},
7123
+ // No Gate line means chant printed none, which it does both when the
7124
+ // query says "nothing pending" and when the Op registers no handler.
7125
+ // Either way: no gate is waiting on a human right now.
7126
+ gate: gateLine ? parseGateLine(gateLine) : null
7127
+ }
7128
+ };
6818
7129
  }
6819
- function declaredConvergeOps(ops) {
6820
- return ops.filter((o) => o.searchAttributes?.Converge === "true").map((o) => ({
6821
- name: o.name,
6822
- ...o.searchAttributes?.Env ? { env: o.searchAttributes.Env } : {},
6823
- ...o.searchAttributes?.Dial ? { dial: o.searchAttributes.Dial } : {}
6824
- }));
7130
+ var initialRunState = {
7131
+ op: null,
7132
+ mode: null,
7133
+ status: "idle",
7134
+ records: [],
7135
+ gate: null,
7136
+ gateNote: null,
7137
+ startedAt: null,
7138
+ endedAt: null,
7139
+ lost: null
7140
+ };
7141
+ function runStarted(op, mode, at) {
7142
+ return { ...initialRunState, op, mode, status: "running", startedAt: at };
6825
7143
  }
6826
- var MANAGED_BY = "app.kubernetes.io/managed-by";
6827
- var COMPONENT = "app.kubernetes.io/component";
6828
- var NAME_LABEL = "app.kubernetes.io/name";
6829
- var CHANT = "chant";
6830
- var CONVERGE_TICK = "converge-tick";
6831
- var CRONJOB_KIND = "K8s::Batch::CronJob";
6832
- var NAMESPACE_KIND = "K8s::Core::Namespace";
6833
- var rec3 = (v) => isRecord5(v) ? v : void 0;
6834
- var str3 = (v) => typeof v === "string" && v ? v : void 0;
6835
- var labelsOf = (n) => rec3(rec3(n.attrs?.metadata)?.labels) ?? {};
6836
- function operatorHomes(ir) {
6837
- const byNamespace = /* @__PURE__ */ new Map();
6838
- const conflicted = /* @__PURE__ */ new Set();
6839
- for (const n of ir.nodes) {
6840
- if (n.kind !== CRONJOB_KIND) continue;
6841
- const labels = labelsOf(n);
6842
- if (labels[MANAGED_BY] !== CHANT || labels[COMPONENT] !== CONVERGE_TICK) continue;
6843
- const meta = rec3(n.attrs?.metadata);
6844
- const ns = str3(meta?.namespace);
6845
- const op = str3(meta?.name);
6846
- if (!ns || !op) continue;
6847
- const home = byNamespace.get(ns) ?? { namespace: ns, ticks: [] };
6848
- const stack = str3(labels[NAME_LABEL]);
6849
- if (stack && !conflicted.has(ns)) {
6850
- if (home.stack === void 0) home.stack = stack;
6851
- else if (home.stack !== stack) {
6852
- conflicted.add(ns);
6853
- delete home.stack;
7144
+ function runRecord(state, record) {
7145
+ return { ...state, records: [...state.records, record] };
7146
+ }
7147
+ function runGate(state, result) {
7148
+ return result.ok ? { ...state, gate: result.read.gate, gateNote: null } : { ...state, gate: null, gateNote: result.refusal.error };
7149
+ }
7150
+ function runEnded(state, exitCode, at) {
7151
+ if (state.status !== "running") return state;
7152
+ if (exitCode === 0) return { ...state, status: "ok", endedAt: at };
7153
+ if (state.records.some((r) => r.status === "fail")) return { ...state, status: "failed", endedAt: at };
7154
+ return {
7155
+ ...state,
7156
+ status: "lost",
7157
+ endedAt: at,
7158
+ lost: `the progress stream ended (exit ${exitCode}) after ${state.records.length} settled step${state.records.length === 1 ? "" : "s"} without a failed step \u2014 behold does not know whether the Op failed or the stream broke, so the playhead stays at the last settled step.`
7159
+ };
7160
+ }
7161
+ function slotsFor(placed) {
7162
+ const out = [];
7163
+ for (const p of placed) {
7164
+ if (p.step.kind === "activity") {
7165
+ out.push({ id: p.id, phase: p.phase, fn: p.step.fn });
7166
+ } else if (p.step.kind === "effect") {
7167
+ out.push({ id: p.id, phase: p.phase, fn: RECEIPT_READ_FN });
7168
+ for (const nested of p.step.steps ?? []) {
7169
+ if (nested.kind === "activity") out.push({ id: p.id, phase: p.phase, fn: nested.fn });
6854
7170
  }
7171
+ out.push({ id: p.id, phase: p.phase, fn: RECEIPT_WRITE_FN });
6855
7172
  }
6856
- const schedule = str3(rec3(n.attrs?.spec)?.schedule);
6857
- home.ticks.push({ node: n.id, op, ...schedule ? { schedule } : {} });
6858
- byNamespace.set(ns, home);
6859
- }
6860
- if (!byNamespace.size) return [];
6861
- for (const n of ir.nodes) {
6862
- if (n.kind !== NAMESPACE_KIND) continue;
6863
- const name = str3(rec3(n.attrs?.metadata)?.name);
6864
- const home = name ? byNamespace.get(name) : void 0;
6865
- if (home && !home.node) home.node = n.id;
6866
7173
  }
6867
- for (const home of byNamespace.values()) home.ticks.sort((a, b) => a.op.localeCompare(b.op));
6868
- return [...byNamespace.values()].sort((a, b) => a.namespace.localeCompare(b.namespace));
7174
+ return out;
6869
7175
  }
6870
- function markOperatorHome(ir) {
6871
- const homes = operatorHomes(ir);
6872
- if (!homes.length) return ir;
6873
- const marks = /* @__PURE__ */ new Map();
6874
- for (const home of homes) {
6875
- if (home.node) {
6876
- marks.set(home.node, {
6877
- role: "home",
6878
- namespace: home.namespace,
6879
- ...home.stack ? { stack: home.stack } : {},
6880
- ticks: home.ticks.map((t) => t.op)
6881
- });
7176
+ function mergeRun(prev, record) {
7177
+ const rank = { fail: 3, ok: 2, skipped: 1 };
7178
+ const keepPrev = prev && rank[prev.status] >= rank[record.status];
7179
+ return {
7180
+ status: keepPrev ? prev.status : record.status,
7181
+ durationMs: (prev?.durationMs ?? 0) + record.durationMs,
7182
+ ...keepPrev && prev.error ? { error: prev.error } : record.error ? { error: record.error } : {},
7183
+ ...keepPrev && prev.outcome ? { outcome: prev.outcome } : record.outcome ? { outcome: `${record.outcome.name}=${String(record.outcome.value)}` } : {},
7184
+ records: (prev?.records ?? 0) + 1
7185
+ };
7186
+ }
7187
+ function joinRun(op, records, gate) {
7188
+ const placed = opPlacedSteps(op);
7189
+ const slots = slotsFor(placed);
7190
+ const byKey = /* @__PURE__ */ new Map();
7191
+ for (const s of slots) {
7192
+ const key = `${s.phase}\0${s.fn}`;
7193
+ if (!byKey.has(key)) byKey.set(key, []);
7194
+ byKey.get(key).push({ id: s.id });
7195
+ }
7196
+ const cursor = /* @__PURE__ */ new Map();
7197
+ const byNode = /* @__PURE__ */ new Map();
7198
+ const unplaced = [];
7199
+ let lastSettledIndex = -1;
7200
+ const indexOf = new Map(placed.map((p, i) => [p.id, i]));
7201
+ for (const record of records) {
7202
+ const key = `${record.phase}\0${record.fn}`;
7203
+ const list3 = byKey.get(key);
7204
+ const i = cursor.get(key) ?? 0;
7205
+ const slot = list3?.[i];
7206
+ if (!slot) {
7207
+ unplaced.push(record);
7208
+ continue;
6882
7209
  }
6883
- for (const tick of home.ticks) {
6884
- marks.set(tick.node, {
6885
- role: "tick",
6886
- namespace: home.namespace,
6887
- ...home.stack ? { stack: home.stack } : {},
6888
- op: tick.op,
6889
- ...tick.schedule ? { schedule: tick.schedule } : {}
6890
- });
7210
+ cursor.set(key, i + 1);
7211
+ byNode.set(slot.id, mergeRun(byNode.get(slot.id), record));
7212
+ if (record.status !== "skipped") {
7213
+ lastSettledIndex = Math.max(lastSettledIndex, indexOf.get(slot.id) ?? -1);
7214
+ }
7215
+ }
7216
+ if (gate) {
7217
+ const gateNode = placed.find((p) => p.step.kind === "gate" && p.step.signalName === gate.signalName);
7218
+ if (gateNode) {
7219
+ return {
7220
+ byNode,
7221
+ at: gateNode.id,
7222
+ atReason: `waiting for ${gate.signalName} since ${gate.since}`,
7223
+ unplaced
7224
+ };
6891
7225
  }
6892
7226
  }
7227
+ const next = placed.find((p, i) => i > lastSettledIndex && p.when === "phases");
6893
7228
  return {
6894
- ...ir,
6895
- nodes: ir.nodes.map((n) => {
6896
- const mark = marks.get(n.id);
6897
- return mark ? { ...n, attrs: { ...n.attrs, _operator: mark } } : n;
6898
- })
7229
+ byNode,
7230
+ at: lastSettledIndex >= 0 && next ? next.id : null,
7231
+ atReason: lastSettledIndex >= 0 && next ? "reached \u2014 not settled yet" : null,
7232
+ unplaced
6899
7233
  };
6900
7234
  }
6901
- var OPERATOR_HOME_GLYPH = {
6902
- body: '<path d="M16.5 6.63A7 7 0 1 1 6.64 7.5"/><path d="M16.5 6.63 14.3 5.9M16.5 6.63 15.9 8.1"/>'
7235
+ var TONE = {
7236
+ ok: "good",
7237
+ fail: "warn",
7238
+ skipped: "neutral"
6903
7239
  };
6904
- function operatorHomeBoxMarks(ir, namespaceBoxes) {
6905
- const out = {};
6906
- for (const home of operatorHomes(ir)) {
6907
- const box = namespaceBoxes[home.namespace];
6908
- if (box) out[box] = OPERATOR_HOME_GLYPH;
6909
- }
6910
- return out;
6911
- }
6912
- function operatorStrip(rows, now = Date.now()) {
6913
- const out = rows.map((r) => {
6914
- const expiresAt = r.lease ? Date.parse(r.lease.expiresAt) : NaN;
6915
- const lease = !r.lease ? "free" : Number.isFinite(expiresAt) && expiresAt <= now ? "expired" : "held";
6916
- return {
6917
- op: r.op,
6918
- env: r.env,
6919
- log: r.lastTick?.log ?? null,
6920
- at: r.lastTick?.timestamp ?? null,
6921
- lease,
6922
- leaseHolder: r.lease?.holder ?? null,
6923
- leaseExpiresAt: r.lease?.expiresAt ?? null,
6924
- pendingGates: r.pendingGates.length
6925
- };
6926
- });
6927
- return { rows: out, pendingGates: out.reduce((n, r) => n + r.pendingGates, 0) };
7240
+ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
7241
+ function tookLabel(ms) {
7242
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
6928
7243
  }
6929
- var CONVERGE_GATE_LOOP = "converge";
6930
- var APPROVE_SEMANTICS = "Approving records a fact in the gate ledger; it does not unblock the dispatch. The next tick reads it.";
6931
- function convergeGateCards(rows) {
6932
- const out = [];
6933
- for (const row of rows) {
6934
- for (const g of row.pendingGates) {
6935
- if (!g.op) continue;
6936
- out.push({
6937
- loop: CONVERGE_GATE_LOOP,
6938
- rule: g.rule,
6939
- op: g.op,
6940
- gate: g.gate,
6941
- convergeOp: row.op,
6942
- env: row.env,
6943
- since: row.lastTick?.timestamp ?? null,
6944
- semantics: APPROVE_SEMANTICS,
6945
- approve: {
6946
- method: "POST",
6947
- path: `/api/operator/approve/${encodeURIComponent(g.op)}/${encodeURIComponent(g.gate)}`,
6948
- command: `chant approve ${g.op} ${g.gate}`
6949
- }
6950
- });
7244
+ function paintPlayhead(ir, join21, gate) {
7245
+ if (!join21.byNode.size && !join21.at) return ir;
7246
+ const nodes = ir.nodes.map((node) => {
7247
+ const run3 = join21.byNode.get(node.id);
7248
+ const here = join21.at === node.id;
7249
+ if (!run3 && !here) return node;
7250
+ const isGate = node.attrs?._step === "gate";
7251
+ const attrs = { ...node.attrs };
7252
+ if (run3) {
7253
+ attrs._status = TONE[run3.status];
7254
+ attrs.run = run3.status === "skipped" ? "skipped" : `${RAN[run3.status]} \xB7 ${tookLabel(run3.durationMs)}`;
7255
+ attrs.ran = tookLabel(run3.durationMs);
7256
+ if (run3.error) attrs.error = run3.error;
7257
+ if (run3.outcome) attrs.outcome = run3.outcome;
7258
+ if (run3.records > 1) attrs.runRecords = run3.records;
6951
7259
  }
6952
- }
6953
- return out;
7260
+ if (here) {
7261
+ attrs._playhead = true;
7262
+ attrs._status = isGate && gate ? "warn" : "accent";
7263
+ attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join21.atReason ?? "here";
7264
+ if (isGate && gate?.description) attrs.gateDescription = gate.description;
7265
+ }
7266
+ return { ...node, attrs };
7267
+ });
7268
+ return { ...ir, nodes };
6954
7269
  }
6955
- var initialOperatorState = {
6956
- declared: [],
6957
- strip: null,
6958
- gates: [],
6959
- note: null,
6960
- code: null,
6961
- readAt: null
6962
- };
6963
- function operatorRead(state, result, at, now = Date.now()) {
6964
- if (!result.ok) {
6965
- return { ...state, strip: null, gates: [], note: result.refusal.error, code: result.refusal.code, readAt: at };
6966
- }
7270
+ function pendingGateCard(op, gate, ir) {
7271
+ const node = ir.nodes.find((n) => n.attrs?._step === "gate" && n.attrs?.gate === gate.signalName && n.attrs?.op === op);
6967
7272
  return {
6968
- ...state,
6969
- strip: operatorStrip(result.rows, now),
6970
- gates: convergeGateCards(result.rows),
6971
- note: null,
6972
- code: null,
6973
- readAt: at
7273
+ op,
7274
+ signalName: gate.signalName,
7275
+ ...gate.description ? { description: gate.description } : {},
7276
+ since: gate.since,
7277
+ ...node ? { node: node.id } : {},
7278
+ ...node && typeof node.attrs?.guards === "string" ? { guards: node.attrs.guards } : {},
7279
+ approve: {
7280
+ method: "POST",
7281
+ path: `/api/ops/${encodeURIComponent(op)}/signal/${encodeURIComponent(gate.signalName)}`
7282
+ }
6974
7283
  };
6975
7284
  }
6976
- function operatorNote(state) {
6977
- if (!state.declared.length) {
6978
- return "No operating loop: this project declares no ConvergeOp, so there is no operator strip.";
6979
- }
6980
- const loops = `${state.declared.length} ConvergeOp${state.declared.length === 1 ? "" : "s"}`;
6981
- if (!state.strip) {
6982
- return state.note ? `Operating loop: ${loops} declared \u2014 status unavailable: ${state.note}` : `Operating loop: ${loops} declared \u2014 not read yet.`;
7285
+ function playheadNote(state, join21) {
7286
+ if (!state.op) {
7287
+ return "No run state: nothing has been run from this behold session, so every step paints as declared.";
6983
7288
  }
7289
+ const settled = join21 ? join21.byNode.size : state.records.length;
6984
7290
  const bits = [];
6985
- const ticked = state.strip.rows.filter((r) => r.at).length;
6986
- bits.push(
6987
- ticked ? `last tick only \u2014 chant's status surface exposes one tick per loop, not the history (chant#2029), so this is a strip and not a timeline` : `no tick recorded yet on any loop`
6988
- );
6989
- const held = state.strip.rows.filter((r) => r.lease === "held").length;
6990
- if (held) bits.push(`${held} lease${held === 1 ? "" : "s"} held`);
6991
- const expired = state.strip.rows.filter((r) => r.lease === "expired").length;
6992
- if (expired) bits.push(`${expired} lease${expired === 1 ? "" : "s"} expired \u2014 the next round reclaims it`);
6993
- if (state.strip.pendingGates) {
7291
+ const head = state.status === "idle" ? `Run state for ${state.op}: no per-step records \u2014 this behold session didn't start the run, so only the gate query answers` : `Run: ${state.op} (${state.mode === "temporal" ? "durable" : "local executor"}) \u2014 ${settled} step${settled === 1 ? "" : "s"} settled`;
7292
+ if (state.status === "running") {
6994
7293
  bits.push(
6995
- `${state.strip.pendingGates} converge gate${state.strip.pendingGates === 1 ? "" : "s"} pending \u2014 approving one records a fact for the next tick, it does not release a workflow (that is the run gate, a different card), and a gate fact carries no URL yet (chant#2028)`
7294
+ state.mode === "local" ? "still running; the local executor reports every step at once when it ends, so the track fills in on completion" : "still running; only settled steps are painted"
6996
7295
  );
6997
7296
  }
6998
- return `Operating loop: ${loops} \u2014 ${bits.join("; ")}.`;
7297
+ if (state.status === "ok") bits.push("the run exited cleanly");
7298
+ if (state.status === "failed") bits.push("a step failed");
7299
+ if (state.status === "lost") bits.push(`stream lost \u2014 ${state.lost ?? "the progress stream ended without a verdict"}`);
7300
+ if (state.gate) {
7301
+ bits.push(`waiting on gate ${state.gate.signalName} since ${state.gate.since} \u2014 approve is a delegated signal, behold never signals on its own`);
7302
+ } else if (state.gateNote) {
7303
+ bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
7304
+ }
7305
+ if (join21?.unplaced.length) {
7306
+ bits.push(
7307
+ `${join21.unplaced.length} record${join21.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join21.unplaced.length === 1 ? "is" : "are"} not painted`
7308
+ );
7309
+ }
7310
+ if (!bits.length) return `${head}.`;
7311
+ const rest = bits.join("; ");
7312
+ return `${head} \u2014 ${rest}.`;
6999
7313
  }
7000
7314
 
7001
7315
  // src/adopt.ts
@@ -8847,6 +9161,51 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
8847
9161
  const state = runner.noteOperator({ ok: true, rows });
8848
9162
  return c.json({ ...state, operator: state });
8849
9163
  });
9164
+ app.get("/api/operator/log", async (c) => {
9165
+ const { dirs } = convergeDirs();
9166
+ if (!dirs.length) {
9167
+ return c.json(
9168
+ {
9169
+ error: "This project declares no operating loop \u2014 no emitted Op carries chant's `Converge` search attribute.",
9170
+ code: "no-operator",
9171
+ remedy: HOW_TO_DECLARE,
9172
+ history: null
9173
+ },
9174
+ 200
9175
+ );
9176
+ }
9177
+ const window = operatorLogWindow({ limit: c.req.query("limit") ?? null, since: c.req.query("since") ?? null });
9178
+ if ("error" in window) {
9179
+ return c.json({ error: window.error, code: "operator-log", remedy: "Ask for a bounded window: `?limit=50&since=<iso>`.", history: null }, 400);
9180
+ }
9181
+ const answers = [];
9182
+ for (const dir of dirs) {
9183
+ const version = resolveChant(dir).version;
9184
+ if (!meetsFloor(version, OPERATOR_LOG_FLOOR)) {
9185
+ return c.json(
9186
+ {
9187
+ error: `This chant has no \`operator log\` \u2014 ${version ? `the project resolves chant ${version}` : "the project's chant"}, and the tick history landed in ${OPERATOR_LOG_FLOOR}.`,
9188
+ code: "no-operator-log-cli",
9189
+ remedy: `Upgrade the project's chant to ${OPERATOR_LOG_FLOOR} or newer (chant#2029). The strip itself needs only 0.52, so it keeps reading.`,
9190
+ history: null
9191
+ },
9192
+ 422
9193
+ );
9194
+ }
9195
+ const result = readOperatorLog(await runChantRaw(operatorLogArgs(window), dir));
9196
+ if (!result.ok) return c.json({ ...result.refusal, history: null }, result.refusal.code === "no-operator" ? 200 : 422);
9197
+ answers.push({ entries: result.entries, malformed: result.malformed });
9198
+ }
9199
+ const merged = mergeOperatorLogs(answers, window.limit);
9200
+ const history = {
9201
+ entries: operatorTimeline(merged.entries),
9202
+ malformed: merged.malformed,
9203
+ window: { limit: window.limit, since: window.since ?? null },
9204
+ more: merged.entries.length >= window.limit,
9205
+ readAt: (/* @__PURE__ */ new Date()).toISOString()
9206
+ };
9207
+ return c.json({ history });
9208
+ });
8850
9209
  app.post("/api/operator/approve/:op/:gate", async (c) => {
8851
9210
  const { op, gate } = c.req.param();
8852
9211
  const info = estateOps().find((o) => o.name === op);
@@ -9148,6 +9507,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9148
9507
  { method: "GET", path: "/api/ops", desc: "committed Ops + adopt lexicons + apply progress + run playhead" },
9149
9508
  { method: "GET", path: "/api/ops/:name/status", desc: "an Op's durable run status + pending gate (chant run status)" },
9150
9509
  { method: "GET", path: "/api/operator/status", desc: "the operating loop's strip: last tick per ConvergeOp + lease + pending converge gates" },
9510
+ { method: "GET", path: "/api/operator/log", desc: "the operating loop's timeline: converge ticks + gate resolutions, newest ?limit=n (max 200) from ?since=<iso>" },
9151
9511
  { method: "GET", path: "/api/history", desc: "recent source commits (rollback targets)" },
9152
9512
  { method: "GET", path: "/api/frames", desc: "captured lanes frames" },
9153
9513
  { method: "GET", path: "/api/events", desc: "SSE: changed / op / apply / run / pr" },
@@ -9259,6 +9619,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9259
9619
  if (env) {
9260
9620
  const rows = await componentStatus(cfg.projectDir, env, opts);
9261
9621
  ir = joinComponentStatus(ir, rows);
9622
+ ir = joinTickVerdicts(ir, verdictsForEnv(runner.operatorState.verdicts, env));
9262
9623
  mode = "component-status";
9263
9624
  metaEnv = env;
9264
9625
  }