diff --git a/registry/validate.go b/registry/validate.go new file mode 100644 index 00000000..4d1b5e57 --- /dev/null +++ b/registry/validate.go @@ -0,0 +1,167 @@ +package registry + +import ( + "fmt" + "strings" +) + +// ValidationError represents one or more integrity violations. +type ValidationError struct { + ID string + Errors []string +} + +func (v *ValidationError) Error() string { + return fmt.Sprintf("%s: %s", v.ID, strings.Join(v.Errors, "; ")) +} + +// ValidateFunction checks integrity rules from docs/integrity.md. +// knownFunctions and knownTypes are sets of IDs that exist in the registry +// (including the current indexing batch). +func ValidateFunction(f *Function, knownFunctions, knownTypes map[string]bool) *ValidationError { + var errs []string + + // Required fields + if f.Name == "" { + errs = append(errs, "name is required") + } + if f.Kind == "" { + errs = append(errs, "kind is required") + } + if f.Lang == "" { + errs = append(errs, "lang is required") + } + if f.Domain == "" { + errs = append(errs, "domain is required") + } + if f.Description == "" { + errs = append(errs, "description is required") + } + + // Pipeline rules + if f.Kind == KindPipeline { + if f.Purity != PurityImpure { + errs = append(errs, "pipeline must be impure") + } + if len(f.UsesFunctions) == 0 { + errs = append(errs, "pipeline uses_functions cannot be empty") + } + } + + // Purity rules + if f.Purity == PurityPure { + if f.ReturnsOptional { + errs = append(errs, "pure function cannot have returns_optional: true (model as sum type)") + } + if f.ErrorType != "" { + errs = append(errs, "pure function cannot have error_type") + } + } + if f.Purity == PurityImpure && f.Kind != KindComponent { + if f.ErrorType == "" { + errs = append(errs, "impure function must declare error_type") + } + } + + // Tested rules + if f.Tested { + if f.TestFilePath == "" { + errs = append(errs, "tested: true requires test_file_path") + } + if len(f.Tests) == 0 { + errs = append(errs, "tested: true requires non-empty tests") + } + } else { + if len(f.Tests) > 0 { + errs = append(errs, "tested: false but tests is not empty") + } + if f.TestFilePath != "" { + errs = append(errs, "tested: false but test_file_path is set") + } + } + + // Component rules + if f.Kind == KindComponent { + if f.Framework == "" { + errs = append(errs, "component must declare framework") + } + if len(f.Returns) > 0 { + errs = append(errs, "component returns must be empty (use emits)") + } + if f.HasState != nil && *f.HasState && f.Purity != PurityImpure { + errs = append(errs, "component with has_state: true must be impure") + } + } + + // File path must be relative + if f.FilePath != "" && strings.HasPrefix(f.FilePath, "/") { + errs = append(errs, "file_path must be relative to registry root") + } + + // Reference validation + for _, ref := range f.UsesFunctions { + if !knownFunctions[ref] { + errs = append(errs, fmt.Sprintf("uses_functions references unknown function: %s", ref)) + } + } + for _, ref := range f.UsesTypes { + if !knownTypes[ref] { + errs = append(errs, fmt.Sprintf("uses_types references unknown type: %s", ref)) + } + } + for _, ref := range f.Returns { + if !knownTypes[ref] { + errs = append(errs, fmt.Sprintf("returns references unknown type: %s", ref)) + } + } + if f.ErrorType != "" { + if !knownTypes[f.ErrorType] { + errs = append(errs, fmt.Sprintf("error_type references unknown type: %s", f.ErrorType)) + } + } + + if len(errs) > 0 { + return &ValidationError{ID: f.ID, Errors: errs} + } + return nil +} + +// ValidateType checks integrity rules for types. +func ValidateType(t *Type, knownTypes map[string]bool) *ValidationError { + var errs []string + + if t.Name == "" { + errs = append(errs, "name is required") + } + if t.Lang == "" { + errs = append(errs, "lang is required") + } + if t.Domain == "" { + errs = append(errs, "domain is required") + } + if t.Description == "" { + errs = append(errs, "description is required") + } + if t.Algebraic != AlgebraicProduct && t.Algebraic != AlgebraicSum { + errs = append(errs, fmt.Sprintf("algebraic must be 'product' or 'sum', got %q", t.Algebraic)) + } + + if t.FilePath != "" && strings.HasPrefix(t.FilePath, "/") { + errs = append(errs, "file_path must be relative to registry root") + } + + // Self-reference check + for _, ref := range t.UsesTypes { + if ref == t.ID { + errs = append(errs, "type cannot reference itself in uses_types") + } + if !knownTypes[ref] { + errs = append(errs, fmt.Sprintf("uses_types references unknown type: %s", ref)) + } + } + + if len(errs) > 0 { + return &ValidationError{ID: t.ID, Errors: errs} + } + return nil +}