package pipeline import ( "context" "fmt" "reflect" "sort" "strings" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) type ValidatorSpec struct { Key string `json:"key"` ExecutionClass contracts.ExecutionClass `json:"execution_class"` } type SerializedValidatorSpec struct { ValidatorSpec SupportsChunks bool `json:"supports_chunks,omitempty"` SupportsArtifacts bool `json:"supports_artifacts,omitempty"` } type ValidatorTarget string const ( ValidatorTargetChunk ValidatorTarget = "chunk" ValidatorTargetSerialized ValidatorTarget = "serialized" ValidatorTargetTyped ValidatorTarget = "typed" ) type ValidatorRegistry struct { typedEntries map[artifactVariantKey]typedValidatorEntry chunkEntries map[string]chunkValidatorEntry serializedEntries map[string]serializedValidatorEntry } type typedValidatorEntry struct { spec ValidatorSpec kind contracts.ArtifactKind valueType reflect.Type validateOptions OptionValidator builder func(BuildRequest) (any, error) validate typedValidateOperation } type chunkValidatorEntry struct { spec ValidatorSpec validateOptions OptionValidator builder func(BuildRequest) (contracts.ChunkValidator, error) } type serializedValidatorEntry struct { spec SerializedValidatorSpec validateOptions OptionValidator builder func(BuildRequest) (contracts.SerializedValidator, error) } func NewValidatorRegistry() *ValidatorRegistry { return &ValidatorRegistry{ typedEntries: make(map[artifactVariantKey]typedValidatorEntry), chunkEntries: make(map[string]chunkValidatorEntry), serializedEntries: make(map[string]serializedValidatorEntry), } } func RegisterTypedValidator[T any](registry *ValidatorRegistry, kind contracts.ArtifactKind, spec ValidatorSpec, constructor func() (contracts.TypedValidator[T], error)) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return RegisterTypedValidatorBuilder(registry, kind, spec, rejectUnconfiguredOptions, func(BuildRequest) (contracts.TypedValidator[T], error) { return constructor() }) } func RegisterTypedValidatorBuilder[T any](registry *ValidatorRegistry, kind contracts.ArtifactKind, spec ValidatorSpec, validateOptions OptionValidator, builder func(BuildRequest) (contracts.TypedValidator[T], error)) error { if registry == nil { return fmt.Errorf("validator registry must not be nil") } normalizedSpec, err := normalizeValidatorSpec(spec) if err != nil { return err } kind = normalizeArtifactKind(kind) if kind == "" { return fmt.Errorf("typed validator %q artifact kind must not be empty", normalizedSpec.Key) } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", normalizedSpec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", normalizedSpec.Key) } key := artifactVariantKey{module: normalizedSpec.Key, kind: kind} if _, ok := registry.typedEntries[key]; ok { return fmt.Errorf("validator %q variant for artifact kind %q is already registered", key.module, key.kind) } if registry.typedEntries == nil { registry.typedEntries = make(map[artifactVariantKey]typedValidatorEntry) } registry.typedEntries[key] = typedValidatorEntry{ spec: normalizedSpec, kind: kind, valueType: reflect.TypeFor[T](), validateOptions: validateOptions, builder: func(request BuildRequest) (any, error) { return builder(cloneBuildRequest(request)) }, validate: func(ctx context.Context, implementation any, target typedValidationTarget) (contracts.ValidationResult, error) { validator, ok := implementation.(contracts.TypedValidator[T]) if !ok { return contracts.ValidationResult{}, fmt.Errorf("validator %q has incompatible implementation %T", normalizedSpec.Key, implementation) } value, err := exactTypedValue[T]("validate artifact value", target.value) if err != nil { return contracts.ValidationResult{}, err } return validator.Validate(ctx, contracts.TypedValidationRequest[T]{Stage: string(target.stage), LaneID: target.laneID, ModuleKey: target.moduleKey, Source: target.source, SourceID: target.sourceID, SourceInput: target.sourceInput, SessionID: target.sessionID, References: target.references, LLMProfile: target.llmProfile, Metadata: target.metadata, Chunk: target.chunk, Chunks: target.chunks, Ref: target.ref, Value: value}) }, } return nil } func RegisterChunkValidator(registry *ValidatorRegistry, spec ValidatorSpec, constructor func() (contracts.ChunkValidator, error)) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return RegisterChunkValidatorBuilder(registry, spec, rejectUnconfiguredOptions, func(BuildRequest) (contracts.ChunkValidator, error) { return constructor() }) } func RegisterChunkValidatorBuilder(registry *ValidatorRegistry, spec ValidatorSpec, validateOptions OptionValidator, builder func(BuildRequest) (contracts.ChunkValidator, error)) error { if registry == nil { return fmt.Errorf("validator registry must not be nil") } normalizedSpec, err := normalizeValidatorSpec(spec) if err != nil { return err } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", normalizedSpec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", normalizedSpec.Key) } if _, ok := registry.chunkEntries[normalizedSpec.Key]; ok { return fmt.Errorf("chunk validator %q is already registered", normalizedSpec.Key) } if registry.chunkEntries == nil { registry.chunkEntries = make(map[string]chunkValidatorEntry) } registry.chunkEntries[normalizedSpec.Key] = chunkValidatorEntry{spec: normalizedSpec, validateOptions: validateOptions, builder: builder} return nil } func RegisterSerializedValidator(registry *ValidatorRegistry, spec SerializedValidatorSpec, constructor func() (contracts.SerializedValidator, error)) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return RegisterSerializedValidatorBuilder(registry, spec, rejectUnconfiguredOptions, func(BuildRequest) (contracts.SerializedValidator, error) { return constructor() }) } func RegisterSerializedValidatorBuilder(registry *ValidatorRegistry, spec SerializedValidatorSpec, validateOptions OptionValidator, builder func(BuildRequest) (contracts.SerializedValidator, error)) error { if registry == nil { return fmt.Errorf("validator registry must not be nil") } normalizedValidatorSpec, err := normalizeValidatorSpec(spec.ValidatorSpec) if err != nil { return err } spec.ValidatorSpec = normalizedValidatorSpec if !spec.SupportsChunks && !spec.SupportsArtifacts { return fmt.Errorf("serialized validator %q must support chunks, artifacts, or both", spec.Key) } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", spec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", spec.Key) } if _, ok := registry.serializedEntries[spec.Key]; ok { return fmt.Errorf("serialized validator %q is already registered", spec.Key) } if registry.serializedEntries == nil { registry.serializedEntries = make(map[string]serializedValidatorEntry) } registry.serializedEntries[spec.Key] = serializedValidatorEntry{spec: spec, validateOptions: validateOptions, builder: builder} return nil } func (r *ValidatorRegistry) validateOptions(resolved ResolvedValidator) error { if r == nil { return fmt.Errorf("validator registry must not be nil") } key := strings.TrimSpace(resolved.Binding.Module) var validator OptionValidator switch resolved.Target { case ValidatorTargetTyped: entry, ok := r.typedEntry(key, resolved.ArtifactKind) if ok { validator = entry.validateOptions } case ValidatorTargetChunk: entry, ok := r.chunkEntry(key) if ok { validator = entry.validateOptions } case ValidatorTargetSerialized: entry, ok := r.serializedEntry(key) if ok { validator = entry.validateOptions } } if validator == nil { return fmt.Errorf("validator %q construction entry is not registered", key) } return validateRegisteredOptions(validator, resolved.Binding.Options) } func (r *ValidatorRegistry) Spec(key string) (ValidatorSpec, bool) { if r == nil { return ValidatorSpec{}, false } normalized := strings.TrimSpace(key) if entry, found := r.chunkEntries[normalized]; found { return entry.spec, true } if entry, found := r.serializedEntries[normalized]; found { return entry.spec.ValidatorSpec, true } if kinds := r.registeredTypedKinds(normalized); len(kinds) > 0 { entry, found := r.typedEntry(normalized, kinds[0]) return entry.spec, found } return ValidatorSpec{}, false } func (r *ValidatorRegistry) typedEntry(key string, kind contracts.ArtifactKind) (typedValidatorEntry, bool) { if r == nil { return typedValidatorEntry{}, false } entry, ok := r.typedEntries[artifactVariantKey{module: strings.TrimSpace(key), kind: normalizeArtifactKind(kind)}] return entry, ok } func (r *ValidatorRegistry) chunkEntry(key string) (chunkValidatorEntry, bool) { if r == nil { return chunkValidatorEntry{}, false } entry, ok := r.chunkEntries[strings.TrimSpace(key)] return entry, ok } func (r *ValidatorRegistry) serializedEntry(key string) (serializedValidatorEntry, bool) { if r == nil { return serializedValidatorEntry{}, false } entry, ok := r.serializedEntries[strings.TrimSpace(key)] return entry, ok } func (r *ValidatorRegistry) registeredTypedKinds(key string) []contracts.ArtifactKind { if r == nil { return nil } module := strings.TrimSpace(key) kinds := make([]contracts.ArtifactKind, 0) for variant := range r.typedEntries { if variant.module == module { kinds = append(kinds, variant.kind) } } sortArtifactKinds(kinds) return kinds } func (r *ValidatorRegistry) RegisteredSpecs() []ValidatorSpec { if r == nil { return nil } keys := r.RegisteredKeys() specs := make([]ValidatorSpec, 0, len(keys)) for _, key := range keys { if spec, ok := r.Spec(key); ok { specs = append(specs, spec) } } return specs } func (r *ValidatorRegistry) RegisteredKeys() []string { if r == nil { return nil } keys := make(map[string]struct{}) for key := range r.typedEntries { keys[key.module] = struct{}{} } for key := range r.chunkEntries { keys[key] = struct{}{} } for key := range r.serializedEntries { keys[key] = struct{}{} } return sortedRegistryKeys(keys) } func normalizeValidatorSpec(spec ValidatorSpec) (ValidatorSpec, error) { normalized := ValidatorSpec{Key: strings.TrimSpace(spec.Key), ExecutionClass: spec.ExecutionClass} if normalized.Key == "" { return ValidatorSpec{}, fmt.Errorf("validator key must not be empty") } switch normalized.ExecutionClass { case contracts.ExecutionClassDeterministic, contracts.ExecutionClassLLMBacked: default: return ValidatorSpec{}, fmt.Errorf("validator %q execution class %q is not supported", normalized.Key, normalized.ExecutionClass) } return normalized, nil } func sortValidatorSpecs(specs []ValidatorSpec) { sort.Slice(specs, func(i, j int) bool { return specs[i].Key < specs[j].Key }) }