| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- import type { Spec, UIElement } from "./types";
- // =============================================================================
- // Spec Structural Validation
- // =============================================================================
- /**
- * Severity level for validation issues.
- */
- export type SpecIssueSeverity = "error" | "warning";
- /**
- * A single validation issue found in a spec.
- */
- export interface SpecIssue {
- /** Severity: errors should be fixed, warnings are informational */
- severity: SpecIssueSeverity;
- /** Human-readable description of the issue */
- message: string;
- /** The element key where the issue was found (if applicable) */
- elementKey?: string;
- /** Machine-readable issue code for programmatic handling */
- code:
- | "missing_root"
- | "root_not_found"
- | "missing_child"
- | "visible_in_props"
- | "orphaned_element"
- | "empty_spec"
- | "on_in_props"
- | "repeat_in_props";
- }
- /**
- * Result of spec structural validation.
- */
- export interface SpecValidationIssues {
- /** Whether the spec passed validation (no errors; warnings are OK) */
- valid: boolean;
- /** List of issues found */
- issues: SpecIssue[];
- }
- /**
- * Options for validateSpec.
- */
- export interface ValidateSpecOptions {
- /**
- * Whether to check for orphaned elements (elements not reachable from root).
- * Defaults to false since orphans are harmless (just unused).
- */
- checkOrphans?: boolean;
- }
- /**
- * Validate a spec for structural integrity.
- *
- * Checks for common AI-generation errors:
- * - Missing or empty root
- * - Root element not found in elements map
- * - Children referencing non-existent elements
- * - `visible` placed inside `props` instead of on the element
- * - Orphaned elements (optional)
- *
- * @example
- * ```ts
- * const result = validateSpec(spec);
- * if (!result.valid) {
- * console.log("Spec errors:", result.issues);
- * }
- * ```
- */
- export function validateSpec(
- spec: Spec,
- options: ValidateSpecOptions = {},
- ): SpecValidationIssues {
- const { checkOrphans = false } = options;
- const issues: SpecIssue[] = [];
- // 1. Check root
- if (!spec.root) {
- issues.push({
- severity: "error",
- message: "Spec has no root element defined.",
- code: "missing_root",
- });
- return { valid: false, issues };
- }
- if (!spec.elements[spec.root]) {
- issues.push({
- severity: "error",
- message: `Root element "${spec.root}" not found in elements map.`,
- code: "root_not_found",
- });
- }
- // 2. Check for empty spec
- if (Object.keys(spec.elements).length === 0) {
- issues.push({
- severity: "error",
- message: "Spec has no elements.",
- code: "empty_spec",
- });
- return { valid: false, issues };
- }
- // 3. Check each element
- for (const [key, element] of Object.entries(spec.elements)) {
- // 3a. Missing children
- if (element.children) {
- for (const childKey of element.children) {
- if (!spec.elements[childKey]) {
- issues.push({
- severity: "error",
- message: `Element "${key}" references child "${childKey}" which does not exist in the elements map.`,
- elementKey: key,
- code: "missing_child",
- });
- }
- }
- }
- // 3b. `visible` inside props
- const props = element.props as Record<string, unknown> | undefined;
- if (props && "visible" in props && props.visible !== undefined) {
- issues.push({
- severity: "error",
- message: `Element "${key}" has "visible" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
- elementKey: key,
- code: "visible_in_props",
- });
- }
- // 3c. `on` inside props (should be a top-level field)
- if (props && "on" in props && props.on !== undefined) {
- issues.push({
- severity: "error",
- message: `Element "${key}" has "on" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
- elementKey: key,
- code: "on_in_props",
- });
- }
- // 3d. `repeat` inside props (should be a top-level field)
- if (props && "repeat" in props && props.repeat !== undefined) {
- issues.push({
- severity: "error",
- message: `Element "${key}" has "repeat" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
- elementKey: key,
- code: "repeat_in_props",
- });
- }
- }
- // 4. Orphaned elements (optional)
- if (checkOrphans) {
- const reachable = new Set<string>();
- const walk = (key: string) => {
- if (reachable.has(key)) return;
- reachable.add(key);
- const el = spec.elements[key];
- if (el?.children) {
- for (const childKey of el.children) {
- if (spec.elements[childKey]) {
- walk(childKey);
- }
- }
- }
- };
- if (spec.elements[spec.root]) {
- walk(spec.root);
- }
- for (const key of Object.keys(spec.elements)) {
- if (!reachable.has(key)) {
- issues.push({
- severity: "warning",
- message: `Element "${key}" is not reachable from root "${spec.root}".`,
- elementKey: key,
- code: "orphaned_element",
- });
- }
- }
- }
- const hasErrors = issues.some((i) => i.severity === "error");
- return { valid: !hasErrors, issues };
- }
- /**
- * Auto-fix common spec issues in-place and return a corrected copy.
- *
- * Currently fixes:
- * - `visible` inside `props` → moved to element level
- * - `on` inside `props` → moved to element level
- * - `repeat` inside `props` → moved to element level
- *
- * Returns the fixed spec and a list of fixes applied.
- */
- export function autoFixSpec(spec: Spec): {
- spec: Spec;
- fixes: string[];
- } {
- const fixes: string[] = [];
- const fixedElements: Record<string, UIElement> = {};
- for (const [key, element] of Object.entries(spec.elements)) {
- const props = element.props as Record<string, unknown> | undefined;
- let fixed = element;
- if (props && "visible" in props && props.visible !== undefined) {
- // Move visible from props to element level
- const { visible, ...restProps } = fixed.props as Record<string, unknown>;
- fixed = {
- ...fixed,
- props: restProps,
- visible: visible as UIElement["visible"],
- };
- fixes.push(`Moved "visible" from props to element level on "${key}".`);
- }
- let currentProps = fixed.props as Record<string, unknown> | undefined;
- if (currentProps && "on" in currentProps && currentProps.on !== undefined) {
- // Move on from props to element level
- const { on, ...restProps } = currentProps;
- fixed = {
- ...fixed,
- props: restProps,
- on: on as UIElement["on"],
- };
- fixes.push(`Moved "on" from props to element level on "${key}".`);
- }
- currentProps = fixed.props as Record<string, unknown> | undefined;
- if (
- currentProps &&
- "repeat" in currentProps &&
- currentProps.repeat !== undefined
- ) {
- // Move repeat from props to element level
- const { repeat, ...restProps } = currentProps;
- fixed = {
- ...fixed,
- props: restProps,
- repeat: repeat as UIElement["repeat"],
- };
- fixes.push(`Moved "repeat" from props to element level on "${key}".`);
- }
- fixedElements[key] = fixed;
- }
- return {
- spec: { root: spec.root, elements: fixedElements },
- fixes,
- };
- }
- /**
- * Format validation issues into a human-readable string suitable for
- * inclusion in a repair prompt sent back to the AI.
- */
- export function formatSpecIssues(issues: SpecIssue[]): string {
- const errors = issues.filter((i) => i.severity === "error");
- if (errors.length === 0) return "";
- const lines = ["The generated UI spec has the following errors:"];
- for (const issue of errors) {
- lines.push(`- ${issue.message}`);
- }
- return lines.join("\n");
- }
|