feat: validacion de integridad para funciones y tipos
Implementa todas las reglas de docs/integrity.md: - Pipeline: siempre impuro, uses_functions no vacio - Pura: sin returns_optional, sin error_type - Impura: error_type obligatorio - Tested: coherencia entre tested, tests y test_file_path - Component: framework obligatorio, returns vacio, has_state->impure - file_path siempre relativa - Referencias cruzadas: uses_functions, uses_types, returns, error_type deben apuntar a IDs existentes - Types: algebraic valido, sin auto-referencias, refs validadas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user