import { z } from "zod"; import { portableRepoPathError } from "./config-paths.js"; import { componentGovernanceRecordSchema, governanceConfigSchema } from "./governance.js"; import { compositionAuthoringEntrySchema } from "./rules/composition-pattern-schema.js"; /** * Zod schemas for runtime validation of fragment definitions */ // Figma property mapping schemas const figmaStringMappingSchema = z.object({ __type: z.literal("figma-string"), figmaProperty: z.string().min(1), }); const figmaBooleanMappingSchema = z.object({ __type: z.literal("figma-boolean"), figmaProperty: z.string().min(1), valueMapping: z.object({ true: z.unknown(), false: z.unknown() }).optional(), }); const figmaEnumMappingSchema = z.object({ __type: z.literal("figma-enum"), figmaProperty: z.string().min(1), valueMapping: z.record(z.unknown()), }); const figmaInstanceMappingSchema = z.object({ __type: z.literal("figma-instance"), figmaProperty: z.string().min(1), }); const figmaChildrenMappingSchema = z.object({ __type: z.literal("figma-children"), layers: z.array(z.string().min(1)), }); const figmaTextContentMappingSchema = z.object({ __type: z.literal("figma-text-content"), layer: z.string().min(1), }); export const figmaPropMappingSchema = z.discriminatedUnion("__type", [ figmaStringMappingSchema, figmaBooleanMappingSchema, figmaEnumMappingSchema, figmaInstanceMappingSchema, figmaChildrenMappingSchema, figmaTextContentMappingSchema, ]); export const fragmentMetaSchema = z.object({ name: z.string().min(1), description: z.string().min(1), category: z.string().min(1), tags: z.array(z.string()).optional(), status: z.enum(["stable", "beta", "deprecated", "experimental"]).optional(), since: z.string().optional(), dependencies: z .array( z.object({ name: z.string().min(1), version: z.string().min(1), reason: z.string().optional(), }) ) .optional(), figma: z.string().url().optional(), figmaProps: z.record(figmaPropMappingSchema).optional(), }); export const fragmentUsageSchema = z.object({ when: z.array(z.string()), whenNot: z.array(z.string()), guidelines: z.array(z.string()).optional(), accessibility: z.array(z.string()).optional(), }); export const propTypeSchema: z.ZodType = z.enum([ "string", "number", "boolean", "enum", "function", "node", "element", "object", "array", "union", "custom", ]); export const propDefinitionSchema = z.object({ type: propTypeSchema, values: z.array(z.string()).readonly().optional(), default: z.unknown().optional(), description: z.string().optional(), required: z.boolean().optional(), constraints: z.array(z.string()).optional(), typeDetails: z.record(z.unknown()).optional(), }); export const relationshipTypeSchema = z.enum([ "alternative", "sibling", "parent", "child", "composition", "complementary", "used-by", ]); export const componentRelationSchema = z.object({ component: z.string().min(1), relationship: relationshipTypeSchema, note: z.string().min(1), }); export const fragmentVariantSchema = z.object({ name: z.string().min(1), description: z.string().min(1), render: z.custom<(...args: unknown[]) => unknown>( (value) => typeof value === "function", "Expected a render function" ), code: z.string().optional(), figma: z.string().url().optional(), }); /** * Schema for banned patterns in codebase */ export const fragmentBanSchema = z.object({ pattern: z.string().min(1), message: z.string().min(1), }); /** * Schema for agent-optimized contract metadata */ export const fragmentContractSchema = z.object({ propsSummary: z.array(z.string()).optional(), a11yRules: z.array(z.string()).optional(), bans: z.array(fragmentBanSchema).optional(), scenarioTags: z.array(z.string()).optional(), performanceBudget: z.number().positive().optional(), compoundChildren: z .record( z.object({ required: z.boolean().optional(), accepts: z.array(z.string()).optional(), description: z.string().optional(), }) ) .optional(), canonicalUsage: z.array(z.string()).optional(), // Region-scoped composition constraints (brief 04 §10). Authors write the // `{ inRegion, rule }` sugar; `lowerCompositionContract` lowers it to the // `CompositionPattern[]` the composition/* rules read from policy. composition: z.array(compositionAuthoringEntrySchema).optional(), }); /** * Schema for provenance tracking of generated fragments */ export const fragmentGeneratedSchema = z.object({ source: z.enum(["storybook", "manual", "ai"]), sourceFile: z.string().optional(), confidence: z.number().min(0).max(1).optional(), timestamp: z.string().datetime().optional(), }); /** * Schema for AI-specific metadata for playground context generation */ export const aiMetadataSchema = z.object({ compositionPattern: z.enum(["compound", "simple", "controlled", "wrapper"]).optional(), subComponents: z.array(z.string()).optional(), requiredChildren: z.array(z.string()).optional(), commonPatterns: z.array(z.string()).optional(), }); export const observedUsagePropSchema = z.object({ name: z.string().min(1), kind: z.enum(["static", "dynamic", "spread", "jsx", "boolean", "null"]), value: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(), }); export const observedComponentUsageSchema = z.object({ file: z.string().min(1), line: z.number().int().positive(), column: z.number().int().nonnegative(), props: z.array(observedUsagePropSchema), parentElement: z.string().optional(), conditional: z.boolean().optional(), }); /** * Schema for block definitions */ export const blockDefinitionSchema = z.object({ name: z.string().min(1), description: z.string().min(1), category: z.string().min(1), components: z.array(z.string().min(1)).min(1), code: z.string().min(1), tags: z.array(z.string()).optional(), }); export const fragmentDefinitionSchema = z.object({ component: z.any(), // Allow any component type (function, class, forwardRef, etc.) meta: fragmentMetaSchema, usage: fragmentUsageSchema, props: z.record(propDefinitionSchema), relations: z.array(componentRelationSchema).optional(), variants: z.array(fragmentVariantSchema), // Allow empty variants array contract: fragmentContractSchema.optional(), ai: aiMetadataSchema.optional(), usages: z.array(observedComponentUsageSchema).optional(), _generated: fragmentGeneratedSchema.optional(), }); const repoRelativePathSchema = z .string() .min(1) .superRefine((path, ctx) => { const error = portableRepoPathError(path); if (error) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: error, }); } }); const repoRelativePathsSchema = z.array(repoRelativePathSchema).min(1); const tokenSourceSchema = z.object({ path: repoRelativePathSchema, format: z.enum(["auto", "css", "scss", "dtcg", "tailwind"]).optional(), }); const tokenConfigSchema = z .object({ include: repoRelativePathsSchema.optional(), sources: z.array(tokenSourceSchema).min(1).optional(), exclude: z.array(repoRelativePathSchema).optional(), themeSelectors: z.record(z.string()).optional(), enabled: z.boolean().optional(), format: z.enum(["auto", "css", "scss", "dtcg", "tailwind"]).optional(), namespace: z.string().min(1).optional(), }) .passthrough() .superRefine((tokens, ctx) => { if (!tokens.include?.length && !tokens.sources?.length) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Add tokens.include or tokens.sources", path: ["sources"], }); } }); const topologySchema = z.object({ version: z.number().int().positive(), base: z.enum(["app", "repo"]).optional(), areas: z.array( z.object({ id: z.string().min(1), name: z.string().min(1), criticality: z.enum(["low", "medium", "high", "revenue", "regulated"]), owners: z.array(z.string()), files: repoRelativePathsSchema, routes: z.array(z.string().min(1)).optional(), priority: z.number().optional(), }) ), }); /** * Config schema - validates required fields, passes through optional config objects. * Type definitions are in types.ts - schema just ensures basic structure. */ export const fragmentsConfigSchema = z.object({ app: z .object({ path: repoRelativePathSchema.optional(), include: repoRelativePathsSchema.optional(), exclude: z.array(repoRelativePathSchema).optional(), }) .optional(), designSystem: z .object({ path: repoRelativePathSchema.optional(), packageName: z.string().min(1).optional(), components: repoRelativePathsSchema.optional(), }) .optional(), include: repoRelativePathsSchema.optional(), exclude: z.array(repoRelativePathSchema).optional(), components: repoRelativePathsSchema.optional(), outFile: repoRelativePathSchema.optional(), framework: z.enum(["react", "vue", "svelte"]).optional(), figmaFile: z.string().url().optional(), figmaToken: z.string().optional(), screenshots: z.object({}).passthrough().optional(), service: z.object({}).passthrough().optional(), registry: z.object({}).passthrough().optional(), tokens: tokenConfigSchema.optional(), snippets: z .object({ mode: z.enum(["warn", "error"]).optional(), scope: z.enum(["snippet", "snippet+render"]).optional(), requireFullSnippet: z.boolean().optional(), allowedExternalModules: z.array(z.string().min(1)).optional(), }) .optional(), performance: z .union([ z.enum(["strict", "standard", "relaxed"]), z.object({ preset: z.enum(["strict", "standard", "relaxed"]).optional(), budgets: z .object({ bundleSize: z.number().positive().optional(), }) .optional(), }), ]) .optional(), storybook: z .object({ exclude: z.array(z.string()).optional(), include: z.array(z.string()).optional(), excludeDeprecated: z.boolean().optional(), excludeTests: z.boolean().optional(), excludeSvgIcons: z.boolean().optional(), excludeSubComponents: z.boolean().optional(), }) .optional(), topology: topologySchema.optional(), govern: governanceConfigSchema.optional(), inspect: z .object({ localCanonical: z .record( z.string().min(1), z.object({ importPath: z.string().min(1), resolves: z .array( z.object({ tag: z.string().min(1), role: z.string().min(1).optional(), inputType: z.string().min(1).optional(), }) ) .min(1), }) ) .optional(), }) .optional(), recognizedClassHelpers: z.array(z.string().min(1)).optional(), }); // --------------------------------------------------------------------------- // v2 Schemas — alongside v1 (non-breaking) // --------------------------------------------------------------------------- /** * Schema for v2 composition metadata (promoted from ai) * Uses shorter field names since parent field is `composition`. */ export const compositionMetadataSchema = z.object({ pattern: z.enum(["compound", "simple", "controlled", "wrapper"]).optional(), subComponents: z.array(z.string()).optional(), requiredChildren: z.array(z.string()).optional(), commonPatterns: z.array(z.string()).optional(), }); /** * Schema for v2 extended provenance tracking */ export const fragmentProvenanceSchema = z.object({ source: z.enum(["storybook", "manual", "ai", "scan"]), sourceFile: z.string().optional(), confidence: z.number().min(0).max(1).optional(), timestamp: z.string().datetime().optional(), autoFields: z.array(z.string()).optional(), humanFields: z.array(z.string()).optional(), }); /** * Schema for v2 fragment definitions. * Accepts the v2 field names (guidance, examples, composition, _provenance). */ export const fragmentDefinitionV2Schema = z.object({ component: z.any(), meta: fragmentMetaSchema, guidance: fragmentUsageSchema, props: z.record(propDefinitionSchema), relations: z.array(componentRelationSchema).optional(), examples: z.array(fragmentVariantSchema), composition: compositionMetadataSchema.optional(), contract: fragmentContractSchema.optional(), _provenance: fragmentProvenanceSchema.optional(), }); export const governedFragmentDefinitionSchema = z.object({ component: z.any(), meta: fragmentMetaSchema, guidance: fragmentUsageSchema, props: z.record(propDefinitionSchema).optional(), relations: z.array(componentRelationSchema).optional(), examples: z.array(fragmentVariantSchema).optional(), composition: compositionMetadataSchema.optional(), contract: fragmentContractSchema.optional(), _provenance: fragmentProvenanceSchema.optional(), govern: z.function().optional(), governance: z.array(componentGovernanceRecordSchema).optional(), }); /** * @deprecated Use blockDefinitionSchema instead */ export const recipeDefinitionSchema = blockDefinitionSchema;