/* Manifest resolver: reads section/component manifest YAML from packages/ui/src and returns the composed propsSchema for a given cosmicName. Concentrates all filesystem I/O, YAML parsing, and fragment composition behind one interface. Do not define Zod schemas here — pure resolution and I/O. Do not validate cross-system.yaml references — that is page.block.validate's job. RFC-0484: suppress console.debug for directories without manifests — group directories are expected to have none. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1204: effectsParity flag on section manifests + resolver lookup RFC-1261 */ import { join } from "node:path"; import { parse as parseYaml } from "yaml"; import { sectionManifestSchema } from "../manifest.ts"; import { composeManifestPropsSchema } from "../shared-section-props/index.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; /** * Reads the manifest YAML for the section assigned to `planetName` and returns * its `propsSchema` (JSON Schema object) along with the section's `semanticId` * and current `version`. * * Returns null when no manifest is found for the given planet name, or when * the manifest has no `propsSchema` declared. * * @param cosmicName — PlanetCatalog or passport-reserved MoonCatalog name * @param packagesUiRoot — absolute path to packages/ui/src (for locating manifests) * * Usage (in page.block.validate): * ```ts * const schema = await getSectionPropsSchema("Europa", "/workspace/packages/ui/src"); * ``` */ export async function getSectionPropsSchema( cosmicName: string, packagesUiRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise<{ propsSchema: Record; semanticId: string; version: string; } | null> { for (const manifest of await collectSectionManifests(packagesUiRoot, io)) { if (manifest.cosmicName !== cosmicName) continue; const compose = manifest.propsSchemaCompose; const local = manifest.propsSchema; // RFC-0101 + RFC-0102 + RFC-0103: when the manifest declares // propsSchemaCompose, merge the referenced shared fragments before // validation. A manifest without either field disables strict prop // validation (existing behaviour preserved). if (!compose && !local) return null; const composed = composeManifestPropsSchema({ compose, local: local as Record | undefined, }); return { propsSchema: composed, semanticId: manifest.semanticId, version: manifest.version, }; } return null; } /** * RFC-1261: resolves the `localePolicy` map of the section manifest assigned * to `cosmicName` (for content-ops mirror field-path policy). Returns * undefined when no manifest matches or it declares no policy. */ export async function getSectionLocalePolicy( cosmicName: string, packagesUiRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise | undefined> { for (const manifest of await collectSectionManifests(packagesUiRoot, io)) { if (manifest.cosmicName !== cosmicName) continue; return manifest.localePolicy; } return undefined; } /** * RFC-1204: map of `cosmicName → effectsParity` for every section manifest * that opted into sibling effect-signature parity (only `"strict"` exists * today, so the map value is the literal). Sections without the flag are * absent from the map — consumers treat absence as "no cross-page parity". * * Not a getSectionPropsSchema extension: that function only resolves * manifests that declare propsSchema/propsSchemaCompose and returns a * per-cosmicName record, whereas parity consumers need the full opt-in set * in one pass. */ export async function resolveEffectsParityFlags( packagesUiRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise> { const flags = new Map(); for (const manifest of await collectSectionManifests(packagesUiRoot, io)) { if (manifest.effectsParity === "strict") { flags.set(manifest.cosmicName, "strict"); } } return flags; } type ParsedSectionManifest = ReturnType; /** * Reads every `*-section.manifest.yaml` / `*.manifest.yaml` under * `packagesUiRoot/{sections,components}` and returns the entries that parse * against sectionManifestSchema. Component-layer manifests never satisfy the * `layer: "section"` literal and are filtered out — scanning both trees is * preserved for the historical passport-reserved-moon lookup path. */ async function collectSectionManifests( packagesUiRoot: string, io: WorkspaceIO, ): Promise { // Scan section AND component manifests — the five PASSPORT-RESERVED moons // resolve to component manifests (passport components are invoked as // page-blocks on cosmic/passport and cosmic/star-map routes — RFC-0028). const candidates: Array<{ dir: string; layer: "sections" | "components" }> = []; for (const layer of ["sections", "components"] as const) { const layerDir = join(packagesUiRoot, layer); try { const entries = await io.readdir(layerDir); for (const e of entries) candidates.push({ dir: e.name, layer }); } catch (err) { console.debug( `[manifest-resolver] ${layerDir} not readable — skipping (${err instanceof Error ? err.message : String(err)})`, ); } } const manifests: ParsedSectionManifest[] = []; for (const { dir, layer } of candidates) { const layerSuffix = layer === "sections" ? "-section" : "-component"; const layerDir = join(packagesUiRoot, layer); const manifestPath = join(layerDir, dir, `${dir}${layerSuffix}.manifest.yaml`); const manifestPathAlt = join(layerDir, dir, `${dir}.manifest.yaml`); let raw: string | null = null; try { raw = await io.readFile(manifestPath); } catch { try { raw = await io.readFile(manifestPathAlt); } catch { // No manifest file found for this directory — expected for group // directories (effects, section-body, seo) that contain sub-components // but are not components themselves. Skip silently (RFC-0484). continue; } } if (!raw) continue; let parsed: unknown; try { parsed = parseYaml(raw); } catch (err) { console.debug( `[manifest-resolver] YAML parse failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`, ); continue; } const result = sectionManifestSchema.safeParse(parsed); if (result.success) manifests.push(result.data); } return manifests; }