Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/overlay.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { overlayStatus, reclassifyOverlay, pruneImports, attachRuntimeContainment } from "./overlay.ts";
import { overlayStatus, reclassifyOverlay, pruneImports, attachRuntimeContainment, pruneRuntimeChildren } from "./overlay.ts";

describe("overlayStatus", () => {
it("maps chant's _status vocabulary to overlay statuses", () => {
Expand Down Expand Up @@ -134,3 +134,49 @@ describe("pruneImports", () => {
expect(pruneImports(noImports).nodes).toHaveLength(1);
});
});

// #86 — the runtime layer becomes a TIER, which means it has to be possible to
// not descend. Before this, owner-referenced children were attached at every
// zoom level, so a composites view carried its Pods.
describe("pruneRuntimeChildren (#86)", () => {
const mixed = () => ({
nodes: [
{ id: "web", kind: "K8s::Apps::Deployment", attrs: { _status: "good" } },
{ id: "web-abc", kind: "K8s::Core::Pod", attrs: { _status: "runtime" }, runtimeOwner: "web" },
{ id: "web-def", kind: "K8s::Core::Pod", attrs: { _status: "runtime" }, runtimeOwner: "web" },
{ id: "svc", kind: "K8s::Core::Service", attrs: { _status: "good" } },
],
edges: [
{ from: "svc", to: "web" },
{ from: "web-abc", to: "web" },
],
groups: {},
});

it("drops runtime children, keeping every declared node", () => {
const out = pruneRuntimeChildren(mixed() as unknown as Parameters<typeof pruneRuntimeChildren>[0]);
expect(out.nodes.map((n) => n.id).sort()).toEqual(["svc", "web"]);
});

it("drops edges touching a removed node, so nothing dangles", () => {
const out = pruneRuntimeChildren(mixed() as unknown as Parameters<typeof pruneRuntimeChildren>[0]);
expect(out.edges).toEqual([{ from: "svc", to: "web" }]);
});

it("is a no-op on a graph with no owner chain — AWS and Azure are untouched", () => {
const aws = {
nodes: [{ id: "vpc", kind: "AWS::EC2::VPC", attrs: { _status: "good" } }],
edges: [{ from: "vpc", to: "vpc" }],
groups: {},
};
const before = JSON.stringify(aws);
expect(JSON.stringify(pruneRuntimeChildren(aws as unknown as Parameters<typeof pruneRuntimeChildren>[0]))).toBe(before);
});

it("is the inverse of attaching them — the two tiers show different graphs", () => {
const attached = attachRuntimeContainment(mixed() as unknown as Parameters<typeof attachRuntimeContainment>[0]);
const pruned = pruneRuntimeChildren(mixed() as unknown as Parameters<typeof pruneRuntimeChildren>[0]);
expect(attached.nodes.length).toBe(4);
expect(pruned.nodes.length).toBe(2);
});
});
23 changes: 23 additions & 0 deletions src/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ export function reclassifyOverlay<T extends { nodes: OverlayNode[] }>(ir: T): T
* criterion that the tier is gracefully unavailable, never broken or blank.
* Mutates + returns `ir`. Pure w.r.t. I/O.
*/
/**
* Drop live-only runtime children from an overlay (#86).
*
* chant returns a Pod its Deployment's controller created as a node carrying
* `runtimeOwner` (chant#1180). Those nodes were attached at every zoom level,
* so a project's `composites` view carried its Pods — which is noise at that
* altitude, and meant the runtime layer could be seen but never dialled away.
*
* Making it a TIER means the other tiers stop where your source stops, which is
* what "descend below the declaration boundary" implies: you have to be able to
* not descend. Edges touching a dropped node go with it, since an edge to a node
* that is not in the graph renders as a dangling stub.
*/
export function pruneRuntimeChildren<T extends { nodes: OverlayNode[]; edges: Array<{ from: string; to: string }> }>(
ir: T,
): T {
const runtime = new Set(ir.nodes.filter((n) => n.runtimeOwner).map((n) => n.id));
if (runtime.size === 0) return ir;
ir.nodes = ir.nodes.filter((n) => !runtime.has(n.id));
ir.edges = ir.edges.filter((e) => !runtime.has(e.from) && !runtime.has(e.to));
return ir;
}

export function attachRuntimeContainment<T extends { nodes: OverlayNode[]; groups: { byContainer?: Record<string, string[]> } }>(
ir: T,
): T {
Expand Down
10 changes: 8 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
type ChantFailure,
} from "./chant.ts";
import { joinComponentStatus, componentStatusColor } from "./component-status.ts";
import { reclassifyOverlay, pruneImports, attachRuntimeContainment } from "./overlay.ts";
import { reclassifyOverlay, pruneImports, attachRuntimeContainment, pruneRuntimeChildren } from "./overlay.ts";
import { addValueMatchEdges } from "./value-match.ts";
import { addClusterAnchorEdges } from "./cluster-anchor.ts";
import { projectTopology } from "./logical.ts";
Expand Down Expand Up @@ -728,7 +728,13 @@ export function createApp(
// under its declared owner in `groups.byContainer` — a no-op on a
// substrate with no owner chain. `renderGraph`'s `boxes: "byContainer"`
// opt-in below draws it as a titled boundary box.
ir = attachRuntimeContainment(ir);
// The runtime tier (#86): `?runtime=1` descends below the declaration
// boundary and nests each owner-referenced child under its declared
// parent. Every other tier stops where your source stops — a Pod is noise
// in a composites view, and a layer you cannot dial away is not a tier.
ir = new URL(c.req.url).searchParams.get("runtime") === "1"
? attachRuntimeContainment(ir)
: pruneRuntimeChildren(ir);
// Logical/architecture lens (#63): re-project the live overlay into nested
// region/VPC/subnet ⊃ component boxes, keeping each surviving node's drift
// colour. Short-circuits the detail-tier pruning/composite plumbing below.
Expand Down
18 changes: 15 additions & 3 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ if (staticMode) {

/** Canonical key for a read URL — path + the lens params (whitelisted, sorted)
* that select a distinct snapshot. MUST match src/export.ts `canonicalKey`. */
const LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
const LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "runtime", "tier"];
function canonicalKey(path, params) {
// Components + logical views ignore detail/radial — drop them so they match
// the single captured snapshot (MUST match src/export.ts).
Expand Down Expand Up @@ -542,7 +542,7 @@ function wire(ir) {
// null on a project that declares no `stacks[]` at all — the picker (and the
// status strip's stack tag) then never renders. Every fetch reads this, so
// the `changed` SSE re-pull and a palette lens change go through the same path.
const view = { env: null, detail: 2, components: true, logical: false, tier: null, target: null, stack: null, radial: false };
const view = { env: null, detail: 2, components: true, logical: false, runtime: false, tier: null, target: null, stack: null, radial: false };

// v0.1.0 preview lock (set from /api/project in initActions): hides the git/PR
// write ops (Rollback, Sync, Adopt, Run ▾) — the server also 403s them. Local
Expand All @@ -565,18 +565,25 @@ const ZOOM_OPTS = [
["zoom: composites", "composites"],
["zoom: resources", "resources"],
["zoom: attributes", "attributes"],
// Below the declaration boundary (#86): the owner-referenced children the
// cluster maintains — the Pods under a Deployment. Its own stop rather than a
// detail level, because it is a different axis: every tier above shows what
// you declared, and this one shows what your declaration produced.
["zoom: runtime", "runtime"],
];
const ZOOM_DETAIL = { composites: 1, resources: 2, attributes: 3 };
const ZOOM_DETAIL = { composites: 1, resources: 2, attributes: 3, runtime: 3 };
/** Current zoom value from (components, logical, detail). */
function zoomValue() {
if (view.components) return "components";
if (view.logical) return "logical";
if (view.runtime) return "runtime";
return { 1: "composites", 2: "resources", 3: "attributes" }[view.detail] ?? "resources";
}
/** Apply a zoom value back onto (components, logical, detail). */
function applyZoom(z) {
view.components = z === "components";
view.logical = z === "logical";
view.runtime = z === "runtime";
if (z !== "components" && z !== "logical") view.detail = ZOOM_DETAIL[z] ?? 2;
}

Expand Down Expand Up @@ -1445,6 +1452,11 @@ async function load(opts = {}) {
} else if (view.env) {
endpoint = "/api/overlay";
q.set("env", view.env);
// The runtime tier (#86) descends below the declaration boundary. It is
// live-only by nature — owner-referenced children exist in the cluster,
// never in your source — so it rides the overlay and means nothing
// without an env.
if (view.runtime) q.set("runtime", "1");
}
// Radial layout (entity view only) — curl the wide DAG onto concentric rings.
if (view.radial && !view.components && !view.logical) q.set("radial", "1");
Expand Down
Loading