/*
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"),
);
};
}