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 } // ValidateProposal checks integrity rules for proposals. func ValidateProposal(p *Proposal) *ValidationError { var errs []string if p.ID == "" { errs = append(errs, "id is required") } if p.Title == "" { errs = append(errs, "title is required") } switch p.Kind { case ProposalNewFunction, ProposalNewType, ProposalImproveFunction, ProposalImproveType, ProposalNewPipeline: case "": errs = append(errs, "kind is required") default: errs = append(errs, fmt.Sprintf("invalid kind: %s", p.Kind)) } switch p.Status { case ProposalPending, ProposalApproved, ProposalRejected, ProposalImplemented, "": default: errs = append(errs, fmt.Sprintf("invalid status: %s", p.Status)) } if (p.Kind == ProposalImproveFunction || p.Kind == ProposalImproveType) && p.TargetID == "" { errs = append(errs, "target_id is required for improve_* proposals") } if len(errs) > 0 { return &ValidationError{ID: p.ID, Errors: errs} } return nil } // ValidateApp checks integrity rules for apps. func ValidateApp(a *App, knownFunctions, knownTypes map[string]bool) *ValidationError { var errs []string if a.Name == "" { errs = append(errs, "name is required") } if a.Lang == "" { errs = append(errs, "lang is required") } if a.Domain == "" { errs = append(errs, "domain is required") } if a.Description == "" { errs = append(errs, "description is required") } if a.DirPath != "" && strings.HasPrefix(a.DirPath, "/") { errs = append(errs, "dir_path must be relative to registry root") } for _, ref := range a.UsesFunctions { if !knownFunctions[ref] { errs = append(errs, fmt.Sprintf("uses_functions references unknown function: %s", ref)) } } for _, ref := range a.UsesTypes { if !knownTypes[ref] { errs = append(errs, fmt.Sprintf("uses_types references unknown type: %s", ref)) } } if len(errs) > 0 { return &ValidationError{ID: a.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 }