/** * Native manifest access — pure selectors over `@nwire/scan`'s `Manifest`. * * Studio reads the deep manifest directly. Types are `import type` from * `@nwire/scan` — erased at build, so `typescript` never * enters the browser bundle. Selectors are pure so they unit-test without a DOM. */ import type { Manifest, ManifestModel, GraphNode, GraphEdge, EdgeType } from "@nwire/scan"; export type { Manifest, ManifestModel, GraphNode, GraphEdge, EdgeType }; /** The manifest version this Studio build understands. */ export const SUPPORTED_MANIFEST_VERSION = 4; /** * A normalized view over a manifest — tolerant of missing/old fields. Never * throws on a partial manifest; callers render empty states instead. */ export interface ManifestView { readonly manifest: Manifest; readonly model: ManifestModel; /** True when the manifest version differs from what this build understands. */ readonly versionMismatch: boolean; /** Nodes of one graph kind (e.g. "action", "app", "capability"). */ byKind(kind: string): GraphNode[]; /** A node by id (`"${kind}:${name}"`). */ node(id: string): GraphNode | undefined; /** Edges touching a node — `dir` filters to outgoing/incoming/both. */ edgesOf(id: string, dir?: "out" | "in" | "both"): GraphEdge[]; /** Edges of a given type. */ edgesByType(type: EdgeType): GraphEdge[]; } /** * Flat per-kind array key → graph node kind. Used to synthesize a model when a * manifest predates the deep `model` (resilience: older/partial manifests still * browse in Inspect). Mirrors `@nwire/scan`'s graph kinds. */ const FLAT_KIND: ReadonlyArray = [ ["apps", "app"], ["actions", "action"], ["events", "event"], ["actors", "actor"], ["projections", "projection"], ["queries", "query"], ["resolvers", "resolver"], ["routes", "route"], ["workflows", "workflow"], ["externalCalls", "externalCall"], ["inboundWebhooks", "inboundWebhook"], ["outboxes", "outbox"], ["inboxes", "inbox"], ["crons", "cron"], ["commands", "command"], ["hooks", "hook"], ["plugins", "plugin"], ["sinks", "sinkStage"], ["bindings", "binding"], ["resources", "resource"], ["errors", "error"], ]; const NODE_DISPLAY_FIELDS = new Set(["name", "app", "description", "public", "source"]); /** * Build a node+edge model from a manifest's flat per-kind arrays — the fallback * for manifests without a deep `model`. Nodes cover every present kind; edges * cover the statically-derivable ones (action→event `emits`, query→projection * `reads`). Pure. */ export function synthesizeModel(manifest: Manifest): ManifestModel { const m = manifest as unknown as Record; const nodes: GraphNode[] = []; const edges: GraphEdge[] = []; const seen = new Set(); for (const [key, kind] of FLAT_KIND) { const list = m[key]; if (!Array.isArray(list)) continue; for (const raw of list as Record[]) { const name = typeof raw.name === "string" ? raw.name : typeof raw.id === "string" ? raw.id : undefined; if (!name) continue; const id = `${kind}:${name}`; if (seen.has(id)) continue; seen.add(id); const data: Record = {}; for (const [k, v] of Object.entries(raw)) { if (!NODE_DISPLAY_FIELDS.has(k) && v !== undefined) data[k] = v; } nodes.push({ id, kind, name, source: raw.source as GraphNode["source"], intent: { description: typeof raw.description === "string" ? raw.description : undefined, public: raw.public === true, }, data, }); if (kind === "action" && Array.isArray(raw.emits)) { for (const ev of raw.emits as string[]) edges.push({ from: id, to: `event:${ev}`, type: "emits" }); } if (kind === "query" && typeof raw.projection === "string") { edges.push({ from: id, to: `projection:${raw.projection}`, type: "reads" }); } } } return { nodes, edges }; } /** Build a tolerant view over a raw manifest object. */ export function manifestView(manifest: Manifest): ManifestView { const model: ManifestModel = manifest.model && Array.isArray(manifest.model.nodes) && manifest.model.nodes.length ? manifest.model : synthesizeModel(manifest); const nodes = model.nodes ?? []; const edges = model.edges ?? []; const byId = new Map(nodes.map((n) => [n.id, n])); const version = (manifest as { version?: number }).version; return { manifest, model, versionMismatch: typeof version === "number" && version !== SUPPORTED_MANIFEST_VERSION, byKind: (kind) => nodes.filter((n) => n.kind === kind), node: (id) => byId.get(id), edgesOf: (id, dir = "both") => edges.filter((e) => dir === "out" ? e.from === id : dir === "in" ? e.to === id : e.from === id || e.to === id, ), edgesByType: (type) => edges.filter((e) => e.type === type), }; }