@intentius/behold 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +232 -25
  2. package/package.json +1 -1
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) => ({
@@ -3942,8 +4008,42 @@ function projectTopology(ir, env, boundContext, sourceRoots) {
3942
4008
  }
3943
4009
  placeHelmReleases(nodes, byContainer, ir, env, boundContext);
3944
4010
  nestReleaseBoxes(byContainer);
4011
+ retainCrossLensEdges(ir, projections, nodes, edges, byContainer);
3945
4012
  return { ir: { nodes, edges, groups: {} }, byContainer };
3946
4013
  }
4014
+ function retainCrossLensEdges(ir, projections, nodes, edges, byContainer) {
4015
+ const cards = new Set(nodes.map((n) => n.id));
4016
+ const perLens = projections.map((p) => new Set(p.ir.nodes.map((n) => n.id)));
4017
+ const pairKey = (a, b) => a < b ? `${a}|${b}` : `${b}|${a}`;
4018
+ const drawn = new Set(edges.map((e) => pairKey(e.from, e.to)));
4019
+ const parentsOf = /* @__PURE__ */ new Map();
4020
+ for (const [parent, children] of Object.entries(byContainer))
4021
+ for (const c of children) (parentsOf.get(c) ?? parentsOf.set(c, []).get(c)).push(parent);
4022
+ const nests = (outer, inner) => {
4023
+ const seen = /* @__PURE__ */ new Set([inner]);
4024
+ let frontier = [...parentsOf.get(inner) ?? []];
4025
+ while (frontier.length) {
4026
+ const next = [];
4027
+ for (const p of frontier) {
4028
+ if (seen.has(p)) continue;
4029
+ seen.add(p);
4030
+ if (p === outer) return true;
4031
+ for (const up of parentsOf.get(p) ?? []) if (!seen.has(up)) next.push(up);
4032
+ }
4033
+ frontier = next;
4034
+ }
4035
+ return false;
4036
+ };
4037
+ for (const e of ir.edges) {
4038
+ if (e.from === e.to || !cards.has(e.from) || !cards.has(e.to)) continue;
4039
+ if (perLens.some((ids) => ids.has(e.from) && ids.has(e.to))) continue;
4040
+ const key = pairKey(e.from, e.to);
4041
+ if (drawn.has(key)) continue;
4042
+ if (nests(e.from, e.to) || nests(e.to, e.from)) continue;
4043
+ drawn.add(key);
4044
+ edges.push(e);
4045
+ }
4046
+ }
3947
4047
  function placeHelmReleases(nodes, byContainer, ir, env, boundContext) {
3948
4048
  const releases = nodes.filter((n) => n.kind === "Helm::Release");
3949
4049
  if (releases.length === 0) return;
@@ -7765,6 +7865,110 @@ async function openRollbackBranches(projectDir, env) {
7765
7865
  }
7766
7866
  }
7767
7867
 
7868
+ // src/estate-edges.ts
7869
+ import { statSync as statSync6 } from "node:fs";
7870
+ var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
7871
+ var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
7872
+ var realIsDir2 = (p) => {
7873
+ try {
7874
+ return statSync6(p).isDirectory();
7875
+ } catch {
7876
+ return false;
7877
+ }
7878
+ };
7879
+ function declaredDeliveries(n) {
7880
+ const spec = rec5(n.attrs?.spec);
7881
+ if (n.kind === "K8s::Flux::Kustomization") {
7882
+ const path = str5(spec?.path);
7883
+ return path ? [{ from: n.id, path, viaAttr: "reconciles" }] : [];
7884
+ }
7885
+ if (n.kind === "K8s::Argo::Application") {
7886
+ const sources = [spec?.source, ...Array.isArray(spec?.sources) ? spec.sources : []];
7887
+ const out = [];
7888
+ for (const s of sources) {
7889
+ const path = str5(rec5(s)?.path);
7890
+ if (path) out.push({ from: n.id, path, viaAttr: "delivers" });
7891
+ }
7892
+ return out;
7893
+ }
7894
+ return [];
7895
+ }
7896
+ function membershipOf(ir, members) {
7897
+ const named = new Set(members.map((m) => m.name));
7898
+ const byStack = ir.groups?.byStack;
7899
+ const out = /* @__PURE__ */ new Map();
7900
+ const grouped = rec5(byStack);
7901
+ if (grouped) {
7902
+ for (const [name, ids] of Object.entries(grouped)) {
7903
+ if (!named.has(name) || !Array.isArray(ids)) continue;
7904
+ for (const id of ids) if (typeof id === "string") out.set(id, name);
7905
+ }
7906
+ }
7907
+ if (out.size > 0) return out;
7908
+ const byLength = [...members].sort((a, b) => b.name.length - a.name.length);
7909
+ for (const n of ir.nodes) {
7910
+ const m = byLength.find((c) => n.id.startsWith(`${c.name}/`));
7911
+ if (m) out.set(n.id, m.name);
7912
+ }
7913
+ return out;
7914
+ }
7915
+ function deriveEstateMemberEdges(ir, members, isDir = realIsDir2) {
7916
+ if (members.length < 2) return [];
7917
+ const memberOf = membershipOf(ir, members);
7918
+ if (memberOf.size === 0) return [];
7919
+ const present = new Set(ir.nodes.map((n) => n.id));
7920
+ const pointedAt = /* @__PURE__ */ new Set();
7921
+ for (const e of ir.edges) {
7922
+ const from = memberOf.get(e.from);
7923
+ if (from && from === memberOf.get(e.to)) pointedAt.add(e.to);
7924
+ }
7925
+ const entryNodes = (name, lexicon) => {
7926
+ const own = ir.nodes.filter((n) => memberOf.get(n.id) === name && n.lexicon === lexicon).map((n) => n.id);
7927
+ const entries = own.filter((id) => !pointedAt.has(id));
7928
+ return entries.length > 0 ? entries : own;
7929
+ };
7930
+ const out = [];
7931
+ const seen = /* @__PURE__ */ new Set();
7932
+ for (const n of ir.nodes) {
7933
+ const home = memberOf.get(n.id);
7934
+ if (!home) continue;
7935
+ for (const delivery of declaredDeliveries(n)) {
7936
+ let winner;
7937
+ let tied = false;
7938
+ for (const m of members) {
7939
+ if (m.name === home) continue;
7940
+ const score = pathAlignment(delivery.path, m.dir, isDir);
7941
+ if (score === 0) continue;
7942
+ if (!winner || score > winner.score) {
7943
+ winner = { name: m.name, score };
7944
+ tied = false;
7945
+ } else if (score === winner.score) {
7946
+ tied = true;
7947
+ }
7948
+ }
7949
+ if (!winner || tied) continue;
7950
+ for (const to of entryNodes(winner.name, n.lexicon)) {
7951
+ if (!present.has(to) || to === delivery.from) continue;
7952
+ const key = `${delivery.from}\0${to}`;
7953
+ if (seen.has(key)) continue;
7954
+ seen.add(key);
7955
+ out.push({ from: delivery.from, to, kind: "ref", viaAttr: delivery.viaAttr, inferred: true });
7956
+ }
7957
+ }
7958
+ }
7959
+ return out;
7960
+ }
7961
+ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
7962
+ const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
7963
+ for (const e of deriveEstateMemberEdges(ir, members, isDir)) {
7964
+ const key = `${e.from}\0${e.to}`;
7965
+ if (existing.has(key)) continue;
7966
+ existing.add(key);
7967
+ ir.edges.push(e);
7968
+ }
7969
+ return ir;
7970
+ }
7971
+
7768
7972
  // src/events.ts
7769
7973
  import { watch, existsSync as existsSync11 } from "node:fs";
7770
7974
  import { join as join16 } from "node:path";
@@ -8996,8 +9200,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
8996
9200
  let metaEnv = cfg.env ?? null;
8997
9201
  if (multi) {
8998
9202
  ir = await composeEstate(cfg.projectDirs, logical ? { ...opts, detail: 3 } : opts);
8999
- ir = addValueMatchEdges(ir);
9000
9203
  ir = addK8sDeclaredEdges(ir);
9204
+ ir = addValueMatchEdges(ir);
9205
+ ir = addEstateMemberEdges(ir, estateMembers(cfg.projectDirs));
9001
9206
  ir = markOperatorHome(ir);
9002
9207
  const estateContext = await boundK8sContext(metaEnv ?? void 0);
9003
9208
  ir = addClusterAnchorEdges(ir, estateContext);
@@ -9033,7 +9238,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9033
9238
  ir = joinCiProgress(ir, runner.applyProgress);
9034
9239
  } else if (logical) {
9035
9240
  const raw = mergeClusterRoot(await graphIr(cfg.projectDir, { ...opts, detail: 3 }), await clusterRootGraphIr(cfg.projectDir, opts));
9036
- const base = addK8sDeclaredEdges(addValueMatchEdges(raw));
9241
+ const base = addValueMatchEdges(addK8sDeclaredEdges(raw));
9037
9242
  const { ir: projected, byContainer } = projectTopology(base, metaEnv ?? void 0, await boundK8sContext(metaEnv ?? void 0), [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9038
9243
  const { svg: svg2 } = renderArchitecture(projected, byContainer);
9039
9244
  const logicalNote = notesFor("logical", projected, void 0, base.nodes.length);
@@ -9041,8 +9246,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9041
9246
  } else {
9042
9247
  ir = mergeClusterRoot(await graphIr(cfg.projectDir, opts), await clusterRootGraphIr(cfg.projectDir, opts));
9043
9248
  if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
9044
- ir = addValueMatchEdges(ir);
9045
9249
  ir = addK8sDeclaredEdges(ir);
9250
+ ir = addValueMatchEdges(ir);
9046
9251
  ir = markOperatorHome(ir);
9047
9252
  ir = addClusterAnchorEdges(ir, await boundK8sContext(metaEnv ?? void 0));
9048
9253
  }
@@ -9064,7 +9269,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9064
9269
  const { svg } = renderGraph(ir, multi ? { boxes: "byStack" } : { radial });
9065
9270
  const srcZoom = components ? "components" : logical ? "logical" : opts.detail === 1 ? "composites" : opts.detail === 3 ? "attributes" : "resources";
9066
9271
  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);
9272
+ const srcNote = multi ? estateLensNote : notesFor(srcZoom, ir, srcCompositeEdgesAttached, void 0, opts.detail ?? 2);
9068
9273
  return c.json({
9069
9274
  ir,
9070
9275
  svg,
@@ -9173,8 +9378,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9173
9378
  const composed = est.ir;
9174
9379
  let ir2 = runtime ? attachRuntimeContainment(composed) : pruneRuntimeChildren(composed);
9175
9380
  if ((detail ?? 2) < 3) ir2 = pruneImports(ir2);
9176
- ir2 = addValueMatchEdges(ir2);
9177
9381
  ir2 = addK8sDeclaredEdges(ir2);
9382
+ ir2 = addValueMatchEdges(ir2);
9383
+ ir2 = addEstateMemberEdges(ir2, estateMembers(cfg.projectDirs));
9178
9384
  ir2 = markOperatorHome(ir2);
9179
9385
  const boundContext2 = await boundK8sContext(env);
9180
9386
  ir2 = addClusterAnchorEdges(ir2, boundContext2);
@@ -9200,11 +9406,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9200
9406
  });
9201
9407
  }
9202
9408
  const { svg: svg2 } = renderGraph(ir2, { boxes: runtime ? "byContainer" : "byStack" });
9409
+ const zoom2 = runtime ? "runtime" : detail === 1 ? "composites" : detail === 3 ? "attributes" : "resources";
9203
9410
  const note2 = [
9204
9411
  coverNote,
9205
9412
  namespaceJoinNote(est.joined),
9206
9413
  namespaceMismatchNote(withoutJoinedMembers(ir2.nodes, est.joined)),
9207
- runtime ? notesFor("runtime", ir2, void 0, void 0, detail) : void 0
9414
+ notesFor(zoom2, ir2, void 0, void 0, detail ?? 2)
9208
9415
  ].filter(Boolean).join(" \xB7 ");
9209
9416
  return c.json({
9210
9417
  ir: ir2,
@@ -9226,15 +9433,15 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9226
9433
  ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
9227
9434
  if (logical) {
9228
9435
  const logicalBefore = ir.nodes.length;
9229
- const { ir: projected, byContainer } = projectTopology(addK8sDeclaredEdges(addValueMatchEdges(ir)), env, boundContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9436
+ const { ir: projected, byContainer } = projectTopology(addValueMatchEdges(addK8sDeclaredEdges(ir)), env, boundContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
9230
9437
  const { svg: svg2 } = renderArchitecture(projected, byContainer);
9231
9438
  const logicalTierNote = tierMismatchNote(projected, beholdConfig.tiers, query.tier);
9232
9439
  const logicalNote = [logicalTierNote, notesFor("logical", projected, void 0, logicalBefore)].filter(Boolean).join(" \xB7 ");
9233
9440
  return c.json({ ir: projected, svg: svg2, byContainer, meta: { projectDir: cfg.projectDir, env, mode: "logical", ...logicalNote ? { note: logicalNote } : {} } });
9234
9441
  }
9235
9442
  if ((query.detail ?? 2) < 3) ir = pruneImports(ir);
9236
- ir = addValueMatchEdges(ir);
9237
9443
  ir = addK8sDeclaredEdges(ir);
9444
+ ir = addValueMatchEdges(ir);
9238
9445
  ir = markOperatorHome(ir);
9239
9446
  ir = addClusterAnchorEdges(ir, boundContext);
9240
9447
  let compositeEdgesAttached;
@@ -9279,8 +9486,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
9279
9486
  else pruneRuntimeChildren(result.ir);
9280
9487
  }
9281
9488
  if ((optsFromQuery(new URL(c.req.url)).detail ?? 2) < 3) pruneImports(result.ir);
9282
- addValueMatchEdges(result.ir);
9283
9489
  addK8sDeclaredEdges(result.ir);
9490
+ addValueMatchEdges(result.ir);
9284
9491
  addClusterAnchorEdges(result.ir, await boundK8sContext(env));
9285
9492
  const { svg } = renderGraph(result.ir, { boxes: "byContainer" });
9286
9493
  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.0",
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": {