@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.
- package/dist/cli.js +282 -46
- 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
|
|
2413
|
-
if (
|
|
2414
|
-
if (!isOwnNameAttr(n.kind,
|
|
2415
|
-
owner.set(
|
|
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
|
|
2424
|
-
if (
|
|
2425
|
-
const b = owner.get(
|
|
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:
|
|
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
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
4016
|
+
placeHelmReleases(nodes, byContainer, boxes);
|
|
3944
4017
|
nestReleaseBoxes(byContainer);
|
|
3945
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
3961
|
-
if (byContainer[nsBox]
|
|
3962
|
-
|
|
3963
|
-
|
|
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
|
|
9037
|
-
const
|
|
9038
|
-
const {
|
|
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
|
-
|
|
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
|
|
9230
|
-
const {
|
|
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.
|
|
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.
|
|
40
|
+
"@intentius/pinhole": "^0.3.7",
|
|
41
41
|
"hono": "^4.12.34"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|