/* Zod schema for Biome definition files (packages/werkstatt-site/src/domain/ontology/biomes/*.yaml). A Biome is a full visual-DNA contract: family linkage, provenance, closed design axes, palette, typography, spacing, motion, geometry, and authoring constraints. Applied via html[data-biome=""] in the cascade layer stack. Do not parse YAML here; the caller is responsible for parsing. Do not define CSS; the codegen command (biome-css.ts) produces CSS from parsed Biome objects. Do not reference business or app logic. RFC-0371: Add optional fonts field for biome-driven Fontsource CSS imports. RFC-1298: biomeProvenanceSchema becomes a union — legacy gen/site shape or intake shape (client, intakeId, selectedConcept, intakeSha256). 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-1298: REVIEW-CODE-2026-10-16-01 findings — kebab flag guards, safeParse, reads declaration, family-append ordering, FONTS-SCRIPT-02 unverifiable-stack warning RFC-0071 */ import { z } from "zod"; const semver = z .string() .regex(/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/i, "version must be valid semver"); const kebabId = z .string() .regex(/^[a-z][a-z0-9-]*$/, "id must be kebab-case (lowercase letters, digits, hyphens)"); const cssValue = z.string().min(1); const sourceArtifact = z .string() .regex(/^\d{2}-[a-z0-9][a-z0-9.-]*(?:\.[a-z0-9]+)?(?:\.[a-z0-9]+)?$/i); // RFC-1298: provenance is a union — the legacy gen/site shape (unchanged) or // the intake shape produced by biome.from-intake (werkstatt-onboarding D13). const legacyBiomeProvenanceSchema = z .object({ client: kebabId, selectedConcept: z.string().regex(/^concept-\d+$/), selectedDirection: z.number().int().positive(), sourceFiles: z.array(sourceArtifact).min(1), }) .strict(); const intakeBiomeProvenanceSchema = z .object({ client: kebabId, intakeId: z .string() // Mirrors the canonical intakeId regex on intakeEnvelopeSchema // (onboarding/intake.ts) — duplicated, not imported, because schemas/ // is a leaf and must not depend upward. Update both on format change. .regex(/^int_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), selectedConcept: z.string().min(1), intakeSha256: z.string().regex(/^sha256:[0-9a-f]{64}$/), }) .strict(); export const biomeProvenanceSchema = z.union([ legacyBiomeProvenanceSchema, intakeBiomeProvenanceSchema, ]); export const biomeAxesSchema = z .object({ warmth: z.enum(["cool", "neutral", "warm"]), contrast: z.enum(["low", "medium", "high"]), density: z.enum(["dense", "comfortable", "airy"]), typographySharpness: z.enum(["soft", "balanced", "sharp"]), diagramPresence: z.enum(["absent", "minimal", "supportive", "central"]), photoStance: z.enum(["none", "founder-only", "documentary", "editorial"]), motionStance: z.enum(["static", "restrained", "expressive"]), textContrast: z.enum(["aa", "aaa"]), cornerRadius: cssValue, borderWeight: cssValue, }) .strict(); export const biomePaletteSchema = z .object({ brand: cssValue, brandHover: cssValue, brandContrast: cssValue, accent: cssValue, surface: cssValue, surfaceMuted: cssValue, ink: cssValue, inkSoft: cssValue, inkMuted: cssValue, divider: cssValue, success: cssValue, warning: cssValue, danger: cssValue, info: cssValue, }) .strict(); export const biomeComponentsSchema = z .object({ heroOverlay: cssValue.optional(), heroText: cssValue.optional(), surfaceGlass: cssValue.optional(), sectionAltBase: cssValue.optional(), sectionAltText: cssValue.optional(), sectionAltTextSoft: cssValue.optional(), sectionAltLink: cssValue.optional(), sectionAltLinkVisited: cssValue.optional(), sectionAltLinkHover: cssValue.optional(), sectionAltAccent: cssValue.optional(), statsBg: cssValue.optional(), footerBg: cssValue.optional(), footerText: cssValue.optional(), footerTextMuted: cssValue.optional(), cardBg: cssValue.optional(), cardBorder: cssValue.optional(), cta: cssValue.optional(), ctaHover: cssValue.optional(), ctaText: cssValue.optional(), surfaceAlt: cssValue.optional(), textSoftOnDark: cssValue.optional(), textInverse84: cssValue.optional(), }) .strict() .optional(); export const biomeTypographySchema = z .object({ headingFamily: cssValue, bodyFamily: cssValue, monoFamily: cssValue, scaleRatio: z.number().positive(), baseSize: cssValue, lineHeightBody: z.number().positive(), lineHeightHeading: z.number().positive(), measureBody: cssValue, measureHeading: cssValue, numericFeatures: cssValue, }) .strict(); export const biomeSpacingSchema = z .object({ base: cssValue, sectionPaddingY: cssValue, containerMaxWidth: cssValue, containerNarrow: cssValue.optional(), gutter: cssValue, }) .strict(); export const biomeMotionSchema = z .object({ durationFast: cssValue, durationMedium: cssValue, durationSlow: cssValue, easing: cssValue, reduceMotionRespect: z.boolean(), }) .strict(); export const biomeGeometrySchema = z .object({ diagramLineWeight: cssValue.optional(), diagramAccentColor: cssValue.optional(), decorativeAllowed: z.boolean(), }) .strict(); // RFC-0098: shadows are visual-DNA — a "soft warm material" biome reads // different elevation cues than a "trustworthy nonprofit" biome. Keys map // 1:1 to the historical --ds-shadow-* studio defaults; biome wins when set. export const biomeShadowsSchema = z .object({ sm: cssValue.optional(), md: cssValue.optional(), lg: cssValue.optional(), xl: cssValue.optional(), glass: cssValue.optional(), glow: cssValue.optional(), header: cssValue.optional(), appeal: cssValue.optional(), }) .strict() .optional(); // RFC-0098: gradients carry brand DNA (the previous studio defaults baked // nicaragua-projekt brand RGB into --ds-gradient-primary / accent which is // app-specific). Biome-scoped gradients keep the brand color where the brand // is declared. export const biomeGradientsSchema = z .object({ accent: cssValue.optional(), primary: cssValue.optional(), vignetteDark: cssValue.optional(), }) .strict() .optional(); export const biomeConstraintsSchema = z .object({ forbidStockPhotoTags: z.array(z.string().min(1)).default([]), forbidPhrases: z.array(z.string().min(1)).default([]), enforceTabularNumeralsIn: z.array(z.string().min(1)).default([]), }) .strict(); // RFC-0114: site-background layer schema mirrored from // @warpgogol/werkstatt-shared/schemas/site-background. Defined locally to avoid a // cross-package import dependency (ontology must not depend on share). const biomeSiteBackgroundLayerSchema = z.discriminatedUnion("kind", [ z .object({ kind: z.literal("color"), color: cssValue.optional(), }) .strict(), z .object({ kind: z.literal("image"), imageName: z.string().min(1), fit: z.enum(["cover", "tile", "stretch-width", "stretch-height"]).optional(), quality: z.enum(["low", "mid", "high", "max"]).optional(), loading: z.enum(["eager", "lazy"]).optional(), tint: z .object({ color: cssValue.optional(), opacity: z.number().min(0).max(1).optional(), }) .strict() .optional(), parallax: z .object({ speed: z.number().min(0).max(2).optional(), respectReducedMotion: z.boolean().optional(), }) .strict() .optional(), }) .strict(), z .object({ kind: z.literal("gradient"), direction: z.enum(["vertical", "horizontal", "radial"]), stops: z .array( z .object({ at: z.number().min(0).max(1), color: cssValue, opacity: z.number().min(0).max(1).optional(), }) .strict(), ) .min(2), }) .strict(), ]); export const biomeSiteBackgroundSchema = z .object({ layers: z.array(biomeSiteBackgroundLayerSchema).min(1), }) .strict(); // RFC-0371: biome-driven Fontsource CSS imports. Each entry declares a // @fontsource/* package and the weights (and optional italic weights) the // biome needs. fonts.imports.generate reads this to emit @import statements. export const biomeFontEntrySchema = z .object({ family: z.string().min(1), package: z.string().regex(/^@fontsource\//, "package must start with @fontsource/"), weights: z.array(z.number().int().positive()).min(1), italicWeights: z.array(z.number().int().positive()).optional(), }) .strict(); export const biomeFontsSchema = z.array(biomeFontEntrySchema).optional(); export const biomeSchema = z .object({ id: kebabId, version: semver, displayName: z.string().min(1), family: kebabId, provenance: biomeProvenanceSchema.optional(), axes: biomeAxesSchema, palette: biomePaletteSchema, components: biomeComponentsSchema, typography: biomeTypographySchema, spacing: biomeSpacingSchema, motion: biomeMotionSchema, geometry: biomeGeometrySchema, // RFC-0098: optional visual-DNA blocks promoted from studio tokens. shadows: biomeShadowsSchema, gradients: biomeGradientsSchema, // RFC-0114: optional site-background defaults. When absent, the deriver // (biome.site-background.derive) produces a sensible default from axes; // when present, it is used verbatim and onboarding.scaffold seeds the // new app's system.md shell.background from this block. siteBackground: biomeSiteBackgroundSchema.optional(), // RFC-0371: optional Fontsource CSS import declarations. When absent, // the biome relies on system font fallbacks only. fonts: biomeFontsSchema, constraints: biomeConstraintsSchema, }) .strict(); export type BiomeProvenance = z.infer; export type BiomeAxes = z.infer; export type BiomePalette = z.infer; export type BiomeComponents = z.infer; export type BiomeTypography = z.infer; export type BiomeSpacing = z.infer; export type BiomeMotion = z.infer; export type BiomeGeometry = z.infer; export type BiomeShadows = z.infer; export type BiomeGradients = z.infer; export type BiomeConstraints = z.infer; export type BiomeSiteBackground = z.infer; export type BiomeFontEntry = z.infer; export type BiomeFontsConfig = z.infer; export type Biome = z.infer;