@intentius/behold 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +282 -46
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2405,14 +2405,60 @@ function isOwnNameAttr(kind, key) {
2405
2405
  return typeSegment(kind).toLowerCase().includes(prefix);
2406
2406
  }
2407
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);
2426
+ }
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;
2434
+ }
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
+ }
2445
+ }
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
+ }
2452
+ return out;
2453
+ }
2408
2454
  function addValueMatchEdges(ir) {
2409
2455
  const nodes = ir.nodes;
2410
2456
  const owner = /* @__PURE__ */ new Map();
2411
2457
  for (const n of nodes) {
2412
- for (const [k, v] of Object.entries(n.attrs ?? {})) {
2413
- if (typeof v !== "string" || v.length < MIN_NAME_LEN) continue;
2414
- if (!isOwnNameAttr(n.kind, k)) continue;
2415
- owner.set(v, owner.has(v) ? null : n.id);
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);
2416
2462
  }
2417
2463
  }
2418
2464
  if (owner.size === 0) return ir;
@@ -2420,14 +2466,14 @@ function addValueMatchEdges(ir) {
2420
2466
  for (const e of ir.edges) declared.add(`${e.from}\0${e.to}`);
2421
2467
  const added = /* @__PURE__ */ new Set();
2422
2468
  for (const a of nodes) {
2423
- for (const [k, v] of Object.entries(a.attrs ?? {})) {
2424
- if (typeof v !== "string" || v.length < MIN_NAME_LEN) continue;
2425
- const b = owner.get(v);
2469
+ for (const { path, value } of stringLeaves(a.attrs)) {
2470
+ if (!joinable(value)) continue;
2471
+ const b = owner.get(value);
2426
2472
  if (!b || b === a.id) continue;
2427
2473
  const key = `${a.id}\0${b}`;
2428
2474
  if (declared.has(key) || added.has(key)) continue;
2429
2475
  added.add(key);
2430
- ir.edges.push({ from: a.id, to: b, kind: "ref", viaAttr: k, inferred: true });
2476
+ ir.edges.push({ from: a.id, to: b, kind: "ref", viaAttr: path, inferred: true });
2431
2477
  }
2432
2478
  }
2433
2479
  return ir;
@@ -2440,6 +2486,11 @@ var WORKLOAD_KINDS = /* @__PURE__ */ new Set([
2440
2486
  "K8s::Apps::DaemonSet",
2441
2487
  "K8s::Apps::ReplicaSet"
2442
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
+ ];
2443
2494
  function rec(v) {
2444
2495
  return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
2445
2496
  }
@@ -2462,6 +2513,13 @@ function labelRecord(v) {
2462
2513
  }
2463
2514
  return Object.keys(out).length > 0 ? out : void 0;
2464
2515
  }
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;
2522
+ }
2465
2523
  function selects(selector, labels) {
2466
2524
  return Object.entries(selector).every(([k, v]) => labels[k] === v);
2467
2525
  }
@@ -2556,15 +2614,19 @@ function deriveK8sEdges(nodes) {
2556
2614
  const target = candidates.length === 1 ? candidates[0] : candidates.find((p) => namespaceOf(p) === namespaceOf(n));
2557
2615
  if (target) add(n.id, target.id, viaAttr);
2558
2616
  };
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
+ }
2625
+ };
2559
2626
  for (const n of k8s) {
2560
- if (n.kind === "K8s::Core::Service") {
2561
- const selector = labelRecord(rec(n.attrs?.spec)?.selector);
2562
- if (!selector) continue;
2563
- for (const w of workloads) {
2564
- if (namespaceOf(w) !== namespaceOf(n)) continue;
2565
- const labels = templateLabels(w);
2566
- if (labels && selects(selector, labels)) add(n.id, w.id, "selector");
2567
- }
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);
2568
2630
  continue;
2569
2631
  }
2570
2632
  if (n.kind === "K8s::Networking::Ingress") {
@@ -2850,6 +2912,10 @@ async function mapPool(items, width, fn) {
2850
2912
  await Promise.all(Array.from({ length: Math.max(1, Math.min(width, items.length)) }, () => worker()));
2851
2913
  return results;
2852
2914
  }
2915
+ function estateMembers(projectDirs) {
2916
+ const names = shortStackNames([...projectDirs]);
2917
+ return projectDirs.map((dir, i) => ({ name: names[i], dir }));
2918
+ }
2853
2919
  async function composeEstate(projectDirs, opts = {}) {
2854
2920
  const names = shortStackNames(projectDirs);
2855
2921
  const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
@@ -3524,6 +3590,9 @@ var K8S_PLUMBING_KINDS = /* @__PURE__ */ new Set([
3524
3590
  "K8s::Rbac::ClusterRole",
3525
3591
  "K8s::Rbac::ClusterRoleBinding"
3526
3592
  ]);
3593
+ function namespaceBoxKey(ns) {
3594
+ return `namespace ${ns}`;
3595
+ }
3527
3596
  function metaStr(node, key) {
3528
3597
  const meta = node.attrs?.metadata;
3529
3598
  const v = meta?.[key];
@@ -3557,19 +3626,20 @@ function projectK8sLogical(ir, env, boundContext) {
3557
3626
  const cluster = boundManagedCluster(ir.nodes, boundContext);
3558
3627
  const CLUSTER_TITLE = cluster ? cluster.id : env ? `cluster ${env}` : "cluster";
3559
3628
  const CLUSTER_SCOPED2 = "cluster-scoped";
3560
- const namespaceTitle = (ns) => `namespace ${ns}`;
3561
3629
  const byContainer = {};
3562
3630
  const child = (parent, c) => {
3563
3631
  const arr = byContainer[parent] ?? (byContainer[parent] = []);
3564
3632
  if (!arr.includes(c)) arr.push(c);
3565
3633
  };
3566
- for (const ns of [...declaredNamespaces(k8s)].sort()) child(CLUSTER_TITLE, namespaceTitle(ns));
3634
+ const namespaceBoxes = {};
3635
+ const boxFor = (ns) => namespaceBoxes[ns] ??= namespaceBoxKey(ns);
3636
+ for (const ns of [...declaredNamespaces(k8s)].sort()) child(CLUSTER_TITLE, boxFor(ns));
3567
3637
  const headline = k8s.filter((n) => !K8S_PLUMBING_KINDS.has(n.kind));
3568
3638
  let clusterScoped = false;
3569
3639
  for (const n of headline) {
3570
3640
  const ns = metaStr(n, "namespace");
3571
3641
  if (ns) {
3572
- child(namespaceTitle(ns), n.id);
3642
+ child(boxFor(ns), n.id);
3573
3643
  } else {
3574
3644
  clusterScoped = true;
3575
3645
  child(CLUSTER_SCOPED2, n.id);
@@ -3578,7 +3648,7 @@ function projectK8sLogical(ir, env, boundContext) {
3578
3648
  if (clusterScoped) child(CLUSTER_TITLE, CLUSTER_SCOPED2);
3579
3649
  const kept = new Set(headline.map((n) => n.id));
3580
3650
  const edges = ir.edges.filter((e) => kept.has(e.from) && kept.has(e.to));
3581
- return { ir: { nodes: headline, edges, groups: {} }, byContainer };
3651
+ return { ir: { nodes: headline, edges, groups: {} }, byContainer, clusterBox: CLUSTER_TITLE, namespaceBoxes };
3582
3652
  }
3583
3653
 
3584
3654
  // src/logical-helm.ts
@@ -3932,6 +4002,7 @@ function projectTopology(ir, env, boundContext, sourceRoots) {
3932
4002
  const nodes = [];
3933
4003
  const edges = [];
3934
4004
  const byContainer = {};
4005
+ const boxes = { namespaces: {} };
3935
4006
  for (const p of projections) {
3936
4007
  nodes.push(...p.ir.nodes);
3937
4008
  edges.push(...p.ir.edges);
@@ -3939,28 +4010,67 @@ function projectTopology(ir, env, boundContext, sourceRoots) {
3939
4010
  const arr = byContainer[parent] ?? (byContainer[parent] = []);
3940
4011
  for (const c of children) if (!arr.includes(c)) arr.push(c);
3941
4012
  }
4013
+ if (p.clusterBox) boxes.cluster = p.clusterBox;
4014
+ Object.assign(boxes.namespaces, p.namespaceBoxes);
3942
4015
  }
3943
- placeHelmReleases(nodes, byContainer, ir, env, boundContext);
4016
+ placeHelmReleases(nodes, byContainer, boxes);
3944
4017
  nestReleaseBoxes(byContainer);
3945
- return { ir: { nodes, edges, groups: {} }, byContainer };
4018
+ retainCrossLensEdges(ir, projections, nodes, edges, byContainer);
4019
+ return { ir: { nodes, edges, groups: {} }, byContainer, namespaceBoxes: boxes.namespaces };
4020
+ }
4021
+ function retainCrossLensEdges(ir, projections, nodes, edges, byContainer) {
4022
+ const cards = new Set(nodes.map((n) => n.id));
4023
+ const perLens = projections.map((p) => new Set(p.ir.nodes.map((n) => n.id)));
4024
+ const pairKey = (a, b) => a < b ? `${a}|${b}` : `${b}|${a}`;
4025
+ const drawn = new Set(edges.map((e) => pairKey(e.from, e.to)));
4026
+ const parentsOf = /* @__PURE__ */ new Map();
4027
+ for (const [parent, children] of Object.entries(byContainer))
4028
+ for (const c of children) (parentsOf.get(c) ?? parentsOf.set(c, []).get(c)).push(parent);
4029
+ const nests = (outer, inner) => {
4030
+ const seen = /* @__PURE__ */ new Set([inner]);
4031
+ let frontier = [...parentsOf.get(inner) ?? []];
4032
+ while (frontier.length) {
4033
+ const next = [];
4034
+ for (const p of frontier) {
4035
+ if (seen.has(p)) continue;
4036
+ seen.add(p);
4037
+ if (p === outer) return true;
4038
+ for (const up of parentsOf.get(p) ?? []) if (!seen.has(up)) next.push(up);
4039
+ }
4040
+ frontier = next;
4041
+ }
4042
+ return false;
4043
+ };
4044
+ for (const e of ir.edges) {
4045
+ if (e.from === e.to || !cards.has(e.from) || !cards.has(e.to)) continue;
4046
+ if (perLens.some((ids) => ids.has(e.from) && ids.has(e.to))) continue;
4047
+ const key = pairKey(e.from, e.to);
4048
+ if (drawn.has(key)) continue;
4049
+ if (nests(e.from, e.to) || nests(e.to, e.from)) continue;
4050
+ drawn.add(key);
4051
+ edges.push(e);
4052
+ }
3946
4053
  }
3947
- function placeHelmReleases(nodes, byContainer, ir, env, boundContext) {
4054
+ function placeHelmReleases(nodes, byContainer, boxes) {
3948
4055
  const releases = nodes.filter((n) => n.kind === "Helm::Release");
3949
4056
  if (releases.length === 0) return;
3950
4057
  const child = (parent, c) => {
3951
4058
  const arr = byContainer[parent] ?? (byContainer[parent] = []);
3952
4059
  if (!arr.includes(c)) arr.push(c);
3953
4060
  };
3954
- const cluster = boundManagedCluster(ir.nodes, boundContext);
3955
- const clusterTitle = cluster ? cluster.id : env ? `cluster ${env}` : "cluster";
3956
- const clusterBoxExists = byContainer[clusterTitle] !== void 0;
4061
+ const clusterBox = boxes.cluster !== void 0 && byContainer[boxes.cluster] !== void 0 ? boxes.cluster : void 0;
3957
4062
  for (const r of releases) {
3958
4063
  const ns = r.attrs?.namespace;
3959
4064
  if (typeof ns !== "string" || ns.length === 0) continue;
3960
- const nsBox = `namespace ${ns}`;
3961
- if (byContainer[nsBox] === void 0 && !clusterBoxExists) continue;
3962
- if (byContainer[nsBox] === void 0) child(clusterTitle, nsBox);
3963
- child(nsBox, r.id);
4065
+ const nsBox = boxes.namespaces[ns];
4066
+ if (nsBox !== void 0 && byContainer[nsBox] !== void 0) {
4067
+ child(nsBox, r.id);
4068
+ continue;
4069
+ }
4070
+ if (clusterBox === void 0) continue;
4071
+ const observed = nsBox ?? namespaceBoxKey(ns);
4072
+ child(clusterBox, observed);
4073
+ child(observed, r.id);
3964
4074
  }
3965
4075
  }
3966
4076
  function nestReleaseBoxes(byContainer) {
@@ -5260,6 +5370,12 @@ function renderArchitecture(ir, byContainer, opts = {}) {
5260
5370
  nodesep: Math.round(48 + spread * 48),
5261
5371
  ranksep: Math.round(60 + spread * 56)
5262
5372
  });
5373
+ if (opts.groupMarks) {
5374
+ for (const box of layout.groups ?? []) {
5375
+ const mark = box.id !== void 0 ? opts.groupMarks[box.id] : void 0;
5376
+ if (mark !== void 0) box.mark = mark;
5377
+ }
5378
+ }
5263
5379
  const svg = renderSvg(ir, layout, {
5264
5380
  fit: true,
5265
5381
  hideTitle: true,
@@ -6782,6 +6898,17 @@ function markOperatorHome(ir) {
6782
6898
  })
6783
6899
  };
6784
6900
  }
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"/>'
6903
+ };
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
+ }
6785
6912
  function operatorStrip(rows, now = Date.now()) {
6786
6913
  const out = rows.map((r) => {
6787
6914
  const expiresAt = r.lease ? Date.parse(r.lease.expiresAt) : NaN;
@@ -7765,6 +7892,110 @@ async function openRollbackBranches(projectDir, env) {
7765
7892
  }
7766
7893
  }
7767
7894
 
7895
+ // src/estate-edges.ts
7896
+ import { statSync as statSync6 } from "node:fs";
7897
+ var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
7898
+ var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
7899
+ var realIsDir2 = (p) => {
7900
+ try {
7901
+ return statSync6(p).isDirectory();
7902
+ } catch {
7903
+ return false;
7904
+ }
7905
+ };
7906
+ function declaredDeliveries(n) {
7907
+ const spec = rec5(n.attrs?.spec);
7908
+ if (n.kind === "K8s::Flux::Kustomization") {
7909
+ const path = str5(spec?.path);
7910
+ return path ? [{ from: n.id, path, viaAttr: "reconciles" }] : [];
7911
+ }
7912
+ if (n.kind === "K8s::Argo::Application") {
7913
+ const sources = [spec?.source, ...Array.isArray(spec?.sources) ? spec.sources : []];
7914
+ const out = [];
7915
+ for (const s of sources) {
7916
+ const path = str5(rec5(s)?.path);
7917
+ if (path) out.push({ from: n.id, path, viaAttr: "delivers" });
7918
+ }
7919
+ return out;
7920
+ }
7921
+ return [];
7922
+ }
7923
+ function membershipOf(ir, members) {
7924
+ const named = new Set(members.map((m) => m.name));
7925
+ const byStack = ir.groups?.byStack;
7926
+ const out = /* @__PURE__ */ new Map();
7927
+ const grouped = rec5(byStack);
7928
+ if (grouped) {
7929
+ for (const [name, ids] of Object.entries(grouped)) {
7930
+ if (!named.has(name) || !Array.isArray(ids)) continue;
7931
+ for (const id of ids) if (typeof id === "string") out.set(id, name);
7932
+ }
7933
+ }
7934
+ if (out.size > 0) return out;
7935
+ const byLength = [...members].sort((a, b) => b.name.length - a.name.length);
7936
+ for (const n of ir.nodes) {
7937
+ const m = byLength.find((c) => n.id.startsWith(`${c.name}/`));
7938
+ if (m) out.set(n.id, m.name);
7939
+ }
7940
+ return out;
7941
+ }
7942
+ function deriveEstateMemberEdges(ir, members, isDir = realIsDir2) {
7943
+ if (members.length < 2) return [];
7944
+ const memberOf = membershipOf(ir, members);
7945
+ if (memberOf.size === 0) return [];
7946
+ const present = new Set(ir.nodes.map((n) => n.id));
7947
+ const pointedAt = /* @__PURE__ */ new Set();
7948
+ for (const e of ir.edges) {
7949
+ const from = memberOf.get(e.from);
7950
+ if (from && from === memberOf.get(e.to)) pointedAt.add(e.to);
7951
+ }
7952
+ const entryNodes = (name, lexicon) => {
7953
+ const own = ir.nodes.filter((n) => memberOf.get(n.id) === name && n.lexicon === lexicon).map((n) => n.id);
7954
+ const entries = own.filter((id) => !pointedAt.has(id));
7955
+ return entries.length > 0 ? entries : own;
7956
+ };
7957
+ const out = [];
7958
+ const seen = /* @__PURE__ */ new Set();
7959
+ for (const n of ir.nodes) {
7960
+ const home = memberOf.get(n.id);
7961
+ if (!home) continue;
7962
+ for (const delivery of declaredDeliveries(n)) {
7963
+ let winner;
7964
+ let tied = false;
7965
+ for (const m of members) {
7966
+ if (m.name === home) continue;
7967
+ const score = pathAlignment(delivery.path, m.dir, isDir);
7968
+ if (score === 0) continue;
7969
+ if (!winner || score > winner.score) {
7970
+ winner = { name: m.name, score };
7971
+ tied = false;
7972
+ } else if (score === winner.score) {
7973
+ tied = true;
7974
+ }
7975
+ }
7976
+ if (!winner || tied) continue;
7977
+ for (const to of entryNodes(winner.name, n.lexicon)) {
7978
+ if (!present.has(to) || to === delivery.from) continue;
7979
+ const key = `${delivery.from}\0${to}`;
7980
+ if (seen.has(key)) continue;
7981
+ seen.add(key);
7982
+ out.push({ from: delivery.from, to, kind: "ref", viaAttr: delivery.viaAttr, inferred: true });
7983
+ }
7984
+ }
7985
+ }
7986
+ return out;
7987
+ }
7988
+ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
7989
+ const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
7990
+ for (const e of deriveEstateMemberEdges(ir, members, isDir)) {
7991
+ const key = `${e.from}\0${e.to}`;
7992
+ if (existing.has(key)) continue;
7993
+ existing.add(key);
7994
+ ir.edges.push(e);
7995
+ }
7996
+ return ir;
7997
+ }
7998
+
7768
7999
  // src/events.ts
7769
8000
  import { watch, existsSync as existsSync11 } from "node:fs";
7770
8001
  import { join as join16 } from "node:path";
@@ -8996,15 +9227,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
8996
9227
  let metaEnv = cfg.env ?? null;
8997
9228
  if (multi) {
8998
9229
  ir = await composeEstate(cfg.projectDirs, logical ? { ...opts, detail: 3 } : opts);
8999
- ir = addValueMatchEdges(ir);
9000
9230
  ir = addK8sDeclaredEdges(ir);
9231
+ ir = addValueMatchEdges(ir);
9232
+ ir = addEstateMemberEdges(ir, estateMembers(cfg.projectDirs));
9001
9233
  ir = markOperatorHome(ir);
9002
9234
  const estateContext = await boundK8sContext(metaEnv ?? void 0);
9003
9235
  ir = addClusterAnchorEdges(ir, estateContext);
9004
9236
  if (logical) {
9005
9237
  const logicalBefore = ir.nodes.length;
9006
- const { ir: projected, byContainer } = projectTopology(ir, metaEnv ?? void 0, estateContext, await estateSourceRoots(opts));
9007
- const { svg: svg2 } = renderArchitecture(projected, byContainer);
9238
+ const { ir: projected, byContainer, namespaceBoxes } = projectTopology(ir, metaEnv ?? void 0, estateContext, await estateSourceRoots(opts));
9239
+ const { svg: svg2 } = renderArchitecture(projected, byContainer, { groupMarks: operatorHomeBoxMarks(ir, namespaceBoxes ?? {}) });
9008
9240
  const logicalNote = notesFor("logical", projected, void 0, logicalBefore);
9009
9241
  return c.json({
9010
9242
  ir: projected,
@@ -9033,16 +9265,17 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9033
9265
  ir = joinCiProgress(ir, runner.applyProgress);
9034
9266
  } else if (logical) {
9035
9267
  const raw = mergeClusterRoot(await graphIr(cfg.projectDir, { ...opts, detail: 3 }), await clusterRootGraphIr(cfg.projectDir, opts));
9036
- const base = addK8sDeclaredEdges(addValueMatchEdges(raw));
9037
- const { ir: projected, byContainer } = projectTopology(base, metaEnv ?? void 0, await boundK8sContext(metaEnv ?? void 0), [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9038
- const { svg: svg2 } = renderArchitecture(projected, byContainer);
9268
+ const logicalContext = await boundK8sContext(metaEnv ?? void 0);
9269
+ const base = addClusterAnchorEdges(addValueMatchEdges(addK8sDeclaredEdges(raw)), logicalContext);
9270
+ const { ir: projected, byContainer, namespaceBoxes } = projectTopology(base, metaEnv ?? void 0, logicalContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9271
+ const { svg: svg2 } = renderArchitecture(projected, byContainer, { groupMarks: operatorHomeBoxMarks(base, namespaceBoxes ?? {}) });
9039
9272
  const logicalNote = notesFor("logical", projected, void 0, base.nodes.length);
9040
9273
  return c.json({ ir: projected, svg: svg2, byContainer, meta: { projectDir: cfg.projectDir, env: metaEnv, tier: opts.tier ?? null, target: opts.target ?? null, mode: "logical", ...logicalNote ? { note: logicalNote } : {} } });
9041
9274
  } else {
9042
9275
  ir = mergeClusterRoot(await graphIr(cfg.projectDir, opts), await clusterRootGraphIr(cfg.projectDir, opts));
9043
9276
  if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
9044
- ir = addValueMatchEdges(ir);
9045
9277
  ir = addK8sDeclaredEdges(ir);
9278
+ ir = addValueMatchEdges(ir);
9046
9279
  ir = markOperatorHome(ir);
9047
9280
  ir = addClusterAnchorEdges(ir, await boundK8sContext(metaEnv ?? void 0));
9048
9281
  }
@@ -9064,7 +9297,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9064
9297
  const { svg } = renderGraph(ir, multi ? { boxes: "byStack" } : { radial });
9065
9298
  const srcZoom = components ? "components" : logical ? "logical" : opts.detail === 1 ? "composites" : opts.detail === 3 ? "attributes" : "resources";
9066
9299
  const estateLensNote = multi && components ? "the components lens doesn't apply to a composed estate yet \u2014 showing the composed entity graph" : void 0;
9067
- const srcNote = multi ? estateLensNote : notesFor(srcZoom, ir, srcCompositeEdgesAttached);
9300
+ const srcNote = multi ? estateLensNote : notesFor(srcZoom, ir, srcCompositeEdgesAttached, void 0, opts.detail ?? 2);
9068
9301
  return c.json({
9069
9302
  ir,
9070
9303
  svg,
@@ -9173,8 +9406,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9173
9406
  const composed = est.ir;
9174
9407
  let ir2 = runtime ? attachRuntimeContainment(composed) : pruneRuntimeChildren(composed);
9175
9408
  if ((detail ?? 2) < 3) ir2 = pruneImports(ir2);
9176
- ir2 = addValueMatchEdges(ir2);
9177
9409
  ir2 = addK8sDeclaredEdges(ir2);
9410
+ ir2 = addValueMatchEdges(ir2);
9411
+ ir2 = addEstateMemberEdges(ir2, estateMembers(cfg.projectDirs));
9178
9412
  ir2 = markOperatorHome(ir2);
9179
9413
  const boundContext2 = await boundK8sContext(env);
9180
9414
  ir2 = addClusterAnchorEdges(ir2, boundContext2);
@@ -9184,8 +9418,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9184
9418
  ].join("; ") : void 0;
9185
9419
  if (logical) {
9186
9420
  const logicalBefore = ir2.nodes.length;
9187
- const { ir: projected, byContainer } = projectTopology(ir2, env, boundContext2, await estateSourceRoots(query));
9188
- const { svg: svg3 } = renderArchitecture(projected, byContainer);
9421
+ const { ir: projected, byContainer, namespaceBoxes } = projectTopology(ir2, env, boundContext2, await estateSourceRoots(query));
9422
+ const { svg: svg3 } = renderArchitecture(projected, byContainer, { groupMarks: operatorHomeBoxMarks(ir2, namespaceBoxes ?? {}) });
9189
9423
  const note3 = [
9190
9424
  notesFor("logical", projected, void 0, logicalBefore),
9191
9425
  coverNote,
@@ -9200,11 +9434,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9200
9434
  });
9201
9435
  }
9202
9436
  const { svg: svg2 } = renderGraph(ir2, { boxes: runtime ? "byContainer" : "byStack" });
9437
+ const zoom2 = runtime ? "runtime" : detail === 1 ? "composites" : detail === 3 ? "attributes" : "resources";
9203
9438
  const note2 = [
9204
9439
  coverNote,
9205
9440
  namespaceJoinNote(est.joined),
9206
9441
  namespaceMismatchNote(withoutJoinedMembers(ir2.nodes, est.joined)),
9207
- runtime ? notesFor("runtime", ir2, void 0, void 0, detail) : void 0
9442
+ notesFor(zoom2, ir2, void 0, void 0, detail ?? 2)
9208
9443
  ].filter(Boolean).join(" \xB7 ");
9209
9444
  return c.json({
9210
9445
  ir: ir2,
@@ -9226,15 +9461,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9226
9461
  ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
9227
9462
  if (logical) {
9228
9463
  const logicalBefore = ir.nodes.length;
9229
- const { ir: projected, byContainer } = projectTopology(addK8sDeclaredEdges(addValueMatchEdges(ir)), env, boundContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9230
- const { svg: svg2 } = renderArchitecture(projected, byContainer);
9464
+ const projectionInput = addClusterAnchorEdges(addValueMatchEdges(addK8sDeclaredEdges(ir)), boundContext);
9465
+ const { ir: projected, byContainer, namespaceBoxes } = projectTopology(projectionInput, env, boundContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9466
+ const { svg: svg2 } = renderArchitecture(projected, byContainer, { groupMarks: operatorHomeBoxMarks(projectionInput, namespaceBoxes ?? {}) });
9231
9467
  const logicalTierNote = tierMismatchNote(projected, beholdConfig.tiers, query.tier);
9232
9468
  const logicalNote = [logicalTierNote, notesFor("logical", projected, void 0, logicalBefore)].filter(Boolean).join(" \xB7 ");
9233
9469
  return c.json({ ir: projected, svg: svg2, byContainer, meta: { projectDir: cfg.projectDir, env, mode: "logical", ...logicalNote ? { note: logicalNote } : {} } });
9234
9470
  }
9235
9471
  if ((query.detail ?? 2) < 3) ir = pruneImports(ir);
9236
- ir = addValueMatchEdges(ir);
9237
9472
  ir = addK8sDeclaredEdges(ir);
9473
+ ir = addValueMatchEdges(ir);
9238
9474
  ir = markOperatorHome(ir);
9239
9475
  ir = addClusterAnchorEdges(ir, boundContext);
9240
9476
  let compositeEdgesAttached;
@@ -9279,8 +9515,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9279
9515
  else pruneRuntimeChildren(result.ir);
9280
9516
  }
9281
9517
  if ((optsFromQuery(new URL(c.req.url)).detail ?? 2) < 3) pruneImports(result.ir);
9282
- addValueMatchEdges(result.ir);
9283
9518
  addK8sDeclaredEdges(result.ir);
9519
+ addValueMatchEdges(result.ir);
9284
9520
  addClusterAnchorEdges(result.ir, await boundK8sContext(env));
9285
9521
  const { svg } = renderGraph(result.ir, { boxes: "byContainer" });
9286
9522
  return c.json({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/behold",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "@hono/node-server": "^2.0.5",
38
38
  "@intentius/chant": "^0.52.1",
39
39
  "@intentius/chant-k8s-client": "^0.52.1",
40
- "@intentius/pinhole": "^0.3.5",
40
+ "@intentius/pinhole": "^0.3.7",
41
41
  "hono": "^4.12.34"
42
42
  },
43
43
  "devDependencies": {