@formspec/constraints 0.1.0-alpha.21 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/loader.ts","../src/defaults.ts","../src/validators/field-types.ts","../src/validators/layout.ts","../src/validators/field-options.ts","../src/validators/formspec.ts"],"sourcesContent":["/**\n * \\@formspec/constraints\n *\n * Constraint validation for FormSpec - restrict features based on target\n * environment capabilities.\n *\n * This package provides:\n * - Type definitions for constraint configuration\n * - YAML/TypeScript config file loading\n * - Core validation logic for FormSpec elements\n * - JSON Schema for .formspec.yml validation\n *\n * @example\n * ```ts\n * import { loadConfig, validateFormSpecElements } from '@formspec/constraints';\n * import { formspec, field } from '@formspec/dsl';\n *\n * // Load constraints from .formspec.yml\n * const { config } = await loadConfig();\n *\n * // Create a form\n * const form = formspec(\n * field.text(\"name\"),\n * field.dynamicEnum(\"country\", \"countries\"),\n * );\n *\n * // Validate against constraints\n * const result = validateFormSpecElements(form.elements, { constraints: config });\n *\n * if (!result.valid) {\n * console.error('Validation failed:', result.issues);\n * }\n * ```\n *\n * @packageDocumentation\n */\n\n// Types\nexport type {\n Severity,\n FieldTypeConstraints,\n LayoutConstraints,\n LayoutTypeConstraints,\n RuleEffectConstraints,\n RuleConstraints,\n UISchemaConstraints,\n FieldOptionConstraints,\n ControlOptionConstraints,\n ConstraintConfig,\n ResolvedConstraintConfig,\n ResolvedUISchemaConstraints,\n ResolvedRuleConstraints,\n FormSpecConfig,\n ValidationIssue,\n ValidationResult,\n} from \"./types.js\";\n\n// Config loading\nexport {\n loadConfig,\n loadConfigFromString,\n defineConstraints,\n type LoadConfigOptions,\n type LoadConfigResult,\n} from \"./loader.js\";\n\n// Defaults\nexport { DEFAULT_CONSTRAINTS, DEFAULT_CONFIG, mergeWithDefaults } from \"./defaults.js\";\n\n// Validators\nexport {\n validateFormSpecElements,\n validateFormSpec,\n type FormSpecValidationOptions,\n} from \"./validators/formspec.js\";\n\nexport {\n validateFieldTypes,\n isFieldTypeAllowed,\n getFieldTypeSeverity,\n type FieldTypeContext,\n} from \"./validators/field-types.js\";\n\nexport {\n validateLayout,\n isLayoutTypeAllowed,\n isNestingDepthAllowed,\n type LayoutContext,\n} from \"./validators/layout.js\";\n\nexport {\n validateFieldOptions,\n extractFieldOptions,\n isFieldOptionAllowed,\n getFieldOptionSeverity,\n type FieldOptionsContext,\n type FieldOption,\n} from \"./validators/field-options.js\";\n","import { readFile } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport type { FormSpecConfig, ConstraintConfig, ResolvedConstraintConfig } from \"./types.js\";\nimport { mergeWithDefaults } from \"./defaults.js\";\n\n/**\n * Default config file names to search for (in order of priority).\n */\nconst CONFIG_FILE_NAMES = [\".formspec.yml\", \".formspec.yaml\", \"formspec.yml\"];\n\n/**\n * Options for loading configuration.\n *\n * @public\n */\nexport interface LoadConfigOptions {\n /**\n * The directory to search for config files.\n * Defaults to process.cwd().\n */\n cwd?: string;\n\n /**\n * Explicit path to a config file.\n * If provided, skips searching for default config file names.\n */\n configPath?: string;\n\n /**\n * Whether to search parent directories for config files.\n * Defaults to true.\n */\n searchParents?: boolean;\n}\n\n/**\n * Result of loading configuration.\n *\n * @public\n */\nexport interface LoadConfigResult {\n /** The loaded and merged configuration */\n config: ResolvedConstraintConfig;\n /** The path to the config file that was loaded (if any) */\n configPath: string | null;\n /** Whether a config file was found */\n found: boolean;\n}\n\n/**\n * Searches for a config file in the given directory and optionally parent directories.\n */\nasync function findConfigFile(startDir: string, searchParents: boolean): Promise<string | null> {\n let currentDir = resolve(startDir);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop with break conditions\n while (true) {\n for (const fileName of CONFIG_FILE_NAMES) {\n const filePath = resolve(currentDir, fileName);\n try {\n await readFile(filePath);\n return filePath;\n } catch {\n // File doesn't exist, continue searching\n }\n }\n\n if (!searchParents) {\n break;\n }\n\n const parentDir = dirname(currentDir);\n // Reached filesystem root when dirname returns same path\n if (parentDir === currentDir) {\n break;\n }\n currentDir = parentDir;\n }\n\n return null;\n}\n\n/**\n * Parses a YAML config file and returns the FormSpecConfig.\n */\nasync function parseConfigFile(filePath: string): Promise<FormSpecConfig> {\n const content = await readFile(filePath, \"utf-8\");\n const parsed = parseYaml(content) as unknown;\n\n if (parsed === null || parsed === undefined) {\n return {};\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config file at ${filePath}: expected an object, got ${typeof parsed}`);\n }\n\n return parsed as FormSpecConfig;\n}\n\n/**\n * Loads FormSpec constraint configuration from a .formspec.yml file.\n *\n * @param options - Options for loading configuration\n * @returns The loaded configuration with defaults applied\n *\n * @example\n * ```ts\n * // Load from current directory (searches for .formspec.yml)\n * const result = await loadConfig();\n *\n * // Load from specific directory\n * const result = await loadConfig({ cwd: '/path/to/project' });\n *\n * // Load from specific file\n * const result = await loadConfig({ configPath: '/path/to/config.yml' });\n * ```\n *\n * @public\n */\nexport async function loadConfig(options: LoadConfigOptions = {}): Promise<LoadConfigResult> {\n const { cwd = process.cwd(), configPath, searchParents = true } = options;\n\n let resolvedPath: string | null = null;\n\n if (configPath) {\n resolvedPath = resolve(cwd, configPath);\n try {\n await readFile(resolvedPath);\n } catch {\n throw new Error(`Config file not found at ${resolvedPath}`);\n }\n } else {\n resolvedPath = await findConfigFile(cwd, searchParents);\n }\n\n if (!resolvedPath) {\n return {\n config: mergeWithDefaults(undefined),\n configPath: null,\n found: false,\n };\n }\n\n const fileConfig = await parseConfigFile(resolvedPath);\n const config = mergeWithDefaults(fileConfig.constraints);\n\n return {\n config,\n configPath: resolvedPath,\n found: true,\n };\n}\n\n/**\n * Synchronously loads config from a pre-parsed YAML string.\n * Useful for testing or when config is already available.\n *\n * @param yamlContent - The YAML content to parse\n * @returns The parsed and merged configuration\n *\n * @public\n */\nexport function loadConfigFromString(yamlContent: string): ResolvedConstraintConfig {\n const parsed = parseYaml(yamlContent) as FormSpecConfig | null | undefined;\n\n if (parsed === null || parsed === undefined) {\n return mergeWithDefaults(undefined);\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config content: expected an object, got ${typeof parsed}`);\n }\n\n return mergeWithDefaults(parsed.constraints);\n}\n\n/**\n * Creates a constraint configuration directly from an object.\n * Useful for programmatic configuration without YAML.\n *\n * @param config - Partial constraint configuration\n * @returns Complete configuration with defaults applied\n *\n * @example\n * ```ts\n * const config = defineConstraints({\n * fieldTypes: {\n * dynamicEnum: 'error',\n * dynamicSchema: 'error',\n * },\n * layout: {\n * group: 'error',\n * },\n * });\n * ```\n *\n * @public\n */\nexport function defineConstraints(config: ConstraintConfig): ResolvedConstraintConfig {\n return mergeWithDefaults(config);\n}\n","import type { ConstraintConfig, FormSpecConfig, ResolvedConstraintConfig } from \"./types.js\";\n\n/**\n * Default constraint configuration that allows all features.\n * All constraints default to \"off\" (allowed).\n *\n * @public\n */\nexport const DEFAULT_CONSTRAINTS: ResolvedConstraintConfig = {\n fieldTypes: {\n text: \"off\",\n number: \"off\",\n boolean: \"off\",\n staticEnum: \"off\",\n dynamicEnum: \"off\",\n dynamicSchema: \"off\",\n array: \"off\",\n object: \"off\",\n },\n layout: {\n group: \"off\",\n conditionals: \"off\",\n maxNestingDepth: Infinity,\n },\n uiSchema: {\n layouts: {\n VerticalLayout: \"off\",\n HorizontalLayout: \"off\",\n Group: \"off\",\n Categorization: \"off\",\n Category: \"off\",\n },\n rules: {\n enabled: \"off\",\n effects: {\n SHOW: \"off\",\n HIDE: \"off\",\n ENABLE: \"off\",\n DISABLE: \"off\",\n },\n },\n },\n fieldOptions: {\n label: \"off\",\n placeholder: \"off\",\n required: \"off\",\n minValue: \"off\",\n maxValue: \"off\",\n minItems: \"off\",\n maxItems: \"off\",\n },\n controlOptions: {\n format: \"off\",\n readonly: \"off\",\n multi: \"off\",\n showUnfocusedDescription: \"off\",\n hideRequiredAsterisk: \"off\",\n custom: {},\n },\n};\n\n/**\n * Default FormSpec configuration.\n *\n * @public\n */\nexport const DEFAULT_CONFIG: FormSpecConfig = {\n constraints: DEFAULT_CONSTRAINTS,\n};\n\n/**\n * Merges user constraints with defaults, filling in any missing values.\n *\n * @public\n */\nexport function mergeWithDefaults(config: ConstraintConfig | undefined): ResolvedConstraintConfig {\n if (!config) {\n return DEFAULT_CONSTRAINTS;\n }\n\n return {\n fieldTypes: {\n ...DEFAULT_CONSTRAINTS.fieldTypes,\n ...config.fieldTypes,\n },\n layout: {\n ...DEFAULT_CONSTRAINTS.layout,\n ...config.layout,\n },\n uiSchema: {\n layouts: {\n ...DEFAULT_CONSTRAINTS.uiSchema.layouts,\n ...config.uiSchema?.layouts,\n },\n rules: {\n enabled: config.uiSchema?.rules?.enabled ?? DEFAULT_CONSTRAINTS.uiSchema.rules.enabled,\n effects: {\n ...DEFAULT_CONSTRAINTS.uiSchema.rules.effects,\n ...config.uiSchema?.rules?.effects,\n },\n },\n },\n fieldOptions: {\n ...DEFAULT_CONSTRAINTS.fieldOptions,\n ...config.fieldOptions,\n },\n controlOptions: {\n ...DEFAULT_CONSTRAINTS.controlOptions,\n ...config.controlOptions,\n custom: {\n ...DEFAULT_CONSTRAINTS.controlOptions.custom,\n ...config.controlOptions?.custom,\n },\n },\n };\n}\n","import type { FieldTypeConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Maps FormSpec field._field values to constraint config keys.\n */\nconst FIELD_TYPE_MAP: Record<string, keyof FieldTypeConstraints> = {\n text: \"text\",\n number: \"number\",\n boolean: \"boolean\",\n enum: \"staticEnum\",\n dynamic_enum: \"dynamicEnum\",\n dynamic_schema: \"dynamicSchema\",\n array: \"array\",\n object: \"object\",\n};\n\n/**\n * Human-readable names for field types.\n */\nconst FIELD_TYPE_NAMES: Record<string, string> = {\n text: \"text field\",\n number: \"number field\",\n boolean: \"boolean field\",\n enum: \"static enum field\",\n dynamic_enum: \"dynamic enum field\",\n dynamic_schema: \"dynamic schema field\",\n array: \"array field\",\n object: \"object field\",\n};\n\n/**\n * Context for field type validation.\n *\n * @public\n */\nexport interface FieldTypeContext {\n /** The _field discriminator value (e.g., \"text\", \"number\", \"enum\") */\n fieldType: string;\n /** The field name */\n fieldName: string;\n /** Optional path for nested fields */\n path?: string;\n}\n\n/**\n * Validates a field type against constraints.\n *\n * @param context - Information about the field being validated\n * @param constraints - Field type constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @public\n */\nexport function validateFieldTypes(\n context: FieldTypeContext,\n constraints: FieldTypeConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n const constraintKey = FIELD_TYPE_MAP[context.fieldType];\n if (!constraintKey) {\n // Unknown field type, skip validation\n return issues;\n }\n\n const severity = constraints[constraintKey];\n if (severity && severity !== \"off\") {\n const fieldTypeName = FIELD_TYPE_NAMES[context.fieldType] ?? context.fieldType;\n issues.push(createFieldTypeIssue(context, fieldTypeName, severity));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field type.\n */\nfunction createFieldTypeIssue(\n context: FieldTypeContext,\n fieldTypeName: string,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_TYPE\",\n message: `Field \"${context.fieldName}\" uses ${fieldTypeName}, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldTypes\",\n path,\n fieldName: context.fieldName,\n fieldType: context.fieldType,\n };\n}\n\n/**\n * Checks if a field type is allowed by the constraints.\n * Useful for quick checks without generating issues.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns true if allowed, false if disallowed\n *\n * @public\n */\nexport function isFieldTypeAllowed(fieldType: string, constraints: FieldTypeConstraints): boolean {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return true; // Unknown types are allowed by default\n }\n\n const severity = constraints[constraintKey];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field type.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @public\n */\nexport function getFieldTypeSeverity(\n fieldType: string,\n constraints: FieldTypeConstraints\n): Severity {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return \"off\";\n }\n\n return constraints[constraintKey] ?? \"off\";\n}\n","import type { LayoutConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Context for layout validation.\n *\n * @public\n */\nexport interface LayoutContext {\n /** The type of layout element (\"group\" | \"conditional\") */\n layoutType: \"group\" | \"conditional\";\n /** Optional label for the element (for groups) */\n label?: string;\n /** Current nesting depth */\n depth: number;\n /** Path to this element */\n path?: string;\n}\n\n/**\n * Validates a layout element against constraints.\n *\n * @param context - Information about the layout element\n * @param constraints - Layout constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @public\n */\nexport function validateLayout(\n context: LayoutContext,\n constraints: LayoutConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n // Check if groups are allowed\n if (context.layoutType === \"group\") {\n const groupSeverity = constraints.group;\n if (groupSeverity && groupSeverity !== \"off\") {\n issues.push(createGroupIssue(context, groupSeverity));\n }\n }\n\n // Check if conditionals are allowed\n if (context.layoutType === \"conditional\") {\n const conditionalSeverity = constraints.conditionals;\n if (conditionalSeverity && conditionalSeverity !== \"off\") {\n issues.push(createConditionalIssue(context, conditionalSeverity));\n }\n }\n\n // Check nesting depth (applies to both groups and fields within nested structures)\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth !== undefined && context.depth > maxDepth) {\n issues.push(createNestingDepthIssue(context, maxDepth));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed group.\n */\nfunction createGroupIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const labelInfo = context.label ? ` \"${context.label}\"` : \"\";\n const issue: ValidationIssue = {\n code: \"DISALLOWED_GROUP\",\n message: `Group${labelInfo} is not allowed - visual grouping is not supported in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for a disallowed conditional.\n */\nfunction createConditionalIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"DISALLOWED_CONDITIONAL\",\n message: `Conditional visibility (when/is) is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for exceeding nesting depth.\n */\nfunction createNestingDepthIssue(context: LayoutContext, maxDepth: number): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"EXCEEDED_NESTING_DEPTH\",\n message: `Nesting depth ${String(context.depth)} exceeds maximum allowed depth of ${String(maxDepth)}`,\n severity: \"error\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Checks if a layout type is allowed by the constraints.\n *\n * @param layoutType - The type of layout element\n * @param constraints - Layout constraints\n * @returns true if allowed, false if disallowed\n *\n * @public\n */\nexport function isLayoutTypeAllowed(\n layoutType: \"group\" | \"conditional\",\n constraints: LayoutConstraints\n): boolean {\n if (layoutType === \"group\") {\n const severity = constraints.group;\n return !severity || severity === \"off\";\n }\n\n // layoutType === \"conditional\"\n const severity = constraints.conditionals;\n return !severity || severity === \"off\";\n}\n\n/**\n * Checks if a nesting depth is allowed.\n *\n * @param depth - Current nesting depth\n * @param constraints - Layout constraints\n * @returns true if allowed, false if exceeds limit\n *\n * @public\n */\nexport function isNestingDepthAllowed(depth: number, constraints: LayoutConstraints): boolean {\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth === undefined) {\n return true;\n }\n return depth <= maxDepth;\n}\n","import type { FieldOptionConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Known field options that can be validated.\n *\n * @public\n */\nexport type FieldOption =\n | \"label\"\n | \"placeholder\"\n | \"required\"\n | \"minValue\"\n | \"maxValue\"\n | \"minItems\"\n | \"maxItems\";\n\n/**\n * Context for field option validation.\n *\n * @public\n */\nexport interface FieldOptionsContext {\n /** The field name */\n fieldName: string;\n /** Which options are present on this field */\n presentOptions: FieldOption[];\n /** Path to this field */\n path?: string;\n}\n\n/**\n * Validates field options against constraints.\n *\n * @param context - Information about the field and its options\n * @param constraints - Field option constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @public\n */\nexport function validateFieldOptions(\n context: FieldOptionsContext,\n constraints: FieldOptionConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n for (const option of context.presentOptions) {\n const severity = constraints[option];\n if (severity && severity !== \"off\") {\n issues.push(createFieldOptionIssue(context, option, severity));\n }\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field option.\n */\nfunction createFieldOptionIssue(\n context: FieldOptionsContext,\n option: FieldOption,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_OPTION\",\n message: `Field \"${context.fieldName}\" uses the \"${option}\" option, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldOptions\",\n path,\n fieldName: context.fieldName,\n };\n}\n\n/**\n * Extracts which options are present on a field object.\n * Works with FormSpec field types.\n *\n * @param field - A field object with potential options\n * @returns Array of present option names\n *\n * @public\n */\nexport function extractFieldOptions(field: Record<string, unknown>): FieldOption[] {\n const options: FieldOption[] = [];\n\n if (field[\"label\"] !== undefined) options.push(\"label\");\n if (field[\"placeholder\"] !== undefined) options.push(\"placeholder\");\n if (field[\"required\"] !== undefined) options.push(\"required\");\n // NumberField uses \"min\"/\"max\" in core types, map to \"minValue\"/\"maxValue\" constraints\n if (field[\"min\"] !== undefined || field[\"minValue\"] !== undefined) options.push(\"minValue\");\n if (field[\"max\"] !== undefined || field[\"maxValue\"] !== undefined) options.push(\"maxValue\");\n if (field[\"minItems\"] !== undefined) options.push(\"minItems\");\n if (field[\"maxItems\"] !== undefined) options.push(\"maxItems\");\n\n return options;\n}\n\n/**\n * Checks if a specific field option is allowed.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns true if allowed, false if disallowed\n *\n * @public\n */\nexport function isFieldOptionAllowed(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): boolean {\n const severity = constraints[option];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field option.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @public\n */\nexport function getFieldOptionSeverity(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): Severity {\n return constraints[option] ?? \"off\";\n}\n","import type { FormElement, FormSpec, AnyField } from \"@formspec/core\";\nimport type {\n ConstraintConfig,\n ResolvedConstraintConfig,\n ValidationIssue,\n ValidationResult,\n} from \"../types.js\";\nimport { mergeWithDefaults } from \"../defaults.js\";\nimport { validateFieldTypes } from \"./field-types.js\";\nimport { validateLayout } from \"./layout.js\";\nimport { validateFieldOptions, extractFieldOptions } from \"./field-options.js\";\n\n/**\n * Options for validating FormSpec elements.\n *\n * @public\n */\nexport interface FormSpecValidationOptions {\n /** Constraint configuration (will be merged with defaults) */\n constraints?: ConstraintConfig;\n}\n\n/**\n * Validates FormSpec elements against constraints.\n *\n * This is the main entry point for validating a form specification\n * against a constraint configuration. It walks through all elements\n * and checks each one against the configured constraints.\n *\n * @param elements - FormSpec elements to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @example\n * ```ts\n * import { formspec, field, group } from '@formspec/dsl';\n * import { validateFormSpecElements, defineConstraints } from '@formspec/constraints';\n *\n * const form = formspec(\n * group(\"Contact\",\n * field.text(\"name\"),\n * field.dynamicEnum(\"country\", \"countries\"),\n * ),\n * );\n *\n * const result = validateFormSpecElements(form.elements, {\n * constraints: {\n * fieldTypes: { dynamicEnum: 'error' },\n * layout: { group: 'error' },\n * },\n * });\n *\n * if (!result.valid) {\n * console.error('Validation failed:', result.issues);\n * }\n * ```\n *\n * @public\n */\nexport function validateFormSpecElements(\n elements: readonly FormElement[],\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n const constraints = mergeWithDefaults(options.constraints);\n const issues: ValidationIssue[] = [];\n\n // Walk through all elements\n walkElements(elements, constraints, issues, \"\", 0);\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n}\n\n/**\n * Validates a complete FormSpec against constraints.\n *\n * @param formSpec - The FormSpec to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @public\n */\nexport function validateFormSpec(\n formSpec: FormSpec<readonly FormElement[]>,\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n return validateFormSpecElements(formSpec.elements, options);\n}\n\n/**\n * Recursively walks through FormSpec elements and validates each one.\n */\nfunction walkElements(\n elements: readonly FormElement[],\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n for (const element of elements) {\n const elementPath = pathPrefix;\n\n if (element._type === \"field\") {\n validateField(element, constraints, issues, elementPath, depth);\n } else if (element._type === \"group\") {\n validateGroup(element, constraints, issues, elementPath, depth);\n } else {\n // element._type === \"conditional\"\n validateConditional(element, constraints, issues, elementPath, depth);\n }\n }\n}\n\n/**\n * Validates a field element.\n */\nfunction validateField(\n field: AnyField,\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const fieldPath = pathPrefix ? `${pathPrefix}/${field.name}` : field.name;\n\n // Validate field type\n const fieldTypeIssues = validateFieldTypes(\n {\n fieldType: field._field,\n fieldName: field.name,\n path: fieldPath,\n },\n constraints.fieldTypes\n );\n issues.push(...fieldTypeIssues);\n\n // Validate field options\n const presentOptions = extractFieldOptions(field as unknown as Record<string, unknown>);\n if (presentOptions.length > 0) {\n const optionIssues = validateFieldOptions(\n {\n fieldName: field.name,\n presentOptions,\n path: fieldPath,\n },\n constraints.fieldOptions\n );\n issues.push(...optionIssues);\n }\n\n // Check nesting depth for array/object fields\n if (field._field === \"array\" || field._field === \"object\") {\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\", // Arrays/objects contribute to nesting depth\n depth: depth + 1,\n path: fieldPath,\n },\n constraints.layout\n );\n // Only add nesting depth issues, not group issues\n issues.push(...layoutIssues.filter((issue) => issue.code === \"EXCEEDED_NESTING_DEPTH\"));\n\n // Recursively validate nested elements\n if (field._field === \"array\" && \"items\" in field) {\n walkElements(\n field.items as readonly FormElement[],\n constraints,\n issues,\n `${fieldPath}[]`,\n depth + 1\n );\n } else if (\"properties\" in field) {\n // field._field === \"object\"\n walkElements(\n field.properties as readonly FormElement[],\n constraints,\n issues,\n fieldPath,\n depth + 1\n );\n }\n }\n}\n\n/**\n * Validates a group element.\n */\nfunction validateGroup(\n group: {\n readonly _type: \"group\";\n readonly label: string;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const groupPath = pathPrefix ? `${pathPrefix}/[group:${group.label}]` : `[group:${group.label}]`;\n\n // Validate group usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\",\n label: group.label,\n depth,\n path: groupPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements (groups don't increase nesting depth for schema)\n walkElements(group.elements, constraints, issues, pathPrefix, depth);\n}\n\n/**\n * Validates a conditional element.\n */\nfunction validateConditional(\n conditional: {\n readonly _type: \"conditional\";\n readonly field: string;\n readonly value: unknown;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const condPath = pathPrefix\n ? `${pathPrefix}/[when:${conditional.field}=${String(conditional.value)}]`\n : `[when:${conditional.field}=${String(conditional.value)}]`;\n\n // Validate conditional usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"conditional\",\n depth,\n path: condPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements\n walkElements(conditional.elements, constraints, issues, pathPrefix, depth);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,sBAAyB;AACzB,uBAAiC;AACjC,kBAAmC;;;ACM5B,IAAM,sBAAgD;AAAA,EAC3D,YAAY;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,eAAe;AAAA,IACf,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,QAAQ;AAAA,IACN,OAAO;AAAA,IACP,cAAc;AAAA,IACd,iBAAiB;AAAA,EACnB;AAAA,EACA,UAAU;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,kBAAkB;AAAA,MAClB,OAAO;AAAA,MACP,gBAAgB;AAAA,MAChB,UAAU;AAAA,IACZ;AAAA,IACA,OAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,gBAAgB;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,0BAA0B;AAAA,IAC1B,sBAAsB;AAAA,IACtB,QAAQ,CAAC;AAAA,EACX;AACF;AAOO,IAAM,iBAAiC;AAAA,EAC5C,aAAa;AACf;AAOO,SAAS,kBAAkB,QAAgE;AAChG,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,MACV,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,QAAQ;AAAA,MACN,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,UAAU;AAAA,MACR,SAAS;AAAA,QACP,GAAG,oBAAoB,SAAS;AAAA,QAChC,GAAG,OAAO,UAAU;AAAA,MACtB;AAAA,MACA,OAAO;AAAA,QACL,SAAS,OAAO,UAAU,OAAO,WAAW,oBAAoB,SAAS,MAAM;AAAA,QAC/E,SAAS;AAAA,UACP,GAAG,oBAAoB,SAAS,MAAM;AAAA,UACtC,GAAG,OAAO,UAAU,OAAO;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAAA,IACA,cAAc;AAAA,MACZ,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,gBAAgB;AAAA,MACd,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,MACV,QAAQ;AAAA,QACN,GAAG,oBAAoB,eAAe;AAAA,QACtC,GAAG,OAAO,gBAAgB;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;;;AD1GA,IAAM,oBAAoB,CAAC,iBAAiB,kBAAkB,cAAc;AA4C5E,eAAe,eAAe,UAAkB,eAAgD;AAC9F,MAAI,iBAAa,0BAAQ,QAAQ;AAGjC,SAAO,MAAM;AACX,eAAW,YAAY,mBAAmB;AACxC,YAAM,eAAW,0BAAQ,YAAY,QAAQ;AAC7C,UAAI;AACF,kBAAM,0BAAS,QAAQ;AACvB,eAAO;AAAA,MACT,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,CAAC,eAAe;AAClB;AAAA,IACF;AAEA,UAAM,gBAAY,0BAAQ,UAAU;AAEpC,QAAI,cAAc,YAAY;AAC5B;AAAA,IACF;AACA,iBAAa;AAAA,EACf;AAEA,SAAO;AACT;AAKA,eAAe,gBAAgB,UAA2C;AACxE,QAAM,UAAU,UAAM,0BAAS,UAAU,OAAO;AAChD,QAAM,aAAS,YAAAA,OAAU,OAAO;AAEhC,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO,CAAC;AAAA,EACV;AAEA,MAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,UAAM,IAAI,MAAM,0BAA0B,QAAQ,6BAA6B,OAAO,MAAM,EAAE;AAAA,EAChG;AAEA,SAAO;AACT;AAsBA,eAAsB,WAAW,UAA6B,CAAC,GAA8B;AAC3F,QAAM,EAAE,MAAM,QAAQ,IAAI,GAAG,YAAY,gBAAgB,KAAK,IAAI;AAElE,MAAI,eAA8B;AAElC,MAAI,YAAY;AACd,uBAAe,0BAAQ,KAAK,UAAU;AACtC,QAAI;AACF,gBAAM,0BAAS,YAAY;AAAA,IAC7B,QAAQ;AACN,YAAM,IAAI,MAAM,4BAA4B,YAAY,EAAE;AAAA,IAC5D;AAAA,EACF,OAAO;AACL,mBAAe,MAAM,eAAe,KAAK,aAAa;AAAA,EACxD;AAEA,MAAI,CAAC,cAAc;AACjB,WAAO;AAAA,MACL,QAAQ,kBAAkB,MAAS;AAAA,MACnC,YAAY;AAAA,MACZ,OAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,aAAa,MAAM,gBAAgB,YAAY;AACrD,QAAM,SAAS,kBAAkB,WAAW,WAAW;AAEvD,SAAO;AAAA,IACL;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,EACT;AACF;AAWO,SAAS,qBAAqB,aAA+C;AAClF,QAAM,aAAS,YAAAA,OAAU,WAAW;AAEpC,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO,kBAAkB,MAAS;AAAA,EACpC;AAEA,MAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,UAAM,IAAI,MAAM,mDAAmD,OAAO,MAAM,EAAE;AAAA,EACpF;AAEA,SAAO,kBAAkB,OAAO,WAAW;AAC7C;AAwBO,SAAS,kBAAkB,QAAoD;AACpF,SAAO,kBAAkB,MAAM;AACjC;;;AErMA,IAAM,iBAA6D;AAAA,EACjE,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,QAAQ;AACV;AAKA,IAAM,mBAA2C;AAAA,EAC/C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,QAAQ;AACV;AAyBO,SAAS,mBACd,SACA,aACmB;AACnB,QAAM,SAA4B,CAAC;AAEnC,QAAM,gBAAgB,eAAe,QAAQ,SAAS;AACtD,MAAI,CAAC,eAAe;AAElB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,YAAY,aAAa;AAC1C,MAAI,YAAY,aAAa,OAAO;AAClC,UAAM,gBAAgB,iBAAiB,QAAQ,SAAS,KAAK,QAAQ;AACrE,WAAO,KAAK,qBAAqB,SAAS,eAAe,QAAQ,CAAC;AAAA,EACpE;AAEA,SAAO;AACT;AAKA,SAAS,qBACP,SACA,eACA,UACiB;AACjB,QAAM,OAAO,QAAQ,QAAQ,QAAQ;AACrC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,UAAU,QAAQ,SAAS,UAAU,aAAa;AAAA,IAC3D,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,IACV;AAAA,IACA,WAAW,QAAQ;AAAA,IACnB,WAAW,QAAQ;AAAA,EACrB;AACF;AAYO,SAAS,mBAAmB,WAAmB,aAA4C;AAChG,QAAM,gBAAgB,eAAe,SAAS;AAC9C,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,YAAY,aAAa;AAC1C,SAAO,CAAC,YAAY,aAAa;AACnC;AAWO,SAAS,qBACd,WACA,aACU;AACV,QAAM,gBAAgB,eAAe,SAAS;AAC9C,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAEA,SAAO,YAAY,aAAa,KAAK;AACvC;;;AC1GO,SAAS,eACd,SACA,aACmB;AACnB,QAAM,SAA4B,CAAC;AAGnC,MAAI,QAAQ,eAAe,SAAS;AAClC,UAAM,gBAAgB,YAAY;AAClC,QAAI,iBAAiB,kBAAkB,OAAO;AAC5C,aAAO,KAAK,iBAAiB,SAAS,aAAa,CAAC;AAAA,IACtD;AAAA,EACF;AAGA,MAAI,QAAQ,eAAe,eAAe;AACxC,UAAM,sBAAsB,YAAY;AACxC,QAAI,uBAAuB,wBAAwB,OAAO;AACxD,aAAO,KAAK,uBAAuB,SAAS,mBAAmB,CAAC;AAAA,IAClE;AAAA,EACF;AAGA,QAAM,WAAW,YAAY;AAC7B,MAAI,aAAa,UAAa,QAAQ,QAAQ,UAAU;AACtD,WAAO,KAAK,wBAAwB,SAAS,QAAQ,CAAC;AAAA,EACxD;AAEA,SAAO;AACT;AAKA,SAAS,iBAAiB,SAAwB,UAAqC;AACrF,QAAM,YAAY,QAAQ,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAC1D,QAAM,QAAyB;AAAA,IAC7B,MAAM;AAAA,IACN,SAAS,QAAQ,SAAS;AAAA,IAC1B,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,EACZ;AACA,MAAI,QAAQ,SAAS,QAAW;AAC9B,UAAM,OAAO,QAAQ;AAAA,EACvB;AACA,SAAO;AACT;AAKA,SAAS,uBAAuB,SAAwB,UAAqC;AAC3F,QAAM,QAAyB;AAAA,IAC7B,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,EACZ;AACA,MAAI,QAAQ,SAAS,QAAW;AAC9B,UAAM,OAAO,QAAQ;AAAA,EACvB;AACA,SAAO;AACT;AAKA,SAAS,wBAAwB,SAAwB,UAAmC;AAC1F,QAAM,QAAyB;AAAA,IAC7B,MAAM;AAAA,IACN,SAAS,iBAAiB,OAAO,QAAQ,KAAK,CAAC,qCAAqC,OAAO,QAAQ,CAAC;AAAA,IACpG,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AACA,MAAI,QAAQ,SAAS,QAAW;AAC9B,UAAM,OAAO,QAAQ;AAAA,EACvB;AACA,SAAO;AACT;AAWO,SAAS,oBACd,YACA,aACS;AACT,MAAI,eAAe,SAAS;AAC1B,UAAMC,YAAW,YAAY;AAC7B,WAAO,CAACA,aAAYA,cAAa;AAAA,EACnC;AAGA,QAAM,WAAW,YAAY;AAC7B,SAAO,CAAC,YAAY,aAAa;AACnC;AAWO,SAAS,sBAAsB,OAAe,aAAyC;AAC5F,QAAM,WAAW,YAAY;AAC7B,MAAI,aAAa,QAAW;AAC1B,WAAO;AAAA,EACT;AACA,SAAO,SAAS;AAClB;;;AC1GO,SAAS,qBACd,SACA,aACmB;AACnB,QAAM,SAA4B,CAAC;AAEnC,aAAW,UAAU,QAAQ,gBAAgB;AAC3C,UAAM,WAAW,YAAY,MAAM;AACnC,QAAI,YAAY,aAAa,OAAO;AAClC,aAAO,KAAK,uBAAuB,SAAS,QAAQ,QAAQ,CAAC;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO;AACT;AAKA,SAAS,uBACP,SACA,QACA,UACiB;AACjB,QAAM,OAAO,QAAQ,QAAQ,QAAQ;AACrC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,UAAU,QAAQ,SAAS,eAAe,MAAM;AAAA,IACzD,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,IACV;AAAA,IACA,WAAW,QAAQ;AAAA,EACrB;AACF;AAWO,SAAS,oBAAoB,OAA+C;AACjF,QAAM,UAAyB,CAAC;AAEhC,MAAI,MAAM,OAAO,MAAM,OAAW,SAAQ,KAAK,OAAO;AACtD,MAAI,MAAM,aAAa,MAAM,OAAW,SAAQ,KAAK,aAAa;AAClE,MAAI,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAE5D,MAAI,MAAM,KAAK,MAAM,UAAa,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAC1F,MAAI,MAAM,KAAK,MAAM,UAAa,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAC1F,MAAI,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAC5D,MAAI,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAE5D,SAAO;AACT;AAWO,SAAS,qBACd,QACA,aACS;AACT,QAAM,WAAW,YAAY,MAAM;AACnC,SAAO,CAAC,YAAY,aAAa;AACnC;AAWO,SAAS,uBACd,QACA,aACU;AACV,SAAO,YAAY,MAAM,KAAK;AAChC;;;ACtEO,SAAS,yBACd,UACA,UAAqC,CAAC,GACpB;AAClB,QAAM,cAAc,kBAAkB,QAAQ,WAAW;AACzD,QAAM,SAA4B,CAAC;AAGnC,eAAa,UAAU,aAAa,QAAQ,IAAI,CAAC;AAEjD,SAAO;AAAA,IACL,OAAO,CAAC,OAAO,KAAK,CAAC,UAAU,MAAM,aAAa,OAAO;AAAA,IACzD;AAAA,EACF;AACF;AAWO,SAAS,iBACd,UACA,UAAqC,CAAC,GACpB;AAClB,SAAO,yBAAyB,SAAS,UAAU,OAAO;AAC5D;AAKA,SAAS,aACP,UACA,aACA,QACA,YACA,OACM;AACN,aAAW,WAAW,UAAU;AAC9B,UAAM,cAAc;AAEpB,QAAI,QAAQ,UAAU,SAAS;AAC7B,oBAAc,SAAS,aAAa,QAAQ,aAAa,KAAK;AAAA,IAChE,WAAW,QAAQ,UAAU,SAAS;AACpC,oBAAc,SAAS,aAAa,QAAQ,aAAa,KAAK;AAAA,IAChE,OAAO;AAEL,0BAAoB,SAAS,aAAa,QAAQ,aAAa,KAAK;AAAA,IACtE;AAAA,EACF;AACF;AAKA,SAAS,cACP,OACA,aACA,QACA,YACA,OACM;AACN,QAAM,YAAY,aAAa,GAAG,UAAU,IAAI,MAAM,IAAI,KAAK,MAAM;AAGrE,QAAM,kBAAkB;AAAA,IACtB;AAAA,MACE,WAAW,MAAM;AAAA,MACjB,WAAW,MAAM;AAAA,MACjB,MAAM;AAAA,IACR;AAAA,IACA,YAAY;AAAA,EACd;AACA,SAAO,KAAK,GAAG,eAAe;AAG9B,QAAM,iBAAiB,oBAAoB,KAA2C;AACtF,MAAI,eAAe,SAAS,GAAG;AAC7B,UAAM,eAAe;AAAA,MACnB;AAAA,QACE,WAAW,MAAM;AAAA,QACjB;AAAA,QACA,MAAM;AAAA,MACR;AAAA,MACA,YAAY;AAAA,IACd;AACA,WAAO,KAAK,GAAG,YAAY;AAAA,EAC7B;AAGA,MAAI,MAAM,WAAW,WAAW,MAAM,WAAW,UAAU;AACzD,UAAM,eAAe;AAAA,MACnB;AAAA,QACE,YAAY;AAAA;AAAA,QACZ,OAAO,QAAQ;AAAA,QACf,MAAM;AAAA,MACR;AAAA,MACA,YAAY;AAAA,IACd;AAEA,WAAO,KAAK,GAAG,aAAa,OAAO,CAAC,UAAU,MAAM,SAAS,wBAAwB,CAAC;AAGtF,QAAI,MAAM,WAAW,WAAW,WAAW,OAAO;AAChD;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,GAAG,SAAS;AAAA,QACZ,QAAQ;AAAA,MACV;AAAA,IACF,WAAW,gBAAgB,OAAO;AAEhC;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AAKA,SAAS,cACP,OAKA,aACA,QACA,YACA,OACM;AACN,QAAM,YAAY,aAAa,GAAG,UAAU,WAAW,MAAM,KAAK,MAAM,UAAU,MAAM,KAAK;AAG7F,QAAM,eAAe;AAAA,IACnB;AAAA,MACE,YAAY;AAAA,MACZ,OAAO,MAAM;AAAA,MACb;AAAA,MACA,MAAM;AAAA,IACR;AAAA,IACA,YAAY;AAAA,EACd;AACA,SAAO,KAAK,GAAG,YAAY;AAG3B,eAAa,MAAM,UAAU,aAAa,QAAQ,YAAY,KAAK;AACrE;AAKA,SAAS,oBACP,aAMA,aACA,QACA,YACA,OACM;AACN,QAAM,WAAW,aACb,GAAG,UAAU,UAAU,YAAY,KAAK,IAAI,OAAO,YAAY,KAAK,CAAC,MACrE,SAAS,YAAY,KAAK,IAAI,OAAO,YAAY,KAAK,CAAC;AAG3D,QAAM,eAAe;AAAA,IACnB;AAAA,MACE,YAAY;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,IACR;AAAA,IACA,YAAY;AAAA,EACd;AACA,SAAO,KAAK,GAAG,YAAY;AAG3B,eAAa,YAAY,UAAU,aAAa,QAAQ,YAAY,KAAK;AAC3E;","names":["parseYaml","severity"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/loader.ts","../src/defaults.ts","../src/validators/field-types.ts","../src/validators/layout.ts","../src/validators/field-options.ts","../src/validators/formspec.ts"],"sourcesContent":["/**\n * \\@formspec/constraints\n *\n * Constraint validation for FormSpec - restrict features based on target\n * environment capabilities.\n *\n * This package provides:\n * - Type definitions for constraint configuration\n * - YAML/TypeScript config file loading\n * - Core validation logic for FormSpec elements\n * - JSON Schema for .formspec.yml validation\n *\n * @example\n * ```ts\n * import { loadConfig, validateFormSpecElements } from '@formspec/constraints';\n * import { formspec, field } from '@formspec/dsl';\n *\n * // Load constraints from .formspec.yml\n * const { config } = await loadConfig();\n *\n * // Create a form\n * const form = formspec(\n * field.text(\"name\"),\n * field.dynamicEnum(\"country\", \"countries\"),\n * );\n *\n * // Validate against constraints\n * const result = validateFormSpecElements(form.elements, { constraints: config });\n *\n * if (!result.valid) {\n * console.error('Validation failed:', result.issues);\n * }\n * ```\n *\n * @packageDocumentation\n */\n\n// Types\nexport type {\n Severity,\n FieldTypeConstraints,\n LayoutConstraints,\n LayoutTypeConstraints,\n RuleEffectConstraints,\n RuleConstraints,\n UISchemaConstraints,\n FieldOptionConstraints,\n ControlOptionConstraints,\n ConstraintConfig,\n ResolvedConstraintConfig,\n ResolvedUISchemaConstraints,\n ResolvedRuleConstraints,\n FormSpecConfig,\n ValidationIssue,\n ValidationResult,\n} from \"./types.js\";\nexport type {\n AnyField,\n ArrayField,\n BooleanField,\n Conditional,\n DynamicEnumField,\n DynamicSchemaField,\n EnumOption,\n EnumOptionValue,\n FormElement,\n FormSpec,\n Group,\n NumberField,\n ObjectField,\n StaticEnumField,\n TextField,\n} from \"@formspec/core\";\n\n// Config loading\nexport {\n loadConfig,\n loadConfigFromString,\n defineConstraints,\n type LoadConfigOptions,\n type LoadConfigResult,\n} from \"./loader.js\";\n\n// Defaults\nexport { DEFAULT_CONSTRAINTS, DEFAULT_CONFIG, mergeWithDefaults } from \"./defaults.js\";\n\n// Validators\nexport {\n validateFormSpecElements,\n validateFormSpec,\n type FormSpecValidationOptions,\n} from \"./validators/formspec.js\";\n\nexport {\n validateFieldTypes,\n isFieldTypeAllowed,\n getFieldTypeSeverity,\n type FieldTypeContext,\n} from \"./validators/field-types.js\";\n\nexport {\n validateLayout,\n isLayoutTypeAllowed,\n isNestingDepthAllowed,\n type LayoutContext,\n} from \"./validators/layout.js\";\n\nexport {\n validateFieldOptions,\n extractFieldOptions,\n isFieldOptionAllowed,\n getFieldOptionSeverity,\n type FieldOptionsContext,\n type FieldOption,\n} from \"./validators/field-options.js\";\n","import { readFile } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport type { FormSpecConfig, ConstraintConfig, ResolvedConstraintConfig } from \"./types.js\";\nimport { mergeWithDefaults } from \"./defaults.js\";\n\n/**\n * Default config file names to search for (in order of priority).\n */\nconst CONFIG_FILE_NAMES = [\".formspec.yml\", \".formspec.yaml\", \"formspec.yml\"];\n\n/**\n * Options for loading configuration.\n *\n * @public\n */\nexport interface LoadConfigOptions {\n /**\n * The directory to search for config files.\n * Defaults to process.cwd().\n */\n cwd?: string;\n\n /**\n * Explicit path to a config file.\n * If provided, skips searching for default config file names.\n */\n configPath?: string;\n\n /**\n * Whether to search parent directories for config files.\n * Defaults to true.\n */\n searchParents?: boolean;\n}\n\n/**\n * Result of loading configuration.\n *\n * @public\n */\nexport interface LoadConfigResult {\n /** The loaded and merged configuration */\n config: ResolvedConstraintConfig;\n /** The path to the config file that was loaded (if any) */\n configPath: string | null;\n /** Whether a config file was found */\n found: boolean;\n}\n\n/**\n * Searches for a config file in the given directory and optionally parent directories.\n */\nasync function findConfigFile(startDir: string, searchParents: boolean): Promise<string | null> {\n let currentDir = resolve(startDir);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop with break conditions\n while (true) {\n for (const fileName of CONFIG_FILE_NAMES) {\n const filePath = resolve(currentDir, fileName);\n try {\n await readFile(filePath);\n return filePath;\n } catch {\n // File doesn't exist, continue searching\n }\n }\n\n if (!searchParents) {\n break;\n }\n\n const parentDir = dirname(currentDir);\n // Reached filesystem root when dirname returns same path\n if (parentDir === currentDir) {\n break;\n }\n currentDir = parentDir;\n }\n\n return null;\n}\n\n/**\n * Parses a YAML config file and returns the FormSpecConfig.\n */\nasync function parseConfigFile(filePath: string): Promise<FormSpecConfig> {\n const content = await readFile(filePath, \"utf-8\");\n const parsed = parseYaml(content) as unknown;\n\n if (parsed === null || parsed === undefined) {\n return {};\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config file at ${filePath}: expected an object, got ${typeof parsed}`);\n }\n\n return parsed as FormSpecConfig;\n}\n\n/**\n * Loads FormSpec constraint configuration from a .formspec.yml file.\n *\n * @param options - Options for loading configuration\n * @returns The loaded configuration with defaults applied\n *\n * @example\n * ```ts\n * // Load from current directory (searches for .formspec.yml)\n * const result = await loadConfig();\n *\n * // Load from specific directory\n * const result = await loadConfig({ cwd: '/path/to/project' });\n *\n * // Load from specific file\n * const result = await loadConfig({ configPath: '/path/to/config.yml' });\n * ```\n *\n * @public\n */\nexport async function loadConfig(options: LoadConfigOptions = {}): Promise<LoadConfigResult> {\n const { cwd = process.cwd(), configPath, searchParents = true } = options;\n\n let resolvedPath: string | null = null;\n\n if (configPath) {\n resolvedPath = resolve(cwd, configPath);\n try {\n await readFile(resolvedPath);\n } catch {\n throw new Error(`Config file not found at ${resolvedPath}`);\n }\n } else {\n resolvedPath = await findConfigFile(cwd, searchParents);\n }\n\n if (!resolvedPath) {\n return {\n config: mergeWithDefaults(undefined),\n configPath: null,\n found: false,\n };\n }\n\n const fileConfig = await parseConfigFile(resolvedPath);\n const config = mergeWithDefaults(fileConfig.constraints);\n\n return {\n config,\n configPath: resolvedPath,\n found: true,\n };\n}\n\n/**\n * Synchronously loads config from a pre-parsed YAML string.\n * Useful for testing or when config is already available.\n *\n * @param yamlContent - The YAML content to parse\n * @returns The parsed and merged configuration\n *\n * @public\n */\nexport function loadConfigFromString(yamlContent: string): ResolvedConstraintConfig {\n const parsed = parseYaml(yamlContent) as FormSpecConfig | null | undefined;\n\n if (parsed === null || parsed === undefined) {\n return mergeWithDefaults(undefined);\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config content: expected an object, got ${typeof parsed}`);\n }\n\n return mergeWithDefaults(parsed.constraints);\n}\n\n/**\n * Creates a constraint configuration directly from an object.\n * Useful for programmatic configuration without YAML.\n *\n * @param config - Partial constraint configuration\n * @returns Complete configuration with defaults applied\n *\n * @example\n * ```ts\n * const config = defineConstraints({\n * fieldTypes: {\n * dynamicEnum: 'error',\n * dynamicSchema: 'error',\n * },\n * layout: {\n * group: 'error',\n * },\n * });\n * ```\n *\n * @public\n */\nexport function defineConstraints(config: ConstraintConfig): ResolvedConstraintConfig {\n return mergeWithDefaults(config);\n}\n","import type { ConstraintConfig, FormSpecConfig, ResolvedConstraintConfig } from \"./types.js\";\n\n/**\n * Default constraint configuration that allows all features.\n * All constraints default to \"off\" (allowed).\n *\n * @beta\n */\nexport const DEFAULT_CONSTRAINTS: ResolvedConstraintConfig = {\n fieldTypes: {\n text: \"off\",\n number: \"off\",\n boolean: \"off\",\n staticEnum: \"off\",\n dynamicEnum: \"off\",\n dynamicSchema: \"off\",\n array: \"off\",\n object: \"off\",\n },\n layout: {\n group: \"off\",\n conditionals: \"off\",\n maxNestingDepth: Infinity,\n },\n uiSchema: {\n layouts: {\n VerticalLayout: \"off\",\n HorizontalLayout: \"off\",\n Group: \"off\",\n Categorization: \"off\",\n Category: \"off\",\n },\n rules: {\n enabled: \"off\",\n effects: {\n SHOW: \"off\",\n HIDE: \"off\",\n ENABLE: \"off\",\n DISABLE: \"off\",\n },\n },\n },\n fieldOptions: {\n label: \"off\",\n placeholder: \"off\",\n required: \"off\",\n minValue: \"off\",\n maxValue: \"off\",\n minItems: \"off\",\n maxItems: \"off\",\n },\n controlOptions: {\n format: \"off\",\n readonly: \"off\",\n multi: \"off\",\n showUnfocusedDescription: \"off\",\n hideRequiredAsterisk: \"off\",\n custom: {},\n },\n};\n\n/**\n * Default FormSpec configuration.\n *\n * @beta\n */\nexport const DEFAULT_CONFIG: FormSpecConfig = {\n constraints: DEFAULT_CONSTRAINTS,\n};\n\n/**\n * Merges user constraints with defaults, filling in any missing values.\n *\n * @beta\n */\nexport function mergeWithDefaults(config: ConstraintConfig | undefined): ResolvedConstraintConfig {\n if (!config) {\n return DEFAULT_CONSTRAINTS;\n }\n\n return {\n fieldTypes: {\n ...DEFAULT_CONSTRAINTS.fieldTypes,\n ...config.fieldTypes,\n },\n layout: {\n ...DEFAULT_CONSTRAINTS.layout,\n ...config.layout,\n },\n uiSchema: {\n layouts: {\n ...DEFAULT_CONSTRAINTS.uiSchema.layouts,\n ...config.uiSchema?.layouts,\n },\n rules: {\n enabled: config.uiSchema?.rules?.enabled ?? DEFAULT_CONSTRAINTS.uiSchema.rules.enabled,\n effects: {\n ...DEFAULT_CONSTRAINTS.uiSchema.rules.effects,\n ...config.uiSchema?.rules?.effects,\n },\n },\n },\n fieldOptions: {\n ...DEFAULT_CONSTRAINTS.fieldOptions,\n ...config.fieldOptions,\n },\n controlOptions: {\n ...DEFAULT_CONSTRAINTS.controlOptions,\n ...config.controlOptions,\n custom: {\n ...DEFAULT_CONSTRAINTS.controlOptions.custom,\n ...config.controlOptions?.custom,\n },\n },\n };\n}\n","import type { FieldTypeConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Maps FormSpec field._field values to constraint config keys.\n */\nconst FIELD_TYPE_MAP: Record<string, keyof FieldTypeConstraints> = {\n text: \"text\",\n number: \"number\",\n boolean: \"boolean\",\n enum: \"staticEnum\",\n dynamic_enum: \"dynamicEnum\",\n dynamic_schema: \"dynamicSchema\",\n array: \"array\",\n object: \"object\",\n};\n\n/**\n * Human-readable names for field types.\n */\nconst FIELD_TYPE_NAMES: Record<string, string> = {\n text: \"text field\",\n number: \"number field\",\n boolean: \"boolean field\",\n enum: \"static enum field\",\n dynamic_enum: \"dynamic enum field\",\n dynamic_schema: \"dynamic schema field\",\n array: \"array field\",\n object: \"object field\",\n};\n\n/**\n * Context for field type validation.\n *\n * @beta\n */\nexport interface FieldTypeContext {\n /** The _field discriminator value (e.g., \"text\", \"number\", \"enum\") */\n fieldType: string;\n /** The field name */\n fieldName: string;\n /** Optional path for nested fields */\n path?: string;\n}\n\n/**\n * Validates a field type against constraints.\n *\n * @param context - Information about the field being validated\n * @param constraints - Field type constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @beta\n */\nexport function validateFieldTypes(\n context: FieldTypeContext,\n constraints: FieldTypeConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n const constraintKey = FIELD_TYPE_MAP[context.fieldType];\n if (!constraintKey) {\n // Unknown field type, skip validation\n return issues;\n }\n\n const severity = constraints[constraintKey];\n if (severity && severity !== \"off\") {\n const fieldTypeName = FIELD_TYPE_NAMES[context.fieldType] ?? context.fieldType;\n issues.push(createFieldTypeIssue(context, fieldTypeName, severity));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field type.\n */\nfunction createFieldTypeIssue(\n context: FieldTypeContext,\n fieldTypeName: string,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_TYPE\",\n message: `Field \"${context.fieldName}\" uses ${fieldTypeName}, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldTypes\",\n path,\n fieldName: context.fieldName,\n fieldType: context.fieldType,\n };\n}\n\n/**\n * Checks if a field type is allowed by the constraints.\n * Useful for quick checks without generating issues.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns true if allowed, false if disallowed\n *\n * @beta\n */\nexport function isFieldTypeAllowed(fieldType: string, constraints: FieldTypeConstraints): boolean {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return true; // Unknown types are allowed by default\n }\n\n const severity = constraints[constraintKey];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field type.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @beta\n */\nexport function getFieldTypeSeverity(\n fieldType: string,\n constraints: FieldTypeConstraints\n): Severity {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return \"off\";\n }\n\n return constraints[constraintKey] ?? \"off\";\n}\n","import type { LayoutConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Context for layout validation.\n *\n * @beta\n */\nexport interface LayoutContext {\n /** The type of layout element (\"group\" | \"conditional\") */\n layoutType: \"group\" | \"conditional\";\n /** Optional label for the element (for groups) */\n label?: string;\n /** Current nesting depth */\n depth: number;\n /** Path to this element */\n path?: string;\n}\n\n/**\n * Validates a layout element against constraints.\n *\n * @param context - Information about the layout element\n * @param constraints - Layout constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @beta\n */\nexport function validateLayout(\n context: LayoutContext,\n constraints: LayoutConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n // Check if groups are allowed\n if (context.layoutType === \"group\") {\n const groupSeverity = constraints.group;\n if (groupSeverity && groupSeverity !== \"off\") {\n issues.push(createGroupIssue(context, groupSeverity));\n }\n }\n\n // Check if conditionals are allowed\n if (context.layoutType === \"conditional\") {\n const conditionalSeverity = constraints.conditionals;\n if (conditionalSeverity && conditionalSeverity !== \"off\") {\n issues.push(createConditionalIssue(context, conditionalSeverity));\n }\n }\n\n // Check nesting depth (applies to both groups and fields within nested structures)\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth !== undefined && context.depth > maxDepth) {\n issues.push(createNestingDepthIssue(context, maxDepth));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed group.\n */\nfunction createGroupIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const labelInfo = context.label ? ` \"${context.label}\"` : \"\";\n const issue: ValidationIssue = {\n code: \"DISALLOWED_GROUP\",\n message: `Group${labelInfo} is not allowed - visual grouping is not supported in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for a disallowed conditional.\n */\nfunction createConditionalIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"DISALLOWED_CONDITIONAL\",\n message: `Conditional visibility (when/is) is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for exceeding nesting depth.\n */\nfunction createNestingDepthIssue(context: LayoutContext, maxDepth: number): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"EXCEEDED_NESTING_DEPTH\",\n message: `Nesting depth ${String(context.depth)} exceeds maximum allowed depth of ${String(maxDepth)}`,\n severity: \"error\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Checks if a layout type is allowed by the constraints.\n *\n * @param layoutType - The type of layout element\n * @param constraints - Layout constraints\n * @returns true if allowed, false if disallowed\n *\n * @beta\n */\nexport function isLayoutTypeAllowed(\n layoutType: \"group\" | \"conditional\",\n constraints: LayoutConstraints\n): boolean {\n if (layoutType === \"group\") {\n const severity = constraints.group;\n return !severity || severity === \"off\";\n }\n\n // layoutType === \"conditional\"\n const severity = constraints.conditionals;\n return !severity || severity === \"off\";\n}\n\n/**\n * Checks if a nesting depth is allowed.\n *\n * @param depth - Current nesting depth\n * @param constraints - Layout constraints\n * @returns true if allowed, false if exceeds limit\n *\n * @beta\n */\nexport function isNestingDepthAllowed(depth: number, constraints: LayoutConstraints): boolean {\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth === undefined) {\n return true;\n }\n return depth <= maxDepth;\n}\n","import type { FieldOptionConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Known field options that can be validated.\n *\n * @beta\n */\nexport type FieldOption =\n | \"label\"\n | \"placeholder\"\n | \"required\"\n | \"minValue\"\n | \"maxValue\"\n | \"minItems\"\n | \"maxItems\";\n\n/**\n * Context for field option validation.\n *\n * @beta\n */\nexport interface FieldOptionsContext {\n /** The field name */\n fieldName: string;\n /** Which options are present on this field */\n presentOptions: FieldOption[];\n /** Path to this field */\n path?: string;\n}\n\n/**\n * Validates field options against constraints.\n *\n * @param context - Information about the field and its options\n * @param constraints - Field option constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @beta\n */\nexport function validateFieldOptions(\n context: FieldOptionsContext,\n constraints: FieldOptionConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n for (const option of context.presentOptions) {\n const severity = constraints[option];\n if (severity && severity !== \"off\") {\n issues.push(createFieldOptionIssue(context, option, severity));\n }\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field option.\n */\nfunction createFieldOptionIssue(\n context: FieldOptionsContext,\n option: FieldOption,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_OPTION\",\n message: `Field \"${context.fieldName}\" uses the \"${option}\" option, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldOptions\",\n path,\n fieldName: context.fieldName,\n };\n}\n\n/**\n * Extracts which options are present on a field object.\n * Works with FormSpec field types.\n *\n * @param field - A field object with potential options\n * @returns Array of present option names\n *\n * @beta\n */\nexport function extractFieldOptions(field: Record<string, unknown>): FieldOption[] {\n const options: FieldOption[] = [];\n\n if (field[\"label\"] !== undefined) options.push(\"label\");\n if (field[\"placeholder\"] !== undefined) options.push(\"placeholder\");\n if (field[\"required\"] !== undefined) options.push(\"required\");\n // NumberField uses \"min\"/\"max\" in core types, map to \"minValue\"/\"maxValue\" constraints\n if (field[\"min\"] !== undefined || field[\"minValue\"] !== undefined) options.push(\"minValue\");\n if (field[\"max\"] !== undefined || field[\"maxValue\"] !== undefined) options.push(\"maxValue\");\n if (field[\"minItems\"] !== undefined) options.push(\"minItems\");\n if (field[\"maxItems\"] !== undefined) options.push(\"maxItems\");\n\n return options;\n}\n\n/**\n * Checks if a specific field option is allowed.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns true if allowed, false if disallowed\n *\n * @beta\n */\nexport function isFieldOptionAllowed(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): boolean {\n const severity = constraints[option];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field option.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @beta\n */\nexport function getFieldOptionSeverity(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): Severity {\n return constraints[option] ?? \"off\";\n}\n","import type { FormElement, FormSpec, AnyField } from \"@formspec/core\";\nimport type {\n ConstraintConfig,\n ResolvedConstraintConfig,\n ValidationIssue,\n ValidationResult,\n} from \"../types.js\";\nimport { mergeWithDefaults } from \"../defaults.js\";\nimport { validateFieldTypes } from \"./field-types.js\";\nimport { validateLayout } from \"./layout.js\";\nimport { validateFieldOptions, extractFieldOptions } from \"./field-options.js\";\n\n/**\n * Options for validating FormSpec elements.\n *\n * @public\n */\nexport interface FormSpecValidationOptions {\n /** Constraint configuration (will be merged with defaults) */\n constraints?: ConstraintConfig;\n}\n\n/**\n * Validates FormSpec elements against constraints.\n *\n * This is the main entry point for validating a form specification\n * against a constraint configuration. It walks through all elements\n * and checks each one against the configured constraints.\n *\n * @param elements - FormSpec elements to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @example\n * ```ts\n * import { formspec, field, group } from '@formspec/dsl';\n * import { validateFormSpecElements, defineConstraints } from '@formspec/constraints';\n *\n * const form = formspec(\n * group(\"Contact\",\n * field.text(\"name\"),\n * field.dynamicEnum(\"country\", \"countries\"),\n * ),\n * );\n *\n * const result = validateFormSpecElements(form.elements, {\n * constraints: {\n * fieldTypes: { dynamicEnum: 'error' },\n * layout: { group: 'error' },\n * },\n * });\n *\n * if (!result.valid) {\n * console.error('Validation failed:', result.issues);\n * }\n * ```\n *\n * @public\n */\nexport function validateFormSpecElements(\n elements: readonly FormElement[],\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n const constraints = mergeWithDefaults(options.constraints);\n const issues: ValidationIssue[] = [];\n\n // Walk through all elements\n walkElements(elements, constraints, issues, \"\", 0);\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n}\n\n/**\n * Validates a complete FormSpec against constraints.\n *\n * @param formSpec - The FormSpec to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @public\n */\nexport function validateFormSpec(\n formSpec: FormSpec<readonly FormElement[]>,\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n return validateFormSpecElements(formSpec.elements, options);\n}\n\n/**\n * Recursively walks through FormSpec elements and validates each one.\n */\nfunction walkElements(\n elements: readonly FormElement[],\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n for (const element of elements) {\n const elementPath = pathPrefix;\n\n if (element._type === \"field\") {\n validateField(element, constraints, issues, elementPath, depth);\n } else if (element._type === \"group\") {\n validateGroup(element, constraints, issues, elementPath, depth);\n } else {\n // element._type === \"conditional\"\n validateConditional(element, constraints, issues, elementPath, depth);\n }\n }\n}\n\n/**\n * Validates a field element.\n */\nfunction validateField(\n field: AnyField,\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const fieldPath = pathPrefix ? `${pathPrefix}/${field.name}` : field.name;\n\n // Validate field type\n const fieldTypeIssues = validateFieldTypes(\n {\n fieldType: field._field,\n fieldName: field.name,\n path: fieldPath,\n },\n constraints.fieldTypes\n );\n issues.push(...fieldTypeIssues);\n\n // Validate field options\n const presentOptions = extractFieldOptions(field as unknown as Record<string, unknown>);\n if (presentOptions.length > 0) {\n const optionIssues = validateFieldOptions(\n {\n fieldName: field.name,\n presentOptions,\n path: fieldPath,\n },\n constraints.fieldOptions\n );\n issues.push(...optionIssues);\n }\n\n // Check nesting depth for array/object fields\n if (field._field === \"array\" || field._field === \"object\") {\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\", // Arrays/objects contribute to nesting depth\n depth: depth + 1,\n path: fieldPath,\n },\n constraints.layout\n );\n // Only add nesting depth issues, not group issues\n issues.push(...layoutIssues.filter((issue) => issue.code === \"EXCEEDED_NESTING_DEPTH\"));\n\n // Recursively validate nested elements\n if (field._field === \"array\" && \"items\" in field) {\n walkElements(\n field.items as readonly FormElement[],\n constraints,\n issues,\n `${fieldPath}[]`,\n depth + 1\n );\n } else if (\"properties\" in field) {\n // field._field === \"object\"\n walkElements(\n field.properties as readonly FormElement[],\n constraints,\n issues,\n fieldPath,\n depth + 1\n );\n }\n }\n}\n\n/**\n * Validates a group element.\n */\nfunction validateGroup(\n group: {\n readonly _type: \"group\";\n readonly label: string;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const groupPath = pathPrefix ? `${pathPrefix}/[group:${group.label}]` : `[group:${group.label}]`;\n\n // Validate group usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\",\n label: group.label,\n depth,\n path: groupPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements (groups don't increase nesting depth for schema)\n walkElements(group.elements, constraints, issues, pathPrefix, depth);\n}\n\n/**\n * Validates a conditional element.\n */\nfunction validateConditional(\n conditional: {\n readonly _type: \"conditional\";\n readonly field: string;\n readonly value: unknown;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const condPath = pathPrefix\n ? `${pathPrefix}/[when:${conditional.field}=${String(conditional.value)}]`\n : `[when:${conditional.field}=${String(conditional.value)}]`;\n\n // Validate conditional usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"conditional\",\n depth,\n path: condPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements\n walkElements(conditional.elements, constraints, issues, pathPrefix, depth);\n}\n"],"mappings":"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","names":["parseYaml","severity"]}
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@
35
35
  * @packageDocumentation
36
36
  */
37
37
  export type { Severity, FieldTypeConstraints, LayoutConstraints, LayoutTypeConstraints, RuleEffectConstraints, RuleConstraints, UISchemaConstraints, FieldOptionConstraints, ControlOptionConstraints, ConstraintConfig, ResolvedConstraintConfig, ResolvedUISchemaConstraints, ResolvedRuleConstraints, FormSpecConfig, ValidationIssue, ValidationResult, } from "./types.js";
38
+ export type { AnyField, ArrayField, BooleanField, Conditional, DynamicEnumField, DynamicSchemaField, EnumOption, EnumOptionValue, FormElement, FormSpec, Group, NumberField, ObjectField, StaticEnumField, TextField, } from "@formspec/core";
38
39
  export { loadConfig, loadConfigFromString, defineConstraints, type LoadConfigOptions, type LoadConfigResult, } from "./loader.js";
39
40
  export { DEFAULT_CONSTRAINTS, DEFAULT_CONFIG, mergeWithDefaults } from "./defaults.js";
40
41
  export { validateFormSpecElements, validateFormSpec, type FormSpecValidationOptions, } from "./validators/formspec.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,YAAY,EACV,QAAQ,EACR,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGvF,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,gBAAgB,GACtB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,YAAY,EACV,QAAQ,EACR,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,eAAe,EACf,WAAW,EACX,QAAQ,EACR,KAAK,EACL,WAAW,EACX,WAAW,EACX,eAAe,EACf,SAAS,GACV,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGvF,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,gBAAgB,GACtB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,+BAA+B,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/loader.ts","../src/defaults.ts","../src/validators/field-types.ts","../src/validators/layout.ts","../src/validators/field-options.ts","../src/validators/formspec.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport type { FormSpecConfig, ConstraintConfig, ResolvedConstraintConfig } from \"./types.js\";\nimport { mergeWithDefaults } from \"./defaults.js\";\n\n/**\n * Default config file names to search for (in order of priority).\n */\nconst CONFIG_FILE_NAMES = [\".formspec.yml\", \".formspec.yaml\", \"formspec.yml\"];\n\n/**\n * Options for loading configuration.\n *\n * @public\n */\nexport interface LoadConfigOptions {\n /**\n * The directory to search for config files.\n * Defaults to process.cwd().\n */\n cwd?: string;\n\n /**\n * Explicit path to a config file.\n * If provided, skips searching for default config file names.\n */\n configPath?: string;\n\n /**\n * Whether to search parent directories for config files.\n * Defaults to true.\n */\n searchParents?: boolean;\n}\n\n/**\n * Result of loading configuration.\n *\n * @public\n */\nexport interface LoadConfigResult {\n /** The loaded and merged configuration */\n config: ResolvedConstraintConfig;\n /** The path to the config file that was loaded (if any) */\n configPath: string | null;\n /** Whether a config file was found */\n found: boolean;\n}\n\n/**\n * Searches for a config file in the given directory and optionally parent directories.\n */\nasync function findConfigFile(startDir: string, searchParents: boolean): Promise<string | null> {\n let currentDir = resolve(startDir);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop with break conditions\n while (true) {\n for (const fileName of CONFIG_FILE_NAMES) {\n const filePath = resolve(currentDir, fileName);\n try {\n await readFile(filePath);\n return filePath;\n } catch {\n // File doesn't exist, continue searching\n }\n }\n\n if (!searchParents) {\n break;\n }\n\n const parentDir = dirname(currentDir);\n // Reached filesystem root when dirname returns same path\n if (parentDir === currentDir) {\n break;\n }\n currentDir = parentDir;\n }\n\n return null;\n}\n\n/**\n * Parses a YAML config file and returns the FormSpecConfig.\n */\nasync function parseConfigFile(filePath: string): Promise<FormSpecConfig> {\n const content = await readFile(filePath, \"utf-8\");\n const parsed = parseYaml(content) as unknown;\n\n if (parsed === null || parsed === undefined) {\n return {};\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config file at ${filePath}: expected an object, got ${typeof parsed}`);\n }\n\n return parsed as FormSpecConfig;\n}\n\n/**\n * Loads FormSpec constraint configuration from a .formspec.yml file.\n *\n * @param options - Options for loading configuration\n * @returns The loaded configuration with defaults applied\n *\n * @example\n * ```ts\n * // Load from current directory (searches for .formspec.yml)\n * const result = await loadConfig();\n *\n * // Load from specific directory\n * const result = await loadConfig({ cwd: '/path/to/project' });\n *\n * // Load from specific file\n * const result = await loadConfig({ configPath: '/path/to/config.yml' });\n * ```\n *\n * @public\n */\nexport async function loadConfig(options: LoadConfigOptions = {}): Promise<LoadConfigResult> {\n const { cwd = process.cwd(), configPath, searchParents = true } = options;\n\n let resolvedPath: string | null = null;\n\n if (configPath) {\n resolvedPath = resolve(cwd, configPath);\n try {\n await readFile(resolvedPath);\n } catch {\n throw new Error(`Config file not found at ${resolvedPath}`);\n }\n } else {\n resolvedPath = await findConfigFile(cwd, searchParents);\n }\n\n if (!resolvedPath) {\n return {\n config: mergeWithDefaults(undefined),\n configPath: null,\n found: false,\n };\n }\n\n const fileConfig = await parseConfigFile(resolvedPath);\n const config = mergeWithDefaults(fileConfig.constraints);\n\n return {\n config,\n configPath: resolvedPath,\n found: true,\n };\n}\n\n/**\n * Synchronously loads config from a pre-parsed YAML string.\n * Useful for testing or when config is already available.\n *\n * @param yamlContent - The YAML content to parse\n * @returns The parsed and merged configuration\n *\n * @public\n */\nexport function loadConfigFromString(yamlContent: string): ResolvedConstraintConfig {\n const parsed = parseYaml(yamlContent) as FormSpecConfig | null | undefined;\n\n if (parsed === null || parsed === undefined) {\n return mergeWithDefaults(undefined);\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config content: expected an object, got ${typeof parsed}`);\n }\n\n return mergeWithDefaults(parsed.constraints);\n}\n\n/**\n * Creates a constraint configuration directly from an object.\n * Useful for programmatic configuration without YAML.\n *\n * @param config - Partial constraint configuration\n * @returns Complete configuration with defaults applied\n *\n * @example\n * ```ts\n * const config = defineConstraints({\n * fieldTypes: {\n * dynamicEnum: 'error',\n * dynamicSchema: 'error',\n * },\n * layout: {\n * group: 'error',\n * },\n * });\n * ```\n *\n * @public\n */\nexport function defineConstraints(config: ConstraintConfig): ResolvedConstraintConfig {\n return mergeWithDefaults(config);\n}\n","import type { ConstraintConfig, FormSpecConfig, ResolvedConstraintConfig } from \"./types.js\";\n\n/**\n * Default constraint configuration that allows all features.\n * All constraints default to \"off\" (allowed).\n *\n * @public\n */\nexport const DEFAULT_CONSTRAINTS: ResolvedConstraintConfig = {\n fieldTypes: {\n text: \"off\",\n number: \"off\",\n boolean: \"off\",\n staticEnum: \"off\",\n dynamicEnum: \"off\",\n dynamicSchema: \"off\",\n array: \"off\",\n object: \"off\",\n },\n layout: {\n group: \"off\",\n conditionals: \"off\",\n maxNestingDepth: Infinity,\n },\n uiSchema: {\n layouts: {\n VerticalLayout: \"off\",\n HorizontalLayout: \"off\",\n Group: \"off\",\n Categorization: \"off\",\n Category: \"off\",\n },\n rules: {\n enabled: \"off\",\n effects: {\n SHOW: \"off\",\n HIDE: \"off\",\n ENABLE: \"off\",\n DISABLE: \"off\",\n },\n },\n },\n fieldOptions: {\n label: \"off\",\n placeholder: \"off\",\n required: \"off\",\n minValue: \"off\",\n maxValue: \"off\",\n minItems: \"off\",\n maxItems: \"off\",\n },\n controlOptions: {\n format: \"off\",\n readonly: \"off\",\n multi: \"off\",\n showUnfocusedDescription: \"off\",\n hideRequiredAsterisk: \"off\",\n custom: {},\n },\n};\n\n/**\n * Default FormSpec configuration.\n *\n * @public\n */\nexport const DEFAULT_CONFIG: FormSpecConfig = {\n constraints: DEFAULT_CONSTRAINTS,\n};\n\n/**\n * Merges user constraints with defaults, filling in any missing values.\n *\n * @public\n */\nexport function mergeWithDefaults(config: ConstraintConfig | undefined): ResolvedConstraintConfig {\n if (!config) {\n return DEFAULT_CONSTRAINTS;\n }\n\n return {\n fieldTypes: {\n ...DEFAULT_CONSTRAINTS.fieldTypes,\n ...config.fieldTypes,\n },\n layout: {\n ...DEFAULT_CONSTRAINTS.layout,\n ...config.layout,\n },\n uiSchema: {\n layouts: {\n ...DEFAULT_CONSTRAINTS.uiSchema.layouts,\n ...config.uiSchema?.layouts,\n },\n rules: {\n enabled: config.uiSchema?.rules?.enabled ?? DEFAULT_CONSTRAINTS.uiSchema.rules.enabled,\n effects: {\n ...DEFAULT_CONSTRAINTS.uiSchema.rules.effects,\n ...config.uiSchema?.rules?.effects,\n },\n },\n },\n fieldOptions: {\n ...DEFAULT_CONSTRAINTS.fieldOptions,\n ...config.fieldOptions,\n },\n controlOptions: {\n ...DEFAULT_CONSTRAINTS.controlOptions,\n ...config.controlOptions,\n custom: {\n ...DEFAULT_CONSTRAINTS.controlOptions.custom,\n ...config.controlOptions?.custom,\n },\n },\n };\n}\n","import type { FieldTypeConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Maps FormSpec field._field values to constraint config keys.\n */\nconst FIELD_TYPE_MAP: Record<string, keyof FieldTypeConstraints> = {\n text: \"text\",\n number: \"number\",\n boolean: \"boolean\",\n enum: \"staticEnum\",\n dynamic_enum: \"dynamicEnum\",\n dynamic_schema: \"dynamicSchema\",\n array: \"array\",\n object: \"object\",\n};\n\n/**\n * Human-readable names for field types.\n */\nconst FIELD_TYPE_NAMES: Record<string, string> = {\n text: \"text field\",\n number: \"number field\",\n boolean: \"boolean field\",\n enum: \"static enum field\",\n dynamic_enum: \"dynamic enum field\",\n dynamic_schema: \"dynamic schema field\",\n array: \"array field\",\n object: \"object field\",\n};\n\n/**\n * Context for field type validation.\n *\n * @public\n */\nexport interface FieldTypeContext {\n /** The _field discriminator value (e.g., \"text\", \"number\", \"enum\") */\n fieldType: string;\n /** The field name */\n fieldName: string;\n /** Optional path for nested fields */\n path?: string;\n}\n\n/**\n * Validates a field type against constraints.\n *\n * @param context - Information about the field being validated\n * @param constraints - Field type constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @public\n */\nexport function validateFieldTypes(\n context: FieldTypeContext,\n constraints: FieldTypeConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n const constraintKey = FIELD_TYPE_MAP[context.fieldType];\n if (!constraintKey) {\n // Unknown field type, skip validation\n return issues;\n }\n\n const severity = constraints[constraintKey];\n if (severity && severity !== \"off\") {\n const fieldTypeName = FIELD_TYPE_NAMES[context.fieldType] ?? context.fieldType;\n issues.push(createFieldTypeIssue(context, fieldTypeName, severity));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field type.\n */\nfunction createFieldTypeIssue(\n context: FieldTypeContext,\n fieldTypeName: string,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_TYPE\",\n message: `Field \"${context.fieldName}\" uses ${fieldTypeName}, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldTypes\",\n path,\n fieldName: context.fieldName,\n fieldType: context.fieldType,\n };\n}\n\n/**\n * Checks if a field type is allowed by the constraints.\n * Useful for quick checks without generating issues.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns true if allowed, false if disallowed\n *\n * @public\n */\nexport function isFieldTypeAllowed(fieldType: string, constraints: FieldTypeConstraints): boolean {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return true; // Unknown types are allowed by default\n }\n\n const severity = constraints[constraintKey];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field type.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @public\n */\nexport function getFieldTypeSeverity(\n fieldType: string,\n constraints: FieldTypeConstraints\n): Severity {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return \"off\";\n }\n\n return constraints[constraintKey] ?? \"off\";\n}\n","import type { LayoutConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Context for layout validation.\n *\n * @public\n */\nexport interface LayoutContext {\n /** The type of layout element (\"group\" | \"conditional\") */\n layoutType: \"group\" | \"conditional\";\n /** Optional label for the element (for groups) */\n label?: string;\n /** Current nesting depth */\n depth: number;\n /** Path to this element */\n path?: string;\n}\n\n/**\n * Validates a layout element against constraints.\n *\n * @param context - Information about the layout element\n * @param constraints - Layout constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @public\n */\nexport function validateLayout(\n context: LayoutContext,\n constraints: LayoutConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n // Check if groups are allowed\n if (context.layoutType === \"group\") {\n const groupSeverity = constraints.group;\n if (groupSeverity && groupSeverity !== \"off\") {\n issues.push(createGroupIssue(context, groupSeverity));\n }\n }\n\n // Check if conditionals are allowed\n if (context.layoutType === \"conditional\") {\n const conditionalSeverity = constraints.conditionals;\n if (conditionalSeverity && conditionalSeverity !== \"off\") {\n issues.push(createConditionalIssue(context, conditionalSeverity));\n }\n }\n\n // Check nesting depth (applies to both groups and fields within nested structures)\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth !== undefined && context.depth > maxDepth) {\n issues.push(createNestingDepthIssue(context, maxDepth));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed group.\n */\nfunction createGroupIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const labelInfo = context.label ? ` \"${context.label}\"` : \"\";\n const issue: ValidationIssue = {\n code: \"DISALLOWED_GROUP\",\n message: `Group${labelInfo} is not allowed - visual grouping is not supported in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for a disallowed conditional.\n */\nfunction createConditionalIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"DISALLOWED_CONDITIONAL\",\n message: `Conditional visibility (when/is) is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for exceeding nesting depth.\n */\nfunction createNestingDepthIssue(context: LayoutContext, maxDepth: number): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"EXCEEDED_NESTING_DEPTH\",\n message: `Nesting depth ${String(context.depth)} exceeds maximum allowed depth of ${String(maxDepth)}`,\n severity: \"error\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Checks if a layout type is allowed by the constraints.\n *\n * @param layoutType - The type of layout element\n * @param constraints - Layout constraints\n * @returns true if allowed, false if disallowed\n *\n * @public\n */\nexport function isLayoutTypeAllowed(\n layoutType: \"group\" | \"conditional\",\n constraints: LayoutConstraints\n): boolean {\n if (layoutType === \"group\") {\n const severity = constraints.group;\n return !severity || severity === \"off\";\n }\n\n // layoutType === \"conditional\"\n const severity = constraints.conditionals;\n return !severity || severity === \"off\";\n}\n\n/**\n * Checks if a nesting depth is allowed.\n *\n * @param depth - Current nesting depth\n * @param constraints - Layout constraints\n * @returns true if allowed, false if exceeds limit\n *\n * @public\n */\nexport function isNestingDepthAllowed(depth: number, constraints: LayoutConstraints): boolean {\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth === undefined) {\n return true;\n }\n return depth <= maxDepth;\n}\n","import type { FieldOptionConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Known field options that can be validated.\n *\n * @public\n */\nexport type FieldOption =\n | \"label\"\n | \"placeholder\"\n | \"required\"\n | \"minValue\"\n | \"maxValue\"\n | \"minItems\"\n | \"maxItems\";\n\n/**\n * Context for field option validation.\n *\n * @public\n */\nexport interface FieldOptionsContext {\n /** The field name */\n fieldName: string;\n /** Which options are present on this field */\n presentOptions: FieldOption[];\n /** Path to this field */\n path?: string;\n}\n\n/**\n * Validates field options against constraints.\n *\n * @param context - Information about the field and its options\n * @param constraints - Field option constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @public\n */\nexport function validateFieldOptions(\n context: FieldOptionsContext,\n constraints: FieldOptionConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n for (const option of context.presentOptions) {\n const severity = constraints[option];\n if (severity && severity !== \"off\") {\n issues.push(createFieldOptionIssue(context, option, severity));\n }\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field option.\n */\nfunction createFieldOptionIssue(\n context: FieldOptionsContext,\n option: FieldOption,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_OPTION\",\n message: `Field \"${context.fieldName}\" uses the \"${option}\" option, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldOptions\",\n path,\n fieldName: context.fieldName,\n };\n}\n\n/**\n * Extracts which options are present on a field object.\n * Works with FormSpec field types.\n *\n * @param field - A field object with potential options\n * @returns Array of present option names\n *\n * @public\n */\nexport function extractFieldOptions(field: Record<string, unknown>): FieldOption[] {\n const options: FieldOption[] = [];\n\n if (field[\"label\"] !== undefined) options.push(\"label\");\n if (field[\"placeholder\"] !== undefined) options.push(\"placeholder\");\n if (field[\"required\"] !== undefined) options.push(\"required\");\n // NumberField uses \"min\"/\"max\" in core types, map to \"minValue\"/\"maxValue\" constraints\n if (field[\"min\"] !== undefined || field[\"minValue\"] !== undefined) options.push(\"minValue\");\n if (field[\"max\"] !== undefined || field[\"maxValue\"] !== undefined) options.push(\"maxValue\");\n if (field[\"minItems\"] !== undefined) options.push(\"minItems\");\n if (field[\"maxItems\"] !== undefined) options.push(\"maxItems\");\n\n return options;\n}\n\n/**\n * Checks if a specific field option is allowed.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns true if allowed, false if disallowed\n *\n * @public\n */\nexport function isFieldOptionAllowed(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): boolean {\n const severity = constraints[option];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field option.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @public\n */\nexport function getFieldOptionSeverity(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): Severity {\n return constraints[option] ?? \"off\";\n}\n","import type { FormElement, FormSpec, AnyField } from \"@formspec/core\";\nimport type {\n ConstraintConfig,\n ResolvedConstraintConfig,\n ValidationIssue,\n ValidationResult,\n} from \"../types.js\";\nimport { mergeWithDefaults } from \"../defaults.js\";\nimport { validateFieldTypes } from \"./field-types.js\";\nimport { validateLayout } from \"./layout.js\";\nimport { validateFieldOptions, extractFieldOptions } from \"./field-options.js\";\n\n/**\n * Options for validating FormSpec elements.\n *\n * @public\n */\nexport interface FormSpecValidationOptions {\n /** Constraint configuration (will be merged with defaults) */\n constraints?: ConstraintConfig;\n}\n\n/**\n * Validates FormSpec elements against constraints.\n *\n * This is the main entry point for validating a form specification\n * against a constraint configuration. It walks through all elements\n * and checks each one against the configured constraints.\n *\n * @param elements - FormSpec elements to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @example\n * ```ts\n * import { formspec, field, group } from '@formspec/dsl';\n * import { validateFormSpecElements, defineConstraints } from '@formspec/constraints';\n *\n * const form = formspec(\n * group(\"Contact\",\n * field.text(\"name\"),\n * field.dynamicEnum(\"country\", \"countries\"),\n * ),\n * );\n *\n * const result = validateFormSpecElements(form.elements, {\n * constraints: {\n * fieldTypes: { dynamicEnum: 'error' },\n * layout: { group: 'error' },\n * },\n * });\n *\n * if (!result.valid) {\n * console.error('Validation failed:', result.issues);\n * }\n * ```\n *\n * @public\n */\nexport function validateFormSpecElements(\n elements: readonly FormElement[],\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n const constraints = mergeWithDefaults(options.constraints);\n const issues: ValidationIssue[] = [];\n\n // Walk through all elements\n walkElements(elements, constraints, issues, \"\", 0);\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n}\n\n/**\n * Validates a complete FormSpec against constraints.\n *\n * @param formSpec - The FormSpec to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @public\n */\nexport function validateFormSpec(\n formSpec: FormSpec<readonly FormElement[]>,\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n return validateFormSpecElements(formSpec.elements, options);\n}\n\n/**\n * Recursively walks through FormSpec elements and validates each one.\n */\nfunction walkElements(\n elements: readonly FormElement[],\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n for (const element of elements) {\n const elementPath = pathPrefix;\n\n if (element._type === \"field\") {\n validateField(element, constraints, issues, elementPath, depth);\n } else if (element._type === \"group\") {\n validateGroup(element, constraints, issues, elementPath, depth);\n } else {\n // element._type === \"conditional\"\n validateConditional(element, constraints, issues, elementPath, depth);\n }\n }\n}\n\n/**\n * Validates a field element.\n */\nfunction validateField(\n field: AnyField,\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const fieldPath = pathPrefix ? `${pathPrefix}/${field.name}` : field.name;\n\n // Validate field type\n const fieldTypeIssues = validateFieldTypes(\n {\n fieldType: field._field,\n fieldName: field.name,\n path: fieldPath,\n },\n constraints.fieldTypes\n );\n issues.push(...fieldTypeIssues);\n\n // Validate field options\n const presentOptions = extractFieldOptions(field as unknown as Record<string, unknown>);\n if (presentOptions.length > 0) {\n const optionIssues = validateFieldOptions(\n {\n fieldName: field.name,\n presentOptions,\n path: fieldPath,\n },\n constraints.fieldOptions\n );\n issues.push(...optionIssues);\n }\n\n // Check nesting depth for array/object fields\n if (field._field === \"array\" || field._field === \"object\") {\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\", // Arrays/objects contribute to nesting depth\n depth: depth + 1,\n path: fieldPath,\n },\n constraints.layout\n );\n // Only add nesting depth issues, not group issues\n issues.push(...layoutIssues.filter((issue) => issue.code === \"EXCEEDED_NESTING_DEPTH\"));\n\n // Recursively validate nested elements\n if (field._field === \"array\" && \"items\" in field) {\n walkElements(\n field.items as readonly FormElement[],\n constraints,\n issues,\n `${fieldPath}[]`,\n depth + 1\n );\n } else if (\"properties\" in field) {\n // field._field === \"object\"\n walkElements(\n field.properties as readonly FormElement[],\n constraints,\n issues,\n fieldPath,\n depth + 1\n );\n }\n }\n}\n\n/**\n * Validates a group element.\n */\nfunction validateGroup(\n group: {\n readonly _type: \"group\";\n readonly label: string;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const groupPath = pathPrefix ? `${pathPrefix}/[group:${group.label}]` : `[group:${group.label}]`;\n\n // Validate group usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\",\n label: group.label,\n depth,\n path: groupPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements (groups don't increase nesting depth for schema)\n walkElements(group.elements, constraints, issues, pathPrefix, depth);\n}\n\n/**\n * Validates a conditional element.\n */\nfunction validateConditional(\n conditional: {\n readonly _type: \"conditional\";\n readonly field: string;\n readonly value: unknown;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const condPath = pathPrefix\n ? `${pathPrefix}/[when:${conditional.field}=${String(conditional.value)}]`\n : `[when:${conditional.field}=${String(conditional.value)}]`;\n\n // Validate conditional usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"conditional\",\n depth,\n path: condPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements\n walkElements(conditional.elements, constraints, issues, pathPrefix, depth);\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AACzB,SAAS,SAAS,eAAe;AACjC,SAAS,SAAS,iBAAiB;;;ACM5B,IAAM,sBAAgD;AAAA,EAC3D,YAAY;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,eAAe;AAAA,IACf,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,QAAQ;AAAA,IACN,OAAO;AAAA,IACP,cAAc;AAAA,IACd,iBAAiB;AAAA,EACnB;AAAA,EACA,UAAU;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,kBAAkB;AAAA,MAClB,OAAO;AAAA,MACP,gBAAgB;AAAA,MAChB,UAAU;AAAA,IACZ;AAAA,IACA,OAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,gBAAgB;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,0BAA0B;AAAA,IAC1B,sBAAsB;AAAA,IACtB,QAAQ,CAAC;AAAA,EACX;AACF;AAOO,IAAM,iBAAiC;AAAA,EAC5C,aAAa;AACf;AAOO,SAAS,kBAAkB,QAAgE;AAChG,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,MACV,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,QAAQ;AAAA,MACN,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,UAAU;AAAA,MACR,SAAS;AAAA,QACP,GAAG,oBAAoB,SAAS;AAAA,QAChC,GAAG,OAAO,UAAU;AAAA,MACtB;AAAA,MACA,OAAO;AAAA,QACL,SAAS,OAAO,UAAU,OAAO,WAAW,oBAAoB,SAAS,MAAM;AAAA,QAC/E,SAAS;AAAA,UACP,GAAG,oBAAoB,SAAS,MAAM;AAAA,UACtC,GAAG,OAAO,UAAU,OAAO;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAAA,IACA,cAAc;AAAA,MACZ,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,IACZ;AAAA,IACA,gBAAgB;AAAA,MACd,GAAG,oBAAoB;AAAA,MACvB,GAAG,OAAO;AAAA,MACV,QAAQ;AAAA,QACN,GAAG,oBAAoB,eAAe;AAAA,QACtC,GAAG,OAAO,gBAAgB;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;;;AD1GA,IAAM,oBAAoB,CAAC,iBAAiB,kBAAkB,cAAc;AA4C5E,eAAe,eAAe,UAAkB,eAAgD;AAC9F,MAAI,aAAa,QAAQ,QAAQ;AAGjC,SAAO,MAAM;AACX,eAAW,YAAY,mBAAmB;AACxC,YAAM,WAAW,QAAQ,YAAY,QAAQ;AAC7C,UAAI;AACF,cAAM,SAAS,QAAQ;AACvB,eAAO;AAAA,MACT,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,CAAC,eAAe;AAClB;AAAA,IACF;AAEA,UAAM,YAAY,QAAQ,UAAU;AAEpC,QAAI,cAAc,YAAY;AAC5B;AAAA,IACF;AACA,iBAAa;AAAA,EACf;AAEA,SAAO;AACT;AAKA,eAAe,gBAAgB,UAA2C;AACxE,QAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,QAAM,SAAS,UAAU,OAAO;AAEhC,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO,CAAC;AAAA,EACV;AAEA,MAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,UAAM,IAAI,MAAM,0BAA0B,QAAQ,6BAA6B,OAAO,MAAM,EAAE;AAAA,EAChG;AAEA,SAAO;AACT;AAsBA,eAAsB,WAAW,UAA6B,CAAC,GAA8B;AAC3F,QAAM,EAAE,MAAM,QAAQ,IAAI,GAAG,YAAY,gBAAgB,KAAK,IAAI;AAElE,MAAI,eAA8B;AAElC,MAAI,YAAY;AACd,mBAAe,QAAQ,KAAK,UAAU;AACtC,QAAI;AACF,YAAM,SAAS,YAAY;AAAA,IAC7B,QAAQ;AACN,YAAM,IAAI,MAAM,4BAA4B,YAAY,EAAE;AAAA,IAC5D;AAAA,EACF,OAAO;AACL,mBAAe,MAAM,eAAe,KAAK,aAAa;AAAA,EACxD;AAEA,MAAI,CAAC,cAAc;AACjB,WAAO;AAAA,MACL,QAAQ,kBAAkB,MAAS;AAAA,MACnC,YAAY;AAAA,MACZ,OAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,aAAa,MAAM,gBAAgB,YAAY;AACrD,QAAM,SAAS,kBAAkB,WAAW,WAAW;AAEvD,SAAO;AAAA,IACL;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,EACT;AACF;AAWO,SAAS,qBAAqB,aAA+C;AAClF,QAAM,SAAS,UAAU,WAAW;AAEpC,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO,kBAAkB,MAAS;AAAA,EACpC;AAEA,MAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,UAAM,IAAI,MAAM,mDAAmD,OAAO,MAAM,EAAE;AAAA,EACpF;AAEA,SAAO,kBAAkB,OAAO,WAAW;AAC7C;AAwBO,SAAS,kBAAkB,QAAoD;AACpF,SAAO,kBAAkB,MAAM;AACjC;;;AErMA,IAAM,iBAA6D;AAAA,EACjE,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,QAAQ;AACV;AAKA,IAAM,mBAA2C;AAAA,EAC/C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,QAAQ;AACV;AAyBO,SAAS,mBACd,SACA,aACmB;AACnB,QAAM,SAA4B,CAAC;AAEnC,QAAM,gBAAgB,eAAe,QAAQ,SAAS;AACtD,MAAI,CAAC,eAAe;AAElB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,YAAY,aAAa;AAC1C,MAAI,YAAY,aAAa,OAAO;AAClC,UAAM,gBAAgB,iBAAiB,QAAQ,SAAS,KAAK,QAAQ;AACrE,WAAO,KAAK,qBAAqB,SAAS,eAAe,QAAQ,CAAC;AAAA,EACpE;AAEA,SAAO;AACT;AAKA,SAAS,qBACP,SACA,eACA,UACiB;AACjB,QAAM,OAAO,QAAQ,QAAQ,QAAQ;AACrC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,UAAU,QAAQ,SAAS,UAAU,aAAa;AAAA,IAC3D,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,IACV;AAAA,IACA,WAAW,QAAQ;AAAA,IACnB,WAAW,QAAQ;AAAA,EACrB;AACF;AAYO,SAAS,mBAAmB,WAAmB,aAA4C;AAChG,QAAM,gBAAgB,eAAe,SAAS;AAC9C,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,YAAY,aAAa;AAC1C,SAAO,CAAC,YAAY,aAAa;AACnC;AAWO,SAAS,qBACd,WACA,aACU;AACV,QAAM,gBAAgB,eAAe,SAAS;AAC9C,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAEA,SAAO,YAAY,aAAa,KAAK;AACvC;;;AC1GO,SAAS,eACd,SACA,aACmB;AACnB,QAAM,SAA4B,CAAC;AAGnC,MAAI,QAAQ,eAAe,SAAS;AAClC,UAAM,gBAAgB,YAAY;AAClC,QAAI,iBAAiB,kBAAkB,OAAO;AAC5C,aAAO,KAAK,iBAAiB,SAAS,aAAa,CAAC;AAAA,IACtD;AAAA,EACF;AAGA,MAAI,QAAQ,eAAe,eAAe;AACxC,UAAM,sBAAsB,YAAY;AACxC,QAAI,uBAAuB,wBAAwB,OAAO;AACxD,aAAO,KAAK,uBAAuB,SAAS,mBAAmB,CAAC;AAAA,IAClE;AAAA,EACF;AAGA,QAAM,WAAW,YAAY;AAC7B,MAAI,aAAa,UAAa,QAAQ,QAAQ,UAAU;AACtD,WAAO,KAAK,wBAAwB,SAAS,QAAQ,CAAC;AAAA,EACxD;AAEA,SAAO;AACT;AAKA,SAAS,iBAAiB,SAAwB,UAAqC;AACrF,QAAM,YAAY,QAAQ,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAC1D,QAAM,QAAyB;AAAA,IAC7B,MAAM;AAAA,IACN,SAAS,QAAQ,SAAS;AAAA,IAC1B,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,EACZ;AACA,MAAI,QAAQ,SAAS,QAAW;AAC9B,UAAM,OAAO,QAAQ;AAAA,EACvB;AACA,SAAO;AACT;AAKA,SAAS,uBAAuB,SAAwB,UAAqC;AAC3F,QAAM,QAAyB;AAAA,IAC7B,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,EACZ;AACA,MAAI,QAAQ,SAAS,QAAW;AAC9B,UAAM,OAAO,QAAQ;AAAA,EACvB;AACA,SAAO;AACT;AAKA,SAAS,wBAAwB,SAAwB,UAAmC;AAC1F,QAAM,QAAyB;AAAA,IAC7B,MAAM;AAAA,IACN,SAAS,iBAAiB,OAAO,QAAQ,KAAK,CAAC,qCAAqC,OAAO,QAAQ,CAAC;AAAA,IACpG,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AACA,MAAI,QAAQ,SAAS,QAAW;AAC9B,UAAM,OAAO,QAAQ;AAAA,EACvB;AACA,SAAO;AACT;AAWO,SAAS,oBACd,YACA,aACS;AACT,MAAI,eAAe,SAAS;AAC1B,UAAMA,YAAW,YAAY;AAC7B,WAAO,CAACA,aAAYA,cAAa;AAAA,EACnC;AAGA,QAAM,WAAW,YAAY;AAC7B,SAAO,CAAC,YAAY,aAAa;AACnC;AAWO,SAAS,sBAAsB,OAAe,aAAyC;AAC5F,QAAM,WAAW,YAAY;AAC7B,MAAI,aAAa,QAAW;AAC1B,WAAO;AAAA,EACT;AACA,SAAO,SAAS;AAClB;;;AC1GO,SAAS,qBACd,SACA,aACmB;AACnB,QAAM,SAA4B,CAAC;AAEnC,aAAW,UAAU,QAAQ,gBAAgB;AAC3C,UAAM,WAAW,YAAY,MAAM;AACnC,QAAI,YAAY,aAAa,OAAO;AAClC,aAAO,KAAK,uBAAuB,SAAS,QAAQ,QAAQ,CAAC;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO;AACT;AAKA,SAAS,uBACP,SACA,QACA,UACiB;AACjB,QAAM,OAAO,QAAQ,QAAQ,QAAQ;AACrC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,UAAU,QAAQ,SAAS,eAAe,MAAM;AAAA,IACzD,UAAU,aAAa,UAAU,UAAU;AAAA,IAC3C,UAAU;AAAA,IACV;AAAA,IACA,WAAW,QAAQ;AAAA,EACrB;AACF;AAWO,SAAS,oBAAoB,OAA+C;AACjF,QAAM,UAAyB,CAAC;AAEhC,MAAI,MAAM,OAAO,MAAM,OAAW,SAAQ,KAAK,OAAO;AACtD,MAAI,MAAM,aAAa,MAAM,OAAW,SAAQ,KAAK,aAAa;AAClE,MAAI,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAE5D,MAAI,MAAM,KAAK,MAAM,UAAa,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAC1F,MAAI,MAAM,KAAK,MAAM,UAAa,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAC1F,MAAI,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAC5D,MAAI,MAAM,UAAU,MAAM,OAAW,SAAQ,KAAK,UAAU;AAE5D,SAAO;AACT;AAWO,SAAS,qBACd,QACA,aACS;AACT,QAAM,WAAW,YAAY,MAAM;AACnC,SAAO,CAAC,YAAY,aAAa;AACnC;AAWO,SAAS,uBACd,QACA,aACU;AACV,SAAO,YAAY,MAAM,KAAK;AAChC;;;ACtEO,SAAS,yBACd,UACA,UAAqC,CAAC,GACpB;AAClB,QAAM,cAAc,kBAAkB,QAAQ,WAAW;AACzD,QAAM,SAA4B,CAAC;AAGnC,eAAa,UAAU,aAAa,QAAQ,IAAI,CAAC;AAEjD,SAAO;AAAA,IACL,OAAO,CAAC,OAAO,KAAK,CAAC,UAAU,MAAM,aAAa,OAAO;AAAA,IACzD;AAAA,EACF;AACF;AAWO,SAAS,iBACd,UACA,UAAqC,CAAC,GACpB;AAClB,SAAO,yBAAyB,SAAS,UAAU,OAAO;AAC5D;AAKA,SAAS,aACP,UACA,aACA,QACA,YACA,OACM;AACN,aAAW,WAAW,UAAU;AAC9B,UAAM,cAAc;AAEpB,QAAI,QAAQ,UAAU,SAAS;AAC7B,oBAAc,SAAS,aAAa,QAAQ,aAAa,KAAK;AAAA,IAChE,WAAW,QAAQ,UAAU,SAAS;AACpC,oBAAc,SAAS,aAAa,QAAQ,aAAa,KAAK;AAAA,IAChE,OAAO;AAEL,0BAAoB,SAAS,aAAa,QAAQ,aAAa,KAAK;AAAA,IACtE;AAAA,EACF;AACF;AAKA,SAAS,cACP,OACA,aACA,QACA,YACA,OACM;AACN,QAAM,YAAY,aAAa,GAAG,UAAU,IAAI,MAAM,IAAI,KAAK,MAAM;AAGrE,QAAM,kBAAkB;AAAA,IACtB;AAAA,MACE,WAAW,MAAM;AAAA,MACjB,WAAW,MAAM;AAAA,MACjB,MAAM;AAAA,IACR;AAAA,IACA,YAAY;AAAA,EACd;AACA,SAAO,KAAK,GAAG,eAAe;AAG9B,QAAM,iBAAiB,oBAAoB,KAA2C;AACtF,MAAI,eAAe,SAAS,GAAG;AAC7B,UAAM,eAAe;AAAA,MACnB;AAAA,QACE,WAAW,MAAM;AAAA,QACjB;AAAA,QACA,MAAM;AAAA,MACR;AAAA,MACA,YAAY;AAAA,IACd;AACA,WAAO,KAAK,GAAG,YAAY;AAAA,EAC7B;AAGA,MAAI,MAAM,WAAW,WAAW,MAAM,WAAW,UAAU;AACzD,UAAM,eAAe;AAAA,MACnB;AAAA,QACE,YAAY;AAAA;AAAA,QACZ,OAAO,QAAQ;AAAA,QACf,MAAM;AAAA,MACR;AAAA,MACA,YAAY;AAAA,IACd;AAEA,WAAO,KAAK,GAAG,aAAa,OAAO,CAAC,UAAU,MAAM,SAAS,wBAAwB,CAAC;AAGtF,QAAI,MAAM,WAAW,WAAW,WAAW,OAAO;AAChD;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,GAAG,SAAS;AAAA,QACZ,QAAQ;AAAA,MACV;AAAA,IACF,WAAW,gBAAgB,OAAO;AAEhC;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AAKA,SAAS,cACP,OAKA,aACA,QACA,YACA,OACM;AACN,QAAM,YAAY,aAAa,GAAG,UAAU,WAAW,MAAM,KAAK,MAAM,UAAU,MAAM,KAAK;AAG7F,QAAM,eAAe;AAAA,IACnB;AAAA,MACE,YAAY;AAAA,MACZ,OAAO,MAAM;AAAA,MACb;AAAA,MACA,MAAM;AAAA,IACR;AAAA,IACA,YAAY;AAAA,EACd;AACA,SAAO,KAAK,GAAG,YAAY;AAG3B,eAAa,MAAM,UAAU,aAAa,QAAQ,YAAY,KAAK;AACrE;AAKA,SAAS,oBACP,aAMA,aACA,QACA,YACA,OACM;AACN,QAAM,WAAW,aACb,GAAG,UAAU,UAAU,YAAY,KAAK,IAAI,OAAO,YAAY,KAAK,CAAC,MACrE,SAAS,YAAY,KAAK,IAAI,OAAO,YAAY,KAAK,CAAC;AAG3D,QAAM,eAAe;AAAA,IACnB;AAAA,MACE,YAAY;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,IACR;AAAA,IACA,YAAY;AAAA,EACd;AACA,SAAO,KAAK,GAAG,YAAY;AAG3B,eAAa,YAAY,UAAU,aAAa,QAAQ,YAAY,KAAK;AAC3E;","names":["severity"]}
1
+ {"version":3,"sources":["../src/loader.ts","../src/defaults.ts","../src/validators/field-types.ts","../src/validators/layout.ts","../src/validators/field-options.ts","../src/validators/formspec.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport type { FormSpecConfig, ConstraintConfig, ResolvedConstraintConfig } from \"./types.js\";\nimport { mergeWithDefaults } from \"./defaults.js\";\n\n/**\n * Default config file names to search for (in order of priority).\n */\nconst CONFIG_FILE_NAMES = [\".formspec.yml\", \".formspec.yaml\", \"formspec.yml\"];\n\n/**\n * Options for loading configuration.\n *\n * @public\n */\nexport interface LoadConfigOptions {\n /**\n * The directory to search for config files.\n * Defaults to process.cwd().\n */\n cwd?: string;\n\n /**\n * Explicit path to a config file.\n * If provided, skips searching for default config file names.\n */\n configPath?: string;\n\n /**\n * Whether to search parent directories for config files.\n * Defaults to true.\n */\n searchParents?: boolean;\n}\n\n/**\n * Result of loading configuration.\n *\n * @public\n */\nexport interface LoadConfigResult {\n /** The loaded and merged configuration */\n config: ResolvedConstraintConfig;\n /** The path to the config file that was loaded (if any) */\n configPath: string | null;\n /** Whether a config file was found */\n found: boolean;\n}\n\n/**\n * Searches for a config file in the given directory and optionally parent directories.\n */\nasync function findConfigFile(startDir: string, searchParents: boolean): Promise<string | null> {\n let currentDir = resolve(startDir);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop with break conditions\n while (true) {\n for (const fileName of CONFIG_FILE_NAMES) {\n const filePath = resolve(currentDir, fileName);\n try {\n await readFile(filePath);\n return filePath;\n } catch {\n // File doesn't exist, continue searching\n }\n }\n\n if (!searchParents) {\n break;\n }\n\n const parentDir = dirname(currentDir);\n // Reached filesystem root when dirname returns same path\n if (parentDir === currentDir) {\n break;\n }\n currentDir = parentDir;\n }\n\n return null;\n}\n\n/**\n * Parses a YAML config file and returns the FormSpecConfig.\n */\nasync function parseConfigFile(filePath: string): Promise<FormSpecConfig> {\n const content = await readFile(filePath, \"utf-8\");\n const parsed = parseYaml(content) as unknown;\n\n if (parsed === null || parsed === undefined) {\n return {};\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config file at ${filePath}: expected an object, got ${typeof parsed}`);\n }\n\n return parsed as FormSpecConfig;\n}\n\n/**\n * Loads FormSpec constraint configuration from a .formspec.yml file.\n *\n * @param options - Options for loading configuration\n * @returns The loaded configuration with defaults applied\n *\n * @example\n * ```ts\n * // Load from current directory (searches for .formspec.yml)\n * const result = await loadConfig();\n *\n * // Load from specific directory\n * const result = await loadConfig({ cwd: '/path/to/project' });\n *\n * // Load from specific file\n * const result = await loadConfig({ configPath: '/path/to/config.yml' });\n * ```\n *\n * @public\n */\nexport async function loadConfig(options: LoadConfigOptions = {}): Promise<LoadConfigResult> {\n const { cwd = process.cwd(), configPath, searchParents = true } = options;\n\n let resolvedPath: string | null = null;\n\n if (configPath) {\n resolvedPath = resolve(cwd, configPath);\n try {\n await readFile(resolvedPath);\n } catch {\n throw new Error(`Config file not found at ${resolvedPath}`);\n }\n } else {\n resolvedPath = await findConfigFile(cwd, searchParents);\n }\n\n if (!resolvedPath) {\n return {\n config: mergeWithDefaults(undefined),\n configPath: null,\n found: false,\n };\n }\n\n const fileConfig = await parseConfigFile(resolvedPath);\n const config = mergeWithDefaults(fileConfig.constraints);\n\n return {\n config,\n configPath: resolvedPath,\n found: true,\n };\n}\n\n/**\n * Synchronously loads config from a pre-parsed YAML string.\n * Useful for testing or when config is already available.\n *\n * @param yamlContent - The YAML content to parse\n * @returns The parsed and merged configuration\n *\n * @public\n */\nexport function loadConfigFromString(yamlContent: string): ResolvedConstraintConfig {\n const parsed = parseYaml(yamlContent) as FormSpecConfig | null | undefined;\n\n if (parsed === null || parsed === undefined) {\n return mergeWithDefaults(undefined);\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`Invalid config content: expected an object, got ${typeof parsed}`);\n }\n\n return mergeWithDefaults(parsed.constraints);\n}\n\n/**\n * Creates a constraint configuration directly from an object.\n * Useful for programmatic configuration without YAML.\n *\n * @param config - Partial constraint configuration\n * @returns Complete configuration with defaults applied\n *\n * @example\n * ```ts\n * const config = defineConstraints({\n * fieldTypes: {\n * dynamicEnum: 'error',\n * dynamicSchema: 'error',\n * },\n * layout: {\n * group: 'error',\n * },\n * });\n * ```\n *\n * @public\n */\nexport function defineConstraints(config: ConstraintConfig): ResolvedConstraintConfig {\n return mergeWithDefaults(config);\n}\n","import type { ConstraintConfig, FormSpecConfig, ResolvedConstraintConfig } from \"./types.js\";\n\n/**\n * Default constraint configuration that allows all features.\n * All constraints default to \"off\" (allowed).\n *\n * @beta\n */\nexport const DEFAULT_CONSTRAINTS: ResolvedConstraintConfig = {\n fieldTypes: {\n text: \"off\",\n number: \"off\",\n boolean: \"off\",\n staticEnum: \"off\",\n dynamicEnum: \"off\",\n dynamicSchema: \"off\",\n array: \"off\",\n object: \"off\",\n },\n layout: {\n group: \"off\",\n conditionals: \"off\",\n maxNestingDepth: Infinity,\n },\n uiSchema: {\n layouts: {\n VerticalLayout: \"off\",\n HorizontalLayout: \"off\",\n Group: \"off\",\n Categorization: \"off\",\n Category: \"off\",\n },\n rules: {\n enabled: \"off\",\n effects: {\n SHOW: \"off\",\n HIDE: \"off\",\n ENABLE: \"off\",\n DISABLE: \"off\",\n },\n },\n },\n fieldOptions: {\n label: \"off\",\n placeholder: \"off\",\n required: \"off\",\n minValue: \"off\",\n maxValue: \"off\",\n minItems: \"off\",\n maxItems: \"off\",\n },\n controlOptions: {\n format: \"off\",\n readonly: \"off\",\n multi: \"off\",\n showUnfocusedDescription: \"off\",\n hideRequiredAsterisk: \"off\",\n custom: {},\n },\n};\n\n/**\n * Default FormSpec configuration.\n *\n * @beta\n */\nexport const DEFAULT_CONFIG: FormSpecConfig = {\n constraints: DEFAULT_CONSTRAINTS,\n};\n\n/**\n * Merges user constraints with defaults, filling in any missing values.\n *\n * @beta\n */\nexport function mergeWithDefaults(config: ConstraintConfig | undefined): ResolvedConstraintConfig {\n if (!config) {\n return DEFAULT_CONSTRAINTS;\n }\n\n return {\n fieldTypes: {\n ...DEFAULT_CONSTRAINTS.fieldTypes,\n ...config.fieldTypes,\n },\n layout: {\n ...DEFAULT_CONSTRAINTS.layout,\n ...config.layout,\n },\n uiSchema: {\n layouts: {\n ...DEFAULT_CONSTRAINTS.uiSchema.layouts,\n ...config.uiSchema?.layouts,\n },\n rules: {\n enabled: config.uiSchema?.rules?.enabled ?? DEFAULT_CONSTRAINTS.uiSchema.rules.enabled,\n effects: {\n ...DEFAULT_CONSTRAINTS.uiSchema.rules.effects,\n ...config.uiSchema?.rules?.effects,\n },\n },\n },\n fieldOptions: {\n ...DEFAULT_CONSTRAINTS.fieldOptions,\n ...config.fieldOptions,\n },\n controlOptions: {\n ...DEFAULT_CONSTRAINTS.controlOptions,\n ...config.controlOptions,\n custom: {\n ...DEFAULT_CONSTRAINTS.controlOptions.custom,\n ...config.controlOptions?.custom,\n },\n },\n };\n}\n","import type { FieldTypeConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Maps FormSpec field._field values to constraint config keys.\n */\nconst FIELD_TYPE_MAP: Record<string, keyof FieldTypeConstraints> = {\n text: \"text\",\n number: \"number\",\n boolean: \"boolean\",\n enum: \"staticEnum\",\n dynamic_enum: \"dynamicEnum\",\n dynamic_schema: \"dynamicSchema\",\n array: \"array\",\n object: \"object\",\n};\n\n/**\n * Human-readable names for field types.\n */\nconst FIELD_TYPE_NAMES: Record<string, string> = {\n text: \"text field\",\n number: \"number field\",\n boolean: \"boolean field\",\n enum: \"static enum field\",\n dynamic_enum: \"dynamic enum field\",\n dynamic_schema: \"dynamic schema field\",\n array: \"array field\",\n object: \"object field\",\n};\n\n/**\n * Context for field type validation.\n *\n * @beta\n */\nexport interface FieldTypeContext {\n /** The _field discriminator value (e.g., \"text\", \"number\", \"enum\") */\n fieldType: string;\n /** The field name */\n fieldName: string;\n /** Optional path for nested fields */\n path?: string;\n}\n\n/**\n * Validates a field type against constraints.\n *\n * @param context - Information about the field being validated\n * @param constraints - Field type constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @beta\n */\nexport function validateFieldTypes(\n context: FieldTypeContext,\n constraints: FieldTypeConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n const constraintKey = FIELD_TYPE_MAP[context.fieldType];\n if (!constraintKey) {\n // Unknown field type, skip validation\n return issues;\n }\n\n const severity = constraints[constraintKey];\n if (severity && severity !== \"off\") {\n const fieldTypeName = FIELD_TYPE_NAMES[context.fieldType] ?? context.fieldType;\n issues.push(createFieldTypeIssue(context, fieldTypeName, severity));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field type.\n */\nfunction createFieldTypeIssue(\n context: FieldTypeContext,\n fieldTypeName: string,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_TYPE\",\n message: `Field \"${context.fieldName}\" uses ${fieldTypeName}, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldTypes\",\n path,\n fieldName: context.fieldName,\n fieldType: context.fieldType,\n };\n}\n\n/**\n * Checks if a field type is allowed by the constraints.\n * Useful for quick checks without generating issues.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns true if allowed, false if disallowed\n *\n * @beta\n */\nexport function isFieldTypeAllowed(fieldType: string, constraints: FieldTypeConstraints): boolean {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return true; // Unknown types are allowed by default\n }\n\n const severity = constraints[constraintKey];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field type.\n *\n * @param fieldType - The _field discriminator value\n * @param constraints - Field type constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @beta\n */\nexport function getFieldTypeSeverity(\n fieldType: string,\n constraints: FieldTypeConstraints\n): Severity {\n const constraintKey = FIELD_TYPE_MAP[fieldType];\n if (!constraintKey) {\n return \"off\";\n }\n\n return constraints[constraintKey] ?? \"off\";\n}\n","import type { LayoutConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Context for layout validation.\n *\n * @beta\n */\nexport interface LayoutContext {\n /** The type of layout element (\"group\" | \"conditional\") */\n layoutType: \"group\" | \"conditional\";\n /** Optional label for the element (for groups) */\n label?: string;\n /** Current nesting depth */\n depth: number;\n /** Path to this element */\n path?: string;\n}\n\n/**\n * Validates a layout element against constraints.\n *\n * @param context - Information about the layout element\n * @param constraints - Layout constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @beta\n */\nexport function validateLayout(\n context: LayoutContext,\n constraints: LayoutConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n // Check if groups are allowed\n if (context.layoutType === \"group\") {\n const groupSeverity = constraints.group;\n if (groupSeverity && groupSeverity !== \"off\") {\n issues.push(createGroupIssue(context, groupSeverity));\n }\n }\n\n // Check if conditionals are allowed\n if (context.layoutType === \"conditional\") {\n const conditionalSeverity = constraints.conditionals;\n if (conditionalSeverity && conditionalSeverity !== \"off\") {\n issues.push(createConditionalIssue(context, conditionalSeverity));\n }\n }\n\n // Check nesting depth (applies to both groups and fields within nested structures)\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth !== undefined && context.depth > maxDepth) {\n issues.push(createNestingDepthIssue(context, maxDepth));\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed group.\n */\nfunction createGroupIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const labelInfo = context.label ? ` \"${context.label}\"` : \"\";\n const issue: ValidationIssue = {\n code: \"DISALLOWED_GROUP\",\n message: `Group${labelInfo} is not allowed - visual grouping is not supported in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for a disallowed conditional.\n */\nfunction createConditionalIssue(context: LayoutContext, severity: Severity): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"DISALLOWED_CONDITIONAL\",\n message: `Conditional visibility (when/is) is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Creates a validation issue for exceeding nesting depth.\n */\nfunction createNestingDepthIssue(context: LayoutContext, maxDepth: number): ValidationIssue {\n const issue: ValidationIssue = {\n code: \"EXCEEDED_NESTING_DEPTH\",\n message: `Nesting depth ${String(context.depth)} exceeds maximum allowed depth of ${String(maxDepth)}`,\n severity: \"error\",\n category: \"layout\",\n };\n if (context.path !== undefined) {\n issue.path = context.path;\n }\n return issue;\n}\n\n/**\n * Checks if a layout type is allowed by the constraints.\n *\n * @param layoutType - The type of layout element\n * @param constraints - Layout constraints\n * @returns true if allowed, false if disallowed\n *\n * @beta\n */\nexport function isLayoutTypeAllowed(\n layoutType: \"group\" | \"conditional\",\n constraints: LayoutConstraints\n): boolean {\n if (layoutType === \"group\") {\n const severity = constraints.group;\n return !severity || severity === \"off\";\n }\n\n // layoutType === \"conditional\"\n const severity = constraints.conditionals;\n return !severity || severity === \"off\";\n}\n\n/**\n * Checks if a nesting depth is allowed.\n *\n * @param depth - Current nesting depth\n * @param constraints - Layout constraints\n * @returns true if allowed, false if exceeds limit\n *\n * @beta\n */\nexport function isNestingDepthAllowed(depth: number, constraints: LayoutConstraints): boolean {\n const maxDepth = constraints.maxNestingDepth;\n if (maxDepth === undefined) {\n return true;\n }\n return depth <= maxDepth;\n}\n","import type { FieldOptionConstraints, Severity, ValidationIssue } from \"../types.js\";\n\n/**\n * Known field options that can be validated.\n *\n * @beta\n */\nexport type FieldOption =\n | \"label\"\n | \"placeholder\"\n | \"required\"\n | \"minValue\"\n | \"maxValue\"\n | \"minItems\"\n | \"maxItems\";\n\n/**\n * Context for field option validation.\n *\n * @beta\n */\nexport interface FieldOptionsContext {\n /** The field name */\n fieldName: string;\n /** Which options are present on this field */\n presentOptions: FieldOption[];\n /** Path to this field */\n path?: string;\n}\n\n/**\n * Validates field options against constraints.\n *\n * @param context - Information about the field and its options\n * @param constraints - Field option constraints\n * @returns Array of validation issues (empty if valid)\n *\n * @beta\n */\nexport function validateFieldOptions(\n context: FieldOptionsContext,\n constraints: FieldOptionConstraints\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n for (const option of context.presentOptions) {\n const severity = constraints[option];\n if (severity && severity !== \"off\") {\n issues.push(createFieldOptionIssue(context, option, severity));\n }\n }\n\n return issues;\n}\n\n/**\n * Creates a validation issue for a disallowed field option.\n */\nfunction createFieldOptionIssue(\n context: FieldOptionsContext,\n option: FieldOption,\n severity: Severity\n): ValidationIssue {\n const path = context.path ?? context.fieldName;\n return {\n code: \"DISALLOWED_FIELD_OPTION\",\n message: `Field \"${context.fieldName}\" uses the \"${option}\" option, which is not allowed in this project`,\n severity: severity === \"error\" ? \"error\" : \"warning\",\n category: \"fieldOptions\",\n path,\n fieldName: context.fieldName,\n };\n}\n\n/**\n * Extracts which options are present on a field object.\n * Works with FormSpec field types.\n *\n * @param field - A field object with potential options\n * @returns Array of present option names\n *\n * @beta\n */\nexport function extractFieldOptions(field: Record<string, unknown>): FieldOption[] {\n const options: FieldOption[] = [];\n\n if (field[\"label\"] !== undefined) options.push(\"label\");\n if (field[\"placeholder\"] !== undefined) options.push(\"placeholder\");\n if (field[\"required\"] !== undefined) options.push(\"required\");\n // NumberField uses \"min\"/\"max\" in core types, map to \"minValue\"/\"maxValue\" constraints\n if (field[\"min\"] !== undefined || field[\"minValue\"] !== undefined) options.push(\"minValue\");\n if (field[\"max\"] !== undefined || field[\"maxValue\"] !== undefined) options.push(\"maxValue\");\n if (field[\"minItems\"] !== undefined) options.push(\"minItems\");\n if (field[\"maxItems\"] !== undefined) options.push(\"maxItems\");\n\n return options;\n}\n\n/**\n * Checks if a specific field option is allowed.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns true if allowed, false if disallowed\n *\n * @beta\n */\nexport function isFieldOptionAllowed(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): boolean {\n const severity = constraints[option];\n return !severity || severity === \"off\";\n}\n\n/**\n * Gets the severity level for a field option.\n *\n * @param option - The option to check\n * @param constraints - Field option constraints\n * @returns Severity level, or \"off\" if not constrained\n *\n * @beta\n */\nexport function getFieldOptionSeverity(\n option: FieldOption,\n constraints: FieldOptionConstraints\n): Severity {\n return constraints[option] ?? \"off\";\n}\n","import type { FormElement, FormSpec, AnyField } from \"@formspec/core\";\nimport type {\n ConstraintConfig,\n ResolvedConstraintConfig,\n ValidationIssue,\n ValidationResult,\n} from \"../types.js\";\nimport { mergeWithDefaults } from \"../defaults.js\";\nimport { validateFieldTypes } from \"./field-types.js\";\nimport { validateLayout } from \"./layout.js\";\nimport { validateFieldOptions, extractFieldOptions } from \"./field-options.js\";\n\n/**\n * Options for validating FormSpec elements.\n *\n * @public\n */\nexport interface FormSpecValidationOptions {\n /** Constraint configuration (will be merged with defaults) */\n constraints?: ConstraintConfig;\n}\n\n/**\n * Validates FormSpec elements against constraints.\n *\n * This is the main entry point for validating a form specification\n * against a constraint configuration. It walks through all elements\n * and checks each one against the configured constraints.\n *\n * @param elements - FormSpec elements to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @example\n * ```ts\n * import { formspec, field, group } from '@formspec/dsl';\n * import { validateFormSpecElements, defineConstraints } from '@formspec/constraints';\n *\n * const form = formspec(\n * group(\"Contact\",\n * field.text(\"name\"),\n * field.dynamicEnum(\"country\", \"countries\"),\n * ),\n * );\n *\n * const result = validateFormSpecElements(form.elements, {\n * constraints: {\n * fieldTypes: { dynamicEnum: 'error' },\n * layout: { group: 'error' },\n * },\n * });\n *\n * if (!result.valid) {\n * console.error('Validation failed:', result.issues);\n * }\n * ```\n *\n * @public\n */\nexport function validateFormSpecElements(\n elements: readonly FormElement[],\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n const constraints = mergeWithDefaults(options.constraints);\n const issues: ValidationIssue[] = [];\n\n // Walk through all elements\n walkElements(elements, constraints, issues, \"\", 0);\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n}\n\n/**\n * Validates a complete FormSpec against constraints.\n *\n * @param formSpec - The FormSpec to validate\n * @param options - Validation options including constraints\n * @returns Validation result with all issues found\n *\n * @public\n */\nexport function validateFormSpec(\n formSpec: FormSpec<readonly FormElement[]>,\n options: FormSpecValidationOptions = {}\n): ValidationResult {\n return validateFormSpecElements(formSpec.elements, options);\n}\n\n/**\n * Recursively walks through FormSpec elements and validates each one.\n */\nfunction walkElements(\n elements: readonly FormElement[],\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n for (const element of elements) {\n const elementPath = pathPrefix;\n\n if (element._type === \"field\") {\n validateField(element, constraints, issues, elementPath, depth);\n } else if (element._type === \"group\") {\n validateGroup(element, constraints, issues, elementPath, depth);\n } else {\n // element._type === \"conditional\"\n validateConditional(element, constraints, issues, elementPath, depth);\n }\n }\n}\n\n/**\n * Validates a field element.\n */\nfunction validateField(\n field: AnyField,\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const fieldPath = pathPrefix ? `${pathPrefix}/${field.name}` : field.name;\n\n // Validate field type\n const fieldTypeIssues = validateFieldTypes(\n {\n fieldType: field._field,\n fieldName: field.name,\n path: fieldPath,\n },\n constraints.fieldTypes\n );\n issues.push(...fieldTypeIssues);\n\n // Validate field options\n const presentOptions = extractFieldOptions(field as unknown as Record<string, unknown>);\n if (presentOptions.length > 0) {\n const optionIssues = validateFieldOptions(\n {\n fieldName: field.name,\n presentOptions,\n path: fieldPath,\n },\n constraints.fieldOptions\n );\n issues.push(...optionIssues);\n }\n\n // Check nesting depth for array/object fields\n if (field._field === \"array\" || field._field === \"object\") {\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\", // Arrays/objects contribute to nesting depth\n depth: depth + 1,\n path: fieldPath,\n },\n constraints.layout\n );\n // Only add nesting depth issues, not group issues\n issues.push(...layoutIssues.filter((issue) => issue.code === \"EXCEEDED_NESTING_DEPTH\"));\n\n // Recursively validate nested elements\n if (field._field === \"array\" && \"items\" in field) {\n walkElements(\n field.items as readonly FormElement[],\n constraints,\n issues,\n `${fieldPath}[]`,\n depth + 1\n );\n } else if (\"properties\" in field) {\n // field._field === \"object\"\n walkElements(\n field.properties as readonly FormElement[],\n constraints,\n issues,\n fieldPath,\n depth + 1\n );\n }\n }\n}\n\n/**\n * Validates a group element.\n */\nfunction validateGroup(\n group: {\n readonly _type: \"group\";\n readonly label: string;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const groupPath = pathPrefix ? `${pathPrefix}/[group:${group.label}]` : `[group:${group.label}]`;\n\n // Validate group usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"group\",\n label: group.label,\n depth,\n path: groupPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements (groups don't increase nesting depth for schema)\n walkElements(group.elements, constraints, issues, pathPrefix, depth);\n}\n\n/**\n * Validates a conditional element.\n */\nfunction validateConditional(\n conditional: {\n readonly _type: \"conditional\";\n readonly field: string;\n readonly value: unknown;\n readonly elements: readonly FormElement[];\n },\n constraints: ResolvedConstraintConfig,\n issues: ValidationIssue[],\n pathPrefix: string,\n depth: number\n): void {\n const condPath = pathPrefix\n ? `${pathPrefix}/[when:${conditional.field}=${String(conditional.value)}]`\n : `[when:${conditional.field}=${String(conditional.value)}]`;\n\n // Validate conditional usage\n const layoutIssues = validateLayout(\n {\n layoutType: \"conditional\",\n depth,\n path: condPath,\n },\n constraints.layout\n );\n issues.push(...layoutIssues);\n\n // Recursively validate nested elements\n walkElements(conditional.elements, constraints, issues, pathPrefix, depth);\n}\n"],"mappings":"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","names":["severity"]}
@@ -2,13 +2,13 @@ import type { FieldOptionConstraints, Severity, ValidationIssue } from "../types
2
2
  /**
3
3
  * Known field options that can be validated.
4
4
  *
5
- * @public
5
+ * @beta
6
6
  */
7
7
  export type FieldOption = "label" | "placeholder" | "required" | "minValue" | "maxValue" | "minItems" | "maxItems";
8
8
  /**
9
9
  * Context for field option validation.
10
10
  *
11
- * @public
11
+ * @beta
12
12
  */
13
13
  export interface FieldOptionsContext {
14
14
  /** The field name */
@@ -25,7 +25,7 @@ export interface FieldOptionsContext {
25
25
  * @param constraints - Field option constraints
26
26
  * @returns Array of validation issues (empty if valid)
27
27
  *
28
- * @public
28
+ * @beta
29
29
  */
30
30
  export declare function validateFieldOptions(context: FieldOptionsContext, constraints: FieldOptionConstraints): ValidationIssue[];
31
31
  /**
@@ -35,7 +35,7 @@ export declare function validateFieldOptions(context: FieldOptionsContext, const
35
35
  * @param field - A field object with potential options
36
36
  * @returns Array of present option names
37
37
  *
38
- * @public
38
+ * @beta
39
39
  */
40
40
  export declare function extractFieldOptions(field: Record<string, unknown>): FieldOption[];
41
41
  /**
@@ -45,7 +45,7 @@ export declare function extractFieldOptions(field: Record<string, unknown>): Fie
45
45
  * @param constraints - Field option constraints
46
46
  * @returns true if allowed, false if disallowed
47
47
  *
48
- * @public
48
+ * @beta
49
49
  */
50
50
  export declare function isFieldOptionAllowed(option: FieldOption, constraints: FieldOptionConstraints): boolean;
51
51
  /**
@@ -55,7 +55,7 @@ export declare function isFieldOptionAllowed(option: FieldOption, constraints: F
55
55
  * @param constraints - Field option constraints
56
56
  * @returns Severity level, or "off" if not constrained
57
57
  *
58
- * @public
58
+ * @beta
59
59
  */
60
60
  export declare function getFieldOptionSeverity(option: FieldOption, constraints: FieldOptionConstraints): Severity;
61
61
  //# sourceMappingURL=field-options.d.ts.map
@@ -2,7 +2,7 @@ import type { FieldTypeConstraints, Severity, ValidationIssue } from "../types.j
2
2
  /**
3
3
  * Context for field type validation.
4
4
  *
5
- * @public
5
+ * @beta
6
6
  */
7
7
  export interface FieldTypeContext {
8
8
  /** The _field discriminator value (e.g., "text", "number", "enum") */
@@ -19,7 +19,7 @@ export interface FieldTypeContext {
19
19
  * @param constraints - Field type constraints
20
20
  * @returns Array of validation issues (empty if valid)
21
21
  *
22
- * @public
22
+ * @beta
23
23
  */
24
24
  export declare function validateFieldTypes(context: FieldTypeContext, constraints: FieldTypeConstraints): ValidationIssue[];
25
25
  /**
@@ -30,7 +30,7 @@ export declare function validateFieldTypes(context: FieldTypeContext, constraint
30
30
  * @param constraints - Field type constraints
31
31
  * @returns true if allowed, false if disallowed
32
32
  *
33
- * @public
33
+ * @beta
34
34
  */
35
35
  export declare function isFieldTypeAllowed(fieldType: string, constraints: FieldTypeConstraints): boolean;
36
36
  /**
@@ -40,7 +40,7 @@ export declare function isFieldTypeAllowed(fieldType: string, constraints: Field
40
40
  * @param constraints - Field type constraints
41
41
  * @returns Severity level, or "off" if not constrained
42
42
  *
43
- * @public
43
+ * @beta
44
44
  */
45
45
  export declare function getFieldTypeSeverity(fieldType: string, constraints: FieldTypeConstraints): Severity;
46
46
  //# sourceMappingURL=field-types.d.ts.map
@@ -2,7 +2,7 @@ import type { LayoutConstraints, ValidationIssue } from "../types.js";
2
2
  /**
3
3
  * Context for layout validation.
4
4
  *
5
- * @public
5
+ * @beta
6
6
  */
7
7
  export interface LayoutContext {
8
8
  /** The type of layout element ("group" | "conditional") */
@@ -21,7 +21,7 @@ export interface LayoutContext {
21
21
  * @param constraints - Layout constraints
22
22
  * @returns Array of validation issues (empty if valid)
23
23
  *
24
- * @public
24
+ * @beta
25
25
  */
26
26
  export declare function validateLayout(context: LayoutContext, constraints: LayoutConstraints): ValidationIssue[];
27
27
  /**
@@ -31,7 +31,7 @@ export declare function validateLayout(context: LayoutContext, constraints: Layo
31
31
  * @param constraints - Layout constraints
32
32
  * @returns true if allowed, false if disallowed
33
33
  *
34
- * @public
34
+ * @beta
35
35
  */
36
36
  export declare function isLayoutTypeAllowed(layoutType: "group" | "conditional", constraints: LayoutConstraints): boolean;
37
37
  /**
@@ -41,7 +41,7 @@ export declare function isLayoutTypeAllowed(layoutType: "group" | "conditional",
41
41
  * @param constraints - Layout constraints
42
42
  * @returns true if allowed, false if exceeds limit
43
43
  *
44
- * @public
44
+ * @beta
45
45
  */
46
46
  export declare function isNestingDepthAllowed(depth: number, constraints: LayoutConstraints): boolean;
47
47
  //# sourceMappingURL=layout.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formspec/constraints",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.23",
4
4
  "description": "Constraint validation for FormSpec - restrict features based on target environment capabilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -26,12 +26,12 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "yaml": "^2.7.0",
29
- "@formspec/core": "0.1.0-alpha.21"
29
+ "@formspec/core": "0.1.0-alpha.23"
30
30
  },
31
31
  "devDependencies": {
32
32
  "tsd": "^0.31.0",
33
33
  "vitest": "^3.0.0",
34
- "@formspec/dsl": "0.1.0-alpha.21"
34
+ "@formspec/dsl": "0.1.0-alpha.23"
35
35
  },
36
36
  "tsd": {
37
37
  "directory": "src/__tests__"
@@ -48,13 +48,13 @@
48
48
  ],
49
49
  "license": "UNLICENSED",
50
50
  "scripts": {
51
- "build": "tsup && tsc --emitDeclarationOnly && api-extractor run --local",
51
+ "build": "tsup && tsc --emitDeclarationOnly && pnpm run api-extractor:local",
52
52
  "clean": "rm -rf dist temp",
53
53
  "typecheck": "tsc --noEmit",
54
54
  "test": "vitest run",
55
55
  "test:types": "tsd",
56
- "api-extractor": "api-extractor run",
57
- "api-extractor:local": "api-extractor run --local",
58
- "api-documenter": "api-documenter markdown -i temp -o docs"
56
+ "api-extractor": "api-extractor run && node ../../scripts/normalize-generated-markdown.mjs api-report",
57
+ "api-extractor:local": "api-extractor run --local && node ../../scripts/normalize-generated-markdown.mjs api-report",
58
+ "api-documenter": "api-documenter markdown -i temp -o docs && node ../../scripts/normalize-generated-markdown.mjs docs"
59
59
  }
60
60
  }