/*
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;