/*
Zod schema for manifest.yaml — the colocated per-component/section/page metadata
file that forms the fifth leg of the Mirror Quintet (DNA-17, RFC-0023).
Exports a discriminated union on `layer` so TypeScript narrows the `role` and
`cosmicName` fields to the correct closed enum automatically.
- Do not parse YAML here — the caller (manifest.contract.validate command
and uni.registry.build) is responsible for reading the file and passing
the parsed object to manifestSchema.parse().
- Do not hard-code file paths. The schema is path-agnostic.
- Do not add business-shaped fields (company, services, etc.) — those
belong in @warpgogol/werkstatt-site/pbp schemas (RFC-0024, RFC-0471).
- 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 { z } from "zod";
import { layerSchema, componentRoleSchema, industrySchema } from "./enums.ts";
import { starNameSchema } from "./cosmic/star-catalog.ts";
import { planetNameSchema } from "./cosmic/planet-catalog.ts";
import { moonNameSchema } from "./cosmic/moon-catalog.ts";
// ---------------------------------------------------------------------------
// Intent (open vocabulary)
// ---------------------------------------------------------------------------
/**
* Well-known intent strings for manifest.yaml `intent[]` fields.
*
* Intent is an OPEN vocabulary (typed as string). This list is advisory:
* it documents the canonical phrases in active use and enables autocomplete,
* but it is NOT enforced as a closed enum (DNA-19). New intents may be added
* locally without an RFC.
*
* Intents describe *user goals* this component serves, not its technical role.
*/
export const KNOWN_INTENTS = [
// Conversion / acquisition
"convert-visitor",
"capture-lead",
"drive-contact",
"drive-booking",
"drive-donation",
// Trust / credibility
"build-trust",
"show-social-proof",
"demonstrate-expertise",
"signal-transparency",
// Identity / differentiation
"establish-identity",
"clarify-positioning",
"explain-approach",
"articulate-problem",
"highlight-impact",
// Navigation / discovery
"guide-navigation",
"orient-visitor",
"surface-content",
// Team / human connection
"introduce-team",
"humanise-brand",
// Legal / compliance
"fulfill-legal-requirement",
"display-compliance",
] as const;
export type KnownIntent = (typeof KNOWN_INTENTS)[number];
// ---------------------------------------------------------------------------
// Semver helper
// ---------------------------------------------------------------------------
const semverPattern = /^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?(?:\+[a-z0-9.-]+)?$/i;
const semverSchema = z
.string()
.regex(semverPattern, "version must be a valid semver string (e.g. 1.0.0)");
// ---------------------------------------------------------------------------
// Kebab-case helper
// ---------------------------------------------------------------------------
const kebabPattern = /^[a-z][a-z0-9-]*$/;
const kebabSchema = z
.string()
.regex(kebabPattern, "Must be kebab-case (lowercase letters, digits, hyphens)");
const archetypeIdSchema = z
.string()
.regex(
/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/,
"archetype must be kebab-case segments separated by dots",
);
// ---------------------------------------------------------------------------
// Shared base schema
// ---------------------------------------------------------------------------
/**
* Fields common to all three manifest layers (page | section | component).
* `layer` and `role` are declared per-layer to enable discriminated union narrowing.
*/
const manifestBaseSchema = z.object({
/**
* Unique stable identifier for this UI atom in the Uni registry.
* Format: [-]-
* Examples: "hero-section", "footer-component", "home-page"
*/
id: kebabSchema,
/**
* Display/marketing name for the Uni UI Ontology.
* At MVP this equals `semanticId` (RFC-0023 nonGoal). A future naming RFC
* may assign distinct values (e.g. "Beacon" for a hero section variant).
*/
uniName: z.string().min(1),
/**
* The kebab-case semantic identifier matching the component's filename stem.
* Examples: "hero", "final-cta", "footer", "person-profile"
*/
semanticId: kebabSchema,
/**
* RFC-0072 stable archetype identifier used to bind manifests to the central
* archetype catalog in packages/werkstatt-site/src/domain/ontology/archetypes/sections/.
* Supports grouped ids such as "shell.header".
*/
archetype: archetypeIdSchema,
/**
* SemVer version string. Patch = backwards-compatible content change.
* Minor = new optional props. Major = breaking prop/schema change.
*/
version: semverSchema,
/**
* Open-vocabulary list of user-goal phrases this component serves.
* Use values from KNOWN_INTENTS when possible; new values are allowed
* without an RFC (DNA-19).
*/
intent: z.array(z.string()).min(1, "At least one intent is required"),
/**
* Industry verticals for which this component is a natural fit.
* Empty array = universally applicable.
* Uses the closed Industry enum (DNA-19).
*/
industryFit: z.array(industrySchema),
/**
* Key of the Zod content schema this component consumes.
* Must match an export of @warpgogol/werkstatt-shared or the app-local content/schemas/.
* Set to null for structural components that consume no content schema
* (e.g. breadcrumbs, lang-switcher).
*/
contentSchemaKey: z.string().nullable(),
/**
* When true, this component can be placed outside the standard
* page → section → component hierarchy (DNA-8).
* Example: a standalone injected at the layout level.
* Default: false.
*/
standalone: z.boolean().optional().default(false),
});
// ---------------------------------------------------------------------------
// Per-layer manifest schemas (discriminated union)
// ---------------------------------------------------------------------------
/**
* Manifest for a page (layer = "page").
* Pages may carry a descriptive role string but it is not validated
* against a closed enum — pages are too varied to enumerate centrally.
* cosmicName must be a value from the StarCatalog (DNA-23, RFC-0025).
*/
export const pageManifestSchema = manifestBaseSchema.extend({
layer: z.literal(layerSchema.enum.page),
/**
* Optional page-type descriptor. Free-form at MVP.
* Examples: "landing", "legal", "detail"
*/
role: z.string().optional(),
/**
* The IAU star name assigned to this page in the cosmic overlay (DNA-23).
* Must be a value from StarCatalog. Validated at deploy time by
* cosmic.catalog.validate (unique per workspace).
*/
cosmicName: starNameSchema,
});
/**
* Manifest for a section (layer = "section").
* Role is an open kebab-case string; cross-file validation against the
* archetype-derived sectionRoles[] set is performed by
* archetype.registry.validate (RFC-0084). The closed SemanticRoleValues
* enum was retired because the archetype catalog is the actual source of
* truth for permitted section semantic roles.
* cosmicName must be a value from the PlanetCatalog (DNA-23, RFC-0025).
*/
export const sectionManifestSchema = manifestBaseSchema.extend({
layer: z.literal(layerSchema.enum.section),
role: z.string().min(1),
/**
* The IAU moon/dwarf-planet name assigned to this section in the cosmic overlay (DNA-23).
* Must be a value from PlanetCatalog. Validated at deploy time by
* cosmic.catalog.validate (unique per workspace).
*/
cosmicName: planetNameSchema,
/**
* JSON Schema (draft-07 compatible) describing the props this section accepts.
* Used by buildPage / page.block.validate to validate block props at build time (RFC-0026).
* additionalProperties: false is enforced by page.block.validate — extra keys fail the build.
* Omitting this field disables strict prop validation for this section (MVP: not all
* sections have propsSchema yet; they will be added progressively in Wave 1 of RFC-0026).
*/
propsSchema: z.record(z.string(), z.unknown()).optional(),
/**
* RFC-0101 + RFC-0102 + RFC-0103: list of shared fragment ids whose JSON
* Schema properties are merged into `propsSchema` before validation runs.
* Eliminates the 60–100 lines of duplicated visual/header/body schema that
* every section manifest used to carry. Fragments are merged in declared
* order; the manifest's local `propsSchema` (when present) wins over any
* composed property. See packages/werkstatt-site/src/domain/ontology/src/shared-section-props/index.ts
* for the catalog. Unknown ids fail manifest.contract.validate.
*/
propsSchemaCompose: z
.array(
z
.string()
.regex(/^[a-z][a-z0-9-]*(@\d+)?$/, "must match `` or `@` (RFC-0119)"),
)
.optional(),
/**
* RFC-0149: server API routes this section requires. When a site uses this
* section, `api.routes.generate` emits a thin GENERATED Astro APIRoute at
* `src/pages/api/.ts` per entry (`export const prerender = false` +
* a re-export of the section handler), and projects the entry's `secrets`
* into the app's astro:env schema. Handler logic lives once in the section
* package; sites carry only the re-export. Omit the field for sections with
* no endpoint.
*/
api: z
.array(
z.object({
/** Route stem under src/pages/api/. Becomes /api/. Kebab-case. */
route: kebabSchema,
/**
* Bare-import specifier of the module exporting the Astro APIRoute
* handlers, named for the HTTP method (`POST`, `GET`, …). The generated
* route re-exports the handlers named for `methods`.
*/
handler: z.string().min(1),
/**
* HTTP methods this endpoint serves. Each maps to an Astro APIRoute
* export named for the uppercased method (e.g. `POST`) that the
* generated route re-exports by name. Defaults to ["POST"] when omitted.
*/
methods: z
.array(z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]))
.nonempty()
.optional(),
/**
* Runtime secret env keys this endpoint reads via `astro:env/server`.
* `api.routes.generate` projects the union of all used sections' secrets
* into the app's generated astro:env schema (declared optional so a
* missing secret degrades to the handler's error path). Kebab/SCREAMING
* naming is the section's choice; keys must be valid env identifiers.
*/
secrets: z
.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/, "must be a SCREAMING_SNAKE_CASE env key"))
.optional(),
}),
)
.optional(),
/**
* RFC-1204: opt-in sibling parity for authored effect signatures. When
* `strict`, effects.contract.validate emits EFF-PARITY-02 for every page
* instance of this section type whose
* `(effectsContainerPath|target|kind)` signature diverges from the
* cross-page majority. Omit for section types whose effect intent
* legitimately differs between pages (e.g. a page-specific decorative
* variant). If present, only the literal `strict` is accepted.
*/
effectsParity: z.enum(["strict"]).optional(),
/**
* RFC-1261: per-props-path locale policy for mirror ops (content.*).
* Maps a props path (e.g. `props.header.heading` or `header`) to its mirror
* policy: `structural` (identical across locales), `localized` (per-locale
* slots required), `locale-locked` (provider-verbatim, never localized).
* Undeclared paths resolve `structural` — the fail-safe under DNA-11.
*/
localePolicy: z
.record(z.string(), z.enum(["structural", "localized", "locale-locked"]))
.optional(),
});
/**
* Manifest for a component (layer = "component").
* Role is narrowed to the closed ComponentRole enum (DNA-19).
* cosmicName must be a value from the MoonCatalog (DNA-23, RFC-0025).
*/
export const componentManifestSchema = manifestBaseSchema.extend({
layer: z.literal(layerSchema.enum.component),
role: componentRoleSchema,
/**
* The IAU small-moon name assigned to this component in the cosmic overlay (DNA-23).
* Must be a value from MoonCatalog. Validated at deploy time by
* cosmic.catalog.validate (unique per workspace).
*
* Passport-reserved names (Methone, Bianca, Klarissa, Adrastea, Despina) are
* EXCLUSIVELY assigned to the five passport components in
* packages/werkstatt-site/src/domain/ui/components/{passport-header,passport-provenance,
* passport-score-grid,passport-star-map,pulsar} (RFC-0028, DNA-31).
* Do not use these five names for any other component manifest.
*/
cosmicName: moonNameSchema,
/**
* RFC-1023: JSON Schema (draft-07 compatible) describing the shape of
* resolved content returned by getResolvedComponentContent. This is the
* flat, merged content (after pageOverride merge), NOT the prose .md
* frontmatter schema. Used by props.types.generate to emit
* XxxComponentContent interfaces. Required for content-driven components
* (those that call getResolvedComponentContent). Optional for structural
* components that consume no content.
*/
contentSchema: z.record(z.string(), z.unknown()).optional(),
/**
* RFC-1023: JSON Schema (draft-07 compatible) describing the shape of
* Astro.props accepted by the component — caller-supplied props such as
* lang, defaultLang, pageOverride, etc. Used by props.types.generate to
* emit XxxComponentProps interfaces. Distinct from contentSchema: propsSchema
* describes what the caller passes, contentSchema describes what the
* component resolves from content files.
*/
propsSchema: z.record(z.string(), z.unknown()).optional(),
});
/**
* Discriminated union of all three layer-specific manifest schemas.
* This is the authoritative runtime validator for any parsed manifest.yaml.
*
* Usage:
* import { manifestSchema } from "@warpgogol/werkstatt-shared/ontology/manifest";
* const manifest = manifestSchema.parse(parsedYaml);
* // manifest.layer narrows to "page" | "section" | "component"
* // manifest.role narrows accordingly
*/
// @ai-invariant: manifestSchema is the authoritative runtime validator for every
// manifest.yaml in packages/ui. It is a discriminated union on `layer` — adding
// a new layer requires extending the union. cosmicName must be drawn from the
// layer-appropriate closed catalog (StarCatalog/PlanetCatalog/MoonCatalog, DNA-23).
// Role values are closed enums per layer — do not invent ad-hoc roles.
export const manifestSchema = z.discriminatedUnion("layer", [
pageManifestSchema,
sectionManifestSchema,
componentManifestSchema,
]);
// ---------------------------------------------------------------------------
// Exported TypeScript types
// ---------------------------------------------------------------------------
export type PageManifest = z.infer;
export type SectionManifest = z.infer;
export type ComponentManifest = z.infer;
/**
* Union of all three manifest variants.
* Use this when writing code that handles manifests of any layer.
*/
export type Manifest = z.infer;