/* RFC-0262: the dev-only fail-fast prop validator wired into buildPage's `validateProps` hook. Resolves each block's manifest propsSchema at runtime (packages/ui/src, workspace-relative, resolved by walking up from cwd to pnpm-workspace.yaml — this package never ships a hand-rolled path guess) and throws PAGE-PROPS-01 on a genuine shape violation. Infrastructure failures (workspace root not found, manifest unreadable, schema unresolved) are swallowed with a console.warn — this is a dev convenience feature and must never crash rendering for a reason unrelated to the author's actual props. Do not replace page.block.validate — that remains the authoritative build.check gate. Do not run in production — callers must gate construction behind a dev check. RFC-0262: initial implementation. 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. */ import { stat } from "node:fs/promises"; import { dirname, join } from "node:path"; import { getSectionPropsSchema } from "@warpgogol/werkstatt-shared/ontology/schemas/manifest-resolver"; import Ajv, { type ValidateFunction } from "ajv"; const ajv = new Ajv({ allErrors: true, strict: false }); /** * Block-level props consumed by blocks-renderer.astro (NOT by section components). * These are valid on every block regardless of section archetype, so the * validator strips them before per-section propsSchema validation. * * Must stay in sync with packages/os/site-kernel-checks/src/page-block.ts * UNIVERSAL_BLOCK_PROPS and packages/werkstatt-site/src/domain/ui/blocks-renderer.astro. */ const UNIVERSAL_BLOCK_PROPS: ReadonlySet = new Set(["hideSectionNumber", "anchorId"]); let packagesUiRootPromise: Promise | undefined; async function resolvePackagesUiRoot(): Promise { if (!packagesUiRootPromise) { packagesUiRootPromise = (async () => { let dir = process.cwd(); for (let depth = 0; depth < 12; depth += 1) { try { await stat(join(dir, "pnpm-workspace.yaml")); return join(dir, "packages", "werkstatt-site", "src", "domain", "ui"); } catch { const parent = dirname(dir); if (parent === dir) return null; dir = parent; } } return null; })(); } return packagesUiRootPromise; } const schemaCache = new Map | null>(); const validateCache = new Map(); async function resolveSchema(planetName: string): Promise | null> { if (schemaCache.has(planetName)) return schemaCache.get(planetName)!; try { const packagesUiRoot = await resolvePackagesUiRoot(); if (!packagesUiRoot) { schemaCache.set(planetName, null); return null; } const resolved = await getSectionPropsSchema(planetName, packagesUiRoot); const schema = resolved?.propsSchema ?? null; schemaCache.set(planetName, schema); return schema; } catch (error) { console.warn( `[dev-props-validator] Could not resolve propsSchema for "${planetName}": ${String(error)}`, ); schemaCache.set(planetName, null); return null; } } function resolveValidator(planetName: string, schema: Record): ValidateFunction { const cached = validateCache.get(planetName); if (cached) return cached; const validate = ajv.compile(schema); validateCache.set(planetName, validate); return validate; } /** * RFC-0262: build the `validateProps` callback for `BuildPageOptions`. Callers * MUST only pass this in dev contexts (`import.meta.env.DEV` / * `process.env.NODE_ENV !== "production"`) — it is never active during * `astro build`. */ export function createDevPropsValidator(): ( planetName: string, props: Record, blockId: string | null, ) => Promise { return async (planetName, props, blockId) => { const schema = await resolveSchema(planetName); if (!schema) return; // No pinned schema (or resolution failed) — nothing to check. // Strip UNIVERSAL_BLOCK_PROPS before validation — these are consumed by // blocks-renderer.astro (not by the section component) and are valid on // every block regardless of section archetype. Must stay in sync with // packages/os/site-kernel-checks/src/page-block.ts. const strippedProps: Record = {}; for (const [k, v] of Object.entries(props)) { if (!UNIVERSAL_BLOCK_PROPS.has(k)) strippedProps[k] = v; } const validate = resolveValidator(planetName, schema); if (validate(strippedProps)) return; const errors = (validate.errors ?? []).map( (e) => `props${e.instancePath}: ${e.message ?? "validation failed"}`, ); const blockLabel = blockId ? `block "${blockId}"` : "a shell block"; throw new Error( `[PAGE-PROPS-01] ${blockLabel} (${planetName}) violates its pinned propsSchema:\n` + errors.map((e) => ` - ${e}`).join("\n"), ); }; }