everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 15.7 kB
Source Map (JSON)
{"version":3,"file":"release-plane.cjs","names":["isPlainObject","ensureObjectContainers","SHARED_DEP_SPECS","resolveSlotVersion"],"sources":["../../../src/services/resolution/release-plane.ts"],"sourcesContent":["import { SHARED_DEP_SPECS } from \"every-plugin/shared-deps-spec\";\nimport { type ConfigSlot, ensureObjectContainers, walkConfigSlots } from \"../config-slots\";\nimport { resolveSlotVersion } from \"../deploy/version-manifest\";\nimport type { ReleaseSlotRecord } from \"../platform/manifest\";\nimport { isPlainObject } from \"./merge\";\n\n/**\n * The release plane (ADR 0034 amendment — release-scoped extends\n * inheritance): a platform-pinned child composes the slots the pinned\n * release recorded instead of drifting with the parent's live config\n * pointer. One implementation, two adapters: the deploy train stamps the\n * child's publish payload before it publishes, and every consumer boot\n * stamps the extends-merged shape before composition — so production, local\n * dev, and type generation all resolve the same release bytes.\n *\n * Without the plane, a root publish (rc.N+1) moves the inherited auth pin\n * while a platform-pinned child's own ui stays built against rc.N — the\n * host's shared-singleton guard (#546) refuses the composition at boot.\n */\n\n/** Keys that name where a slot's code comes from — owning the slot's bytes. */\nconst SLOT_SOURCE_KEYS = [\"production\", \"development\", \"pin\", \"extends\", \"proxy\"] as const;\n\n/**\n * A slot is authored when its entry names a code source (or opts out with\n * `ui: false` / owns it via a bare plugin ref). An entry that only configures\n * an inherited slot — `variables`, `secrets`, `routes`, … — does not own its\n * bytes, so the release still pins them.\n */\nfunction declaresSlotSource(entry: ConfigSlot[\"entry\"]): boolean {\n if (entry === false || typeof entry === \"string\") return true;\n return SLOT_SOURCE_KEYS.some((key) => entry[key] !== undefined);\n}\n\n/** Authored slot paths: every slot whose code source the leaf itself declares. */\nexport function authoredSlotPaths(config: Record<string, unknown>): Set<string> {\n const paths = new Set<string>();\n walkConfigSlots(config, ({ slot, entry, isPluginRef }) => {\n if (declaresSlotSource(entry)) paths.add(slot);\n // KEEP: a bare-string plugin ref (`extends: \"bos://…\"`) owns its whole\n // surface — composed resolution derives its ui from the extended config,\n // so the ownership set must include it independently of the `ui` key.\n if (isPluginRef) paths.add(`${slot}.ui`);\n });\n return paths;\n}\n\n/** Stamped and omitted slot paths (diagnostic), in stamp order. */\nexport interface StampOutcome {\n stamped: string[];\n omitted: string[];\n}\n\n/**\n * `payload` — the deploy train's publish-payload seam: `app.*` slots only,\n * created when absent (a payload's authored config may know nothing about an\n * inherited app slot; the stamp is its introduction). Creating plugin entries\n * is deliberately refused — a child's declared `plugins` object owns the\n * whole domain in one wholesale replace (`mergeBosConfigWithExtends`), so a\n * stamped plugin ui record alone carries no required `name` and the wholesale\n * replace never fills it (the feedback.nearbuilders.org rc.26 incident).\n *\n * `consumer` — every consumer boot (`bos dev`, host boot, tenant\n * resolution): `app.*` AND `plugins.*` stamp by **merging `{ production, pin\n * }` into the existing inherited entry** — schema-valid by construction (the\n * live entry already carries the slot's full shape). Nothing is created: a\n * path absent from the merged shape is a post-release provisioned slot the\n * pinned plane cannot claim, so it is omitted and reported for the dev\n * audit instead of being silently live-followed (the skew bug class).\n */\nexport type ReleasePlaneMode = \"payload\" | \"consumer\";\n\nexport function stampInheritedReleaseSlots(input: {\n config: Record<string, unknown>;\n authoredConfig: Record<string, unknown>;\n slots: Record<string, ReleaseSlotRecord> | undefined;\n mode: ReleasePlaneMode;\n}): StampOutcome {\n const slots = input.slots ?? {};\n const authored = authoredSlotPaths(input.authoredConfig);\n const stamped: string[] = [];\n const omitted: string[] = [];\n\n for (const [slotPath, record] of Object.entries(slots)) {\n const isAppSlot = /^app\\.[^.]+(\\.ui)?$/.test(slotPath);\n if (input.mode === \"payload\" && !isAppSlot) continue;\n if (authored.has(slotPath)) continue;\n if (input.mode === \"consumer\" && !slotEntryAt(input.config, slotPath)) {\n omitted.push(slotPath);\n continue;\n }\n stampSlotRecord(input.config, slotPath, record);\n stamped.push(slotPath);\n }\n return { stamped, omitted };\n}\n\n/** The entry object at a dotted slot path (2–3 segments), if it exists. */\nfunction slotEntryAt(\n config: Record<string, unknown>,\n slotPath: string,\n): Record<string, unknown> | undefined {\n const segments = slotPath.split(\".\");\n if ((segments.length !== 2 && segments.length !== 3) || !segments[0] || !segments[1]) {\n return undefined;\n }\n let node: unknown = config;\n for (const segment of segments.slice(0, -1)) {\n if (!isPlainObject(node)) return undefined;\n node = node[segment];\n }\n if (!isPlainObject(node)) return undefined;\n const leaf = segments.at(-1);\n if (leaf === undefined) return undefined;\n const entry = node[leaf];\n return isPlainObject(entry) ? entry : undefined;\n}\n\n/** Write `{ production, pin }` for one dotted slot path (2–3 segments),\n * merging into an existing entry rather than replacing it. */\nfunction stampSlotRecord(\n config: Record<string, unknown>,\n slotPath: string,\n record: ReleaseSlotRecord,\n): void {\n const segments = slotPath.split(\".\");\n if ((segments.length !== 2 && segments.length !== 3) || !segments[0] || !segments[1]) return;\n const leaf = segments.at(-1);\n const container = ensureObjectContainers(config, segments.slice(0, -1));\n if (!leaf) return;\n const existing = container[leaf];\n const entry: Record<string, unknown> = isPlainObject(existing) ? { ...existing } : {};\n entry.production = record.base;\n entry.pin = { manifest: record.pin.manifest, integrity: record.pin.integrity };\n container[leaf] = entry;\n}\n\nexport interface PlaneAuditLine {\n slot: string;\n kind: \"inherited\" | \"owned\" | \"provisioned\" | \"uncomposed\";\n message: string;\n}\n\n/**\n * The publish key an owned slot realigns through: the workspace key the\n * scoped deploy train and `bos plugin publish` understand — `app.<key>` and\n * `app.<key>.ui` publish as the app key (`ui`, `api`, …), `plugins.<id>` and\n * `plugins.<id>.ui` as the plugin id. Slot paths themselves are not keys.\n */\nexport function slotPublishKey(slot: string): string {\n const [domain, key] = slot.split(\".\");\n return domain === \"app\" || domain === \"plugins\" ? (key ?? slot) : slot;\n}\n\n/** The repair instruction for an owned slot diverging on shared singletons:\n * app slots republish through the scoped deploy train (they are not plugins);\n * plugin slots through the single-plugin publish. */\nfunction ownedSlotRepair(slot: string): string {\n const key = slotPublishKey(slot);\n const command = slot.startsWith(\"app.\")\n ? `bos deploy --packages ${key}`\n : `bos plugin publish ${key}`;\n return `run ${command} (built against an older chain)`;\n}\n\n/**\n * The dev-boot chain audit (ADR 0034 + #546's shared-singleton trust):\n * every pinned remote slot's embedded singleton versions are compared\n * against the local workspace catalog — the same `satisfiesConstraint`\n * family the deploy-train compat-window gate speaks, so one mismatch\n * reads the same way in the train and on screen. One line per divergent\n * slot, with the repair named for its ownership: inherited →\n * `bos upgrade` (the pin trails the chain serving the slot); owned →\n * `bos plugin publish <key>` (built against an older chain). Empty out:\n * the whole plane composes coherently.\n *\n * Two plane-shape signals ride beside the version check:\n * - a config slot the pinned manifest does not record is genuinely\n * provisioned upstream after the pin — it composes live, unpinned bytes,\n * and `bos upgrade` claims it;\n * - a manifest slot the merged config does not compose is NOT an upgrade\n * candidate — a child declaring its own `plugins` domain wholesale-\n * replaces the inherited one by design, so that is an uncomposed-slot\n * note, never a \"run bos upgrade\" instruction (an upgrade cannot change\n * the merged shape).\n */\nexport async function auditReleasePlane(input: {\n slots: Array<{ slot: string; base: string; pin: { manifest: string; integrity: string } }>;\n authoredPaths: Set<string>;\n /** The installed version this runtime composes for one singleton name —\n * the probe the boot guard consults too (catalog for framework names,\n * node_modules for the rest). */\n localVersionOf: (name: string) => string | undefined;\n provisionedSlots?: readonly string[];\n /** Every slot path the merged resolved config walks (authored + inherited). */\n configSlotPaths?: readonly string[];\n /** Slot paths the pinned release manifest records. */\n manifestSlotPaths?: readonly string[];\n pinnedReleaseVersion?: string;\n /** test seam — threaded to the version-manifest fetch */\n fetchImpl?: typeof fetch;\n}): Promise<PlaneAuditLine[]> {\n const lines: PlaneAuditLine[] = [];\n const strictness = new Map(SHARED_DEP_SPECS.map((spec) => [spec.name, spec.critical]));\n const manifestSlotPaths = new Set(input.manifestSlotPaths ?? []);\n\n for (const { slot, base, pin } of input.slots) {\n const kind = input.authoredPaths.has(slot) ? \"owned\" : \"inherited\";\n const repair =\n kind === \"owned\"\n ? ownedSlotRepair(slot)\n : \"run bos upgrade (the pin trails the chain serving this slot)\";\n try {\n const resolved = await resolveSlotVersion({\n base,\n pin,\n ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),\n });\n for (const [name, version] of Object.entries(resolved.sharedVersions ?? {})) {\n // Strict singletons must agree exactly (shareConfigFor: critical ⇒\n // strictVersion true); non-strict names range-coexist by design — no line.\n if (!strictness.get(name)) continue;\n const local = input.localVersionOf(name);\n if (local !== undefined && local === version) continue;\n lines.push({\n slot,\n kind,\n message:\n `${slot} was built against ${name}@${version}, this runtime composes ${name}@${local ?? \"unknown\"} — ` +\n `${repair}`,\n });\n break;\n }\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n lines.push({\n slot,\n kind,\n message: `${slot}'s version manifest does not serve at its production URL (${reason}) — ${repair}`,\n });\n }\n }\n\n // Config slots the pinned manifest does not record: genuinely provisioned\n // upstream after the pin — they compose live, unpinned bytes, so the repair\n // is a real re-pin. Inherited only (authored slots are the leaf's own\n // decision, and pinned ones are already claimed by the manifest).\n const pinnedSlots = new Set(input.slots.map((ref) => ref.slot));\n for (const slot of input.configSlotPaths ?? []) {\n if (!manifestSlotPaths.has(slot) && !pinnedSlots.has(slot) && !input.authoredPaths.has(slot)) {\n lines.push({\n slot,\n kind: \"provisioned\",\n message:\n `slot \"${slot}\" is served by the extends chain but missing from the ` +\n `${input.pinnedReleaseVersion ?? \"pinned\"} release plane — it composes live, unpinned bytes; ` +\n `run bos upgrade to re-pin and claim it`,\n });\n }\n }\n\n for (const slot of input.provisionedSlots ?? []) {\n lines.push({\n slot,\n kind: \"uncomposed\",\n message:\n `slot \"${slot}\" is recorded in the ${input.pinnedReleaseVersion ?? \"pinned\"} release ` +\n `but not composed by this config — declare it in your config's app/plugins domain to compose it`,\n });\n }\n return lines;\n}\n"],"mappings":"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"}