/** * Manifest health — pure checks over a raw `.nwire/manifest.json` payload. * * The manifest is generated by `@nwire/scan` and its schema evolves. A Studio * served against a manifest built by an older scanner must not crash; pages * that depend on a not-yet-present field render an empty state, and the operator * gets a "rebuild" hint naming exactly what's missing. * * Reports against the native manifest directly so App.vue can drive the * stale-manifest banner from the deep manifest, no flat reshape in between. */ import type { Manifest } from "./manifest"; /** * Top-level array fields the *current* `@nwire/scan` emits on every manifest * (always present, empty when a kind is unused — verified against real * `buildManifest` output). A field that's absent (or not an array) means the * manifest was built by an older scanner; we name it in the rebuild hint. * * Deliberately NOT the old flat-cache set: the deep manifest keeps apps, * resolvers, routes, hooks, plugins, sinks, bindings under `topology`/`model`, * not as top-level arrays — checking for them here false-flagged every real * manifest. `generatedAt` isn't emitted at top level either, so it's dropped. */ const ARRAY_FIELDS = [ "events", "actions", "actors", "projections", "queries", "workflows", "commands", "crons", "externalCalls", "inboundWebhooks", "outboxes", "inboxes", "resources", "errors", "schemas", ] as const; /** * The field paths a manifest is missing relative to the current schema. Empty * when the manifest is current-shape. Pure — no DOM, unit-testable. * * Order matches the schema declaration so the banner reads predictably * (`resolvers`, `workflows`, …). */ export function missingFields(manifest: Manifest | null | undefined): string[] { if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) { return []; } const input = manifest as unknown as Record; const missing: string[] = []; for (const field of ARRAY_FIELDS) { if (!Array.isArray(input[field])) missing.push(field); } // graph.events — nested arrays. const rawGraph = input.graph as { events?: unknown } | undefined; if (!rawGraph || typeof rawGraph !== "object") { missing.push("graph"); } else if (!Array.isArray(rawGraph.events)) { missing.push("graph.events"); } return missing; }