import { z } from 'zod' import { ANNOTATION_KINDS, CHART_TYPES, INTENT_IDS, INTERACTIVE_DEMO_CAPS, INTERACTIVE_DEMO_SCHEMA_VERSION, PANEL_TYPES, PATTERN_IDS, SCOPE_IDS, TRANSITIONS, } from './constants' const intentSchema = z.enum(INTENT_IDS).optional() const patternSchema = z.enum(PATTERN_IDS) const scopeSchema = z.enum(SCOPE_IDS) const revealSchema = z.object({ ids: z.array(z.string().min(1)).min(1), scope: scopeSchema, }) const annotationSchema = z.object({ kind: z.enum(ANNOTATION_KINDS), targetIds: z.array(z.string().min(1)).min(1), scope: scopeSchema, text: z.string().optional(), intent: intentSchema, }) const stepSchema = z.object({ id: z.string().min(1), speak: z.string().min(1), pattern: patternSchema.optional(), pointTo: z.array(z.string().min(1)).optional(), reveal: z.array(revealSchema).optional(), annotate: z .array(annotationSchema) .max(INTERACTIVE_DEMO_CAPS.maxAnnotationsPerStep) .optional(), }) const svgNodeSchema = z.object({ id: z.string().min(1), label: z.string().optional(), intent: intentSchema, }) const svgEdgeSchema = z.object({ id: z.string().min(1), from: z.string().min(1), to: z.string().min(1), style: z.enum(['solid', 'dashed']).optional(), weight: z.number().min(0).max(5).optional(), intent: intentSchema, }) const svgScenePayloadSchema = z.object({ nodes: z.array(svgNodeSchema).min(1), edges: z.array(svgEdgeSchema).optional(), }) const chartSeriesSchema = z.object({ id: z.string().min(1), label: z.string().optional(), values: z.array(z.number()), intent: intentSchema, }) const chartPayloadSchema = z.object({ chartType: z.enum(CHART_TYPES), series: z.array(chartSeriesSchema).min(1), }) const heatmapPayloadSchema = z.object({ rows: z.number().int().positive().max(50), cols: z.number().int().positive().max(50), values: z.array(z.array(z.number())), triangular: z.enum(['lower', 'upper', 'none']).optional(), rowLabels: z.array(z.string()).optional(), colLabels: z.array(z.string()).optional(), }) const panelSchema = z.discriminatedUnion('type', [ z.object({ id: z.string().min(1), type: z.literal('svg-scene'), payload: svgScenePayloadSchema, }), z.object({ id: z.string().min(1), type: z.literal('chart'), payload: chartPayloadSchema, }), z.object({ id: z.string().min(1), type: z.literal('heatmap-matrix'), payload: heatmapPayloadSchema, }), ]) const sceneSchema = z.object({ id: z.string().min(1).optional(), panels: z .array(panelSchema) .min(1) .max(INTERACTIVE_DEMO_CAPS.maxPanelsPerScene), }) const actSchema = z.object({ id: z.string().min(1), title: z.string().min(1), scene: sceneSchema.optional(), transition: z.enum(TRANSITIONS).optional(), pattern: patternSchema.optional(), steps: z.array(stepSchema).min(1).max(INTERACTIVE_DEMO_CAPS.maxStepsPerAct), }) /** Loose BCP-47: primary tag + optional subtags (fr, en, zh-Hans, pt-BR). */ const langSchema = z .string() .regex(/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/, 'Invalid BCP-47 language tag') export const interactiveDemoV1Schema = z.object({ schemaVersion: z.literal(INTERACTIVE_DEMO_SCHEMA_VERSION), id: z.string().min(1), lang: langSchema, disclaimer: z.string().optional(), scene: sceneSchema, acts: z.array(actSchema).min(1).max(INTERACTIVE_DEMO_CAPS.maxActs), }) export type InteractiveDemoV1Parsed = z.infer /** Re-export allowlists for callers that need them at runtime. */ export { PANEL_TYPES, PATTERN_IDS }