2026-07-03 10:01:28 -05:00
package pipeline
2026-07-03 12:50:09 +00:00
import (
"context"
"fmt"
2026-07-04 00:45:21 +00:00
"path"
"strings"
"time"
2026-07-03 12:50:09 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
2026-07-03 10:01:28 -05:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/validate"
2026-07-03 12:50:09 +00:00
)
2026-07-03 15:46:42 +00:00
type Registries struct {
Inputs * InputAdapterRegistry
Chunkers * ChunkerRegistry
Extractors * ExtractorRegistry
Mergers * MergerRegistry
Normalizers * NormalizerRegistry
Validators * ValidatorRegistry
Outputs * OutputEncoderRegistry
2026-07-03 12:50:09 +00:00
}
type Runner struct {
2026-07-03 15:46:42 +00:00
registries Registries
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
func New ( registries Registries ) * Runner {
return & Runner { registries : registries }
2026-07-03 12:50:09 +00:00
}
type RunInput struct {
2026-07-04 00:45:21 +00:00
Pipeline ResolvedPipeline
SourceID string
Path string
RawInput [ ] byte
LLMClient contracts . StructuredLLMClient
RunID string
StartedAt time . Time
LLMProfiles [ ] artifacts . LLMProfileManifest
Metadata map [ string ] any
2026-07-03 12:50:09 +00:00
}
type RunOutput struct {
2026-07-04 00:45:21 +00:00
Manifest artifacts . RunManifest ` json:"manifest" `
Approved [ ] artifacts . Artifact ` json:"approved,omitempty" `
Rejected [ ] artifacts . RejectedArtifact ` json:"rejected,omitempty" `
Warnings [ ] contracts . Warning ` json:"warnings,omitempty" `
OutputFiles [ ] contracts . OutputFile ` json:"-" `
2026-07-03 12:50:09 +00:00
}
func ( r * Runner ) Run ( ctx context . Context , input RunInput ) ( RunOutput , error ) {
var output RunOutput
if r == nil {
return output , fmt . Errorf ( "runner must not be nil" )
}
2026-07-03 15:46:42 +00:00
if err := validateRunInput ( input ) ; err != nil {
return output , err
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
if err := r . validateRegistries ( input . Pipeline ) ; err != nil {
return output , err
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
2026-07-04 00:45:21 +00:00
output . Manifest = manifestFromPipeline ( input )
2026-07-03 15:46:42 +00:00
adapter , err := r . registries . Inputs . Build ( input . Pipeline . Input . Module )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "build input adapter %q: %w" , input . Pipeline . Input . Module , err )
}
2026-07-05 13:23:56 +00:00
attachModuleManifestMetadata ( & output , "input" , adapter )
2026-07-03 15:46:42 +00:00
doc , err := adapter . Parse ( ctx , contracts . ParseRequest {
2026-07-03 11:26:49 -05:00
SourceID : input . SourceID ,
Path : input . Path ,
Raw : input . RawInput ,
LLMProfile : input . Pipeline . Input . LLMProfile ,
Options : cloneOptions ( input . Pipeline . Input . Options ) ,
Metadata : input . Metadata ,
2026-07-03 15:46:42 +00:00
} )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "parse input with adapter %q: %w" , adapter . Key ( ) , err )
}
if err := source . ValidateDocument ( doc ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "validate source document: %w" , err )
}
output . Manifest . SourceDigests = [ ] string { doc . Digest }
chunker , err := r . registries . Chunkers . Build ( input . Pipeline . Chunk . Module )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "build chunker %q: %w" , input . Pipeline . Chunk . Module , err )
}
2026-07-05 13:23:56 +00:00
attachModuleManifestMetadata ( & output , "chunker" , chunker )
2026-07-03 15:46:42 +00:00
chunkResult , err := chunker . Chunk ( ctx , contracts . ChunkRequest {
2026-07-03 11:26:49 -05:00
Source : doc ,
2026-07-04 12:52:16 +00:00
LLMClient : input . LLMClient ,
2026-07-03 11:26:49 -05:00
LLMProfile : input . Pipeline . Chunk . LLMProfile ,
Options : cloneOptions ( input . Pipeline . Chunk . Options ) ,
Metadata : input . Metadata ,
2026-07-03 15:46:42 +00:00
} )
output . Warnings = append ( output . Warnings , chunkResult . Warnings ... )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "chunk source with chunker %q: %w" , chunker . Key ( ) , err )
}
if len ( chunkResult . Chunks ) == 0 {
return failOutput ( output ) , fmt . Errorf ( "chunker %q returned no chunks" , chunker . Key ( ) )
2026-07-03 12:50:09 +00:00
}
2026-07-05 13:28:09 +00:00
canonicalChunks , err := validateAndCanonicalizeChunkResult ( doc , chunkResult . Chunks )
if err != nil {
2026-07-04 12:52:16 +00:00
return failOutput ( output ) , fmt . Errorf ( "validate chunks from chunker %q: %w" , chunker . Key ( ) , err )
}
2026-07-03 12:50:09 +00:00
nextCandidateIndex := 0
2026-07-03 15:46:42 +00:00
for _ , lane := range input . Pipeline . ArtifactLanes {
2026-07-05 13:28:09 +00:00
if err := r . runLane ( ctx , input , doc , canonicalChunks , lane , & output , & nextCandidateIndex ) ; err != nil {
2026-07-03 15:46:42 +00:00
return failOutput ( output ) , err
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
}
if len ( output . Rejected ) > 0 {
output . Manifest . ValidationStatus = "rejected"
} else {
output . Manifest . ValidationStatus = "approved"
}
2026-07-04 00:45:21 +00:00
output . Manifest . CompletedAt = timePtr ( time . Now ( ) . UTC ( ) )
2026-07-03 15:46:42 +00:00
encoder , err := r . registries . Outputs . Build ( input . Pipeline . Output . Module )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "build output encoder %q: %w" , input . Pipeline . Output . Module , err )
}
2026-07-05 13:23:56 +00:00
attachModuleManifestMetadata ( & output , "output" , encoder )
2026-07-03 15:46:42 +00:00
encoded , err := encoder . Encode ( ctx , contracts . OutputRequest {
2026-07-03 11:26:49 -05:00
Manifest : output . Manifest ,
Approved : output . Approved ,
Rejected : output . Rejected ,
Warnings : output . Warnings ,
LLMProfile : input . Pipeline . Output . LLMProfile ,
Options : cloneOptions ( input . Pipeline . Output . Options ) ,
Metadata : input . Metadata ,
2026-07-03 15:46:42 +00:00
} )
output . Warnings = append ( output . Warnings , encoded . Warnings ... )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "encode output with encoder %q: %w" , encoder . Key ( ) , err )
}
2026-07-04 00:45:21 +00:00
files , err := outputFilesFromResult ( encoded )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "validate output files from encoder %q: %w" , encoder . Key ( ) , err )
}
output . OutputFiles = files
2026-07-03 15:46:42 +00:00
return output , nil
}
func ( r * Runner ) runLane ( ctx context . Context , input RunInput , doc * source . SourceDocument , chunks [ ] contracts . SourceChunk , lane ResolvedArtifactLane , output * RunOutput , nextCandidateIndex * int ) error {
extractor , err := r . registries . Extractors . Build ( lane . Extract . Module )
if err != nil {
return fmt . Errorf ( "build extractor %q for lane %q: %w" , lane . Extract . Module , lane . ID , err )
}
merger , err := r . registries . Mergers . Build ( lane . Merge . Module )
if err != nil {
return fmt . Errorf ( "build merger %q for lane %q: %w" , lane . Merge . Module , lane . ID , err )
}
normalizer , err := r . registries . Normalizers . Build ( lane . Normalize . Module )
if err != nil {
return fmt . Errorf ( "build normalizer %q for lane %q: %w" , lane . Normalize . Module , lane . ID , err )
}
2026-07-04 00:45:21 +00:00
setLaneManifestMetadata ( output , lane . ID , extractor , merger , normalizer )
2026-07-03 15:46:42 +00:00
2026-07-03 11:26:49 -05:00
var validators [ ] validatorExecution
2026-07-03 15:46:42 +00:00
if len ( lane . Validators ) > 0 {
validators , err = r . buildConfiguredValidators ( lane )
if err != nil {
return err
2026-07-03 08:26:51 -05:00
}
2026-07-03 15:46:42 +00:00
} else {
2026-07-03 11:26:49 -05:00
for _ , validator := range extractor . Validators ( ) {
validators = append ( validators , validatorExecution { validator : validator } )
}
2026-07-03 15:46:42 +00:00
}
2026-07-03 12:50:09 +00:00
2026-07-03 15:46:42 +00:00
chunkArtifacts := make ( [ ] contracts . ChunkArtifacts , 0 , len ( chunks ) )
for index := range chunks {
chunk := chunks [ index ]
2026-07-03 12:50:09 +00:00
result , err := extractor . Extract ( ctx , contracts . ExtractionRequest {
2026-07-03 11:26:49 -05:00
Source : doc ,
Chunk : & chunk ,
LLMClient : input . LLMClient ,
LLMProfile : lane . Extract . LLMProfile ,
Options : cloneOptions ( lane . Extract . Options ) ,
Metadata : input . Metadata ,
2026-07-03 12:50:09 +00:00
} )
output . Warnings = append ( output . Warnings , result . Warnings ... )
if err != nil {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "extract lane %q chunk %q with extractor %q: %w" , lane . ID , chunk . ID , extractor . Key ( ) , err )
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
candidates , err := normalizeCandidates ( extractor , result . Candidates , nextCandidateIndex )
2026-07-03 12:50:09 +00:00
if err != nil {
2026-07-03 15:46:42 +00:00
return err
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
chunkArtifacts = append ( chunkArtifacts , contracts . ChunkArtifacts {
Chunk : chunk ,
Candidates : candidates ,
} )
}
mergeResult , err := merger . Merge ( ctx , contracts . MergeRequest {
Source : doc ,
LaneID : lane . ID ,
ChunkArtifacts : chunkArtifacts ,
2026-07-03 11:26:49 -05:00
LLMProfile : lane . Merge . LLMProfile ,
Options : cloneOptions ( lane . Merge . Options ) ,
2026-07-03 15:46:42 +00:00
Metadata : input . Metadata ,
} )
output . Warnings = append ( output . Warnings , mergeResult . Warnings ... )
if err != nil {
return fmt . Errorf ( "merge lane %q with merger %q: %w" , lane . ID , merger . Key ( ) , err )
}
normalizeResult , err := normalizer . Normalize ( ctx , contracts . NormalizeRequest {
Source : doc ,
LaneID : lane . ID ,
Candidates : mergeResult . Candidates ,
2026-07-03 11:26:49 -05:00
LLMProfile : lane . Normalize . LLMProfile ,
Options : cloneOptions ( lane . Normalize . Options ) ,
2026-07-03 15:46:42 +00:00
Metadata : input . Metadata ,
} )
output . Warnings = append ( output . Warnings , normalizeResult . Warnings ... )
if err != nil {
return fmt . Errorf ( "normalize lane %q with normalizer %q: %w" , lane . ID , normalizer . Key ( ) , err )
}
2026-07-03 11:26:49 -05:00
if err := validateCandidateEnvelope ( extractor , normalizeResult . Candidates ) ; err != nil {
return fmt . Errorf ( "validate normalized candidates for lane %q: %w" , lane . ID , err )
}
2026-07-03 15:46:42 +00:00
approved , rejected , warnings , err := runValidators ( ctx , extractor . Key ( ) , validators , doc , normalizeResult . Candidates , input . Metadata )
output . Warnings = append ( output . Warnings , warnings ... )
output . Rejected = append ( output . Rejected , rejected ... )
if err != nil {
return err
}
for _ , candidate := range approved {
output . Approved = append ( output . Approved , artifacts . ArtifactFromCandidate ( candidate ) )
}
return nil
}
2026-07-03 12:50:09 +00:00
2026-07-03 11:26:49 -05:00
type validatorExecution struct {
validator contracts . Validator
binding ModuleBinding
}
func ( r * Runner ) buildConfiguredValidators ( lane ResolvedArtifactLane ) ( [ ] validatorExecution , error ) {
validators := make ( [ ] validatorExecution , 0 , len ( lane . Validators ) )
2026-07-03 15:46:42 +00:00
for _ , binding := range lane . Validators {
validator , err := r . registries . Validators . Build ( binding . Module )
2026-07-03 12:50:09 +00:00
if err != nil {
2026-07-03 15:46:42 +00:00
return nil , fmt . Errorf ( "build validator %q for lane %q: %w" , binding . Module , lane . ID , err )
2026-07-03 12:50:09 +00:00
}
2026-07-03 11:26:49 -05:00
validators = append ( validators , validatorExecution {
validator : validator ,
binding : binding ,
} )
2026-07-03 15:46:42 +00:00
}
return validators , nil
}
func ( r * Runner ) validateRegistries ( pipeline ResolvedPipeline ) error {
if r . registries . Inputs == nil {
return fmt . Errorf ( "input registry must not be nil" )
}
if r . registries . Chunkers == nil {
return fmt . Errorf ( "chunker registry must not be nil" )
}
if r . registries . Extractors == nil {
return fmt . Errorf ( "extractor registry must not be nil" )
}
if r . registries . Mergers == nil {
return fmt . Errorf ( "merger registry must not be nil" )
}
if r . registries . Normalizers == nil {
return fmt . Errorf ( "normalizer registry must not be nil" )
}
if r . registries . Outputs == nil {
return fmt . Errorf ( "output encoder registry must not be nil" )
}
if pipelineUsesConfiguredValidators ( pipeline ) && r . registries . Validators == nil {
return fmt . Errorf ( "validator registry must not be nil" )
}
return nil
}
func validateRunInput ( input RunInput ) error {
if input . Pipeline . ID == "" {
return fmt . Errorf ( "resolved pipeline id must not be empty" )
}
if input . Pipeline . Digest == "" {
return fmt . Errorf ( "resolved pipeline digest must not be empty" )
}
if input . Pipeline . Input . Module == "" {
return fmt . Errorf ( "resolved pipeline input module must not be empty" )
}
if input . Pipeline . Chunk . Module == "" {
return fmt . Errorf ( "resolved pipeline chunk module must not be empty" )
}
if input . Pipeline . Output . Module == "" {
return fmt . Errorf ( "resolved pipeline output module must not be empty" )
}
if len ( input . Pipeline . ArtifactLanes ) == 0 {
return fmt . Errorf ( "resolved pipeline artifact lanes must not be empty" )
}
for _ , lane := range input . Pipeline . ArtifactLanes {
if lane . ID == "" {
return fmt . Errorf ( "resolved pipeline artifact lane id must not be empty" )
}
if lane . Extract . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q extract module must not be empty" , lane . ID )
}
if lane . Merge . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q merge module must not be empty" , lane . ID )
}
if lane . Normalize . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q normalize module must not be empty" , lane . ID )
}
for _ , validator := range lane . Validators {
if validator . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q validator module must not be empty" , lane . ID )
}
}
}
return nil
}
2026-07-04 00:45:21 +00:00
func manifestFromPipeline ( input RunInput ) artifacts . RunManifest {
startedAt := input . StartedAt
if startedAt . IsZero ( ) {
startedAt = time . Now ( ) . UTC ( )
}
runID := strings . TrimSpace ( input . RunID )
if runID == "" {
runID = fmt . Sprintf ( "run-%d" , startedAt . UnixNano ( ) )
}
pipeline := input . Pipeline
2026-07-03 15:46:42 +00:00
manifest := artifacts . RunManifest {
PipelineID : pipeline . ID ,
PipelineDigest : pipeline . Digest ,
InputModule : pipeline . Input . Module ,
Chunker : pipeline . Chunk . Module ,
OutputEncoder : pipeline . Output . Module ,
ArtifactLanes : make ( [ ] artifacts . ArtifactLaneManifest , 0 , len ( pipeline . ArtifactLanes ) ) ,
2026-07-04 00:45:21 +00:00
RunID : runID ,
StartedAt : timePtr ( startedAt ) ,
LLMProfiles : cloneLLMProfiles ( input . LLMProfiles ) ,
2026-07-03 15:46:42 +00:00
}
2026-07-03 12:50:09 +00:00
2026-07-03 15:46:42 +00:00
for _ , lane := range pipeline . ArtifactLanes {
laneManifest := artifacts . ArtifactLaneManifest {
ID : lane . ID ,
Extractor : lane . Extract . Module ,
Merger : lane . Merge . Module ,
Normalizer : lane . Normalize . Module ,
}
for _ , validator := range lane . Validators {
laneManifest . Validators = append ( laneManifest . Validators , validator . Module )
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
manifest . ArtifactLanes = append ( manifest . ArtifactLanes , laneManifest )
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
return manifest
}
2026-07-03 12:50:09 +00:00
2026-07-03 15:46:42 +00:00
func failOutput ( output RunOutput ) RunOutput {
if output . Manifest . PipelineID != "" {
output . Manifest . ValidationStatus = "failed"
2026-07-04 00:45:21 +00:00
output . Manifest . CompletedAt = timePtr ( time . Now ( ) . UTC ( ) )
2026-07-03 15:46:42 +00:00
}
return output
}
2026-07-04 00:45:21 +00:00
func setLaneManifestMetadata ( output * RunOutput , laneID string , modules ... any ) {
if output == nil {
return
}
for i := range output . Manifest . ArtifactLanes {
if output . Manifest . ArtifactLanes [ i ] . ID != laneID {
continue
}
metadata := make ( map [ string ] any )
for _ , module := range modules {
2026-07-05 13:23:56 +00:00
moduleMetadata , ok := moduleManifestMetadata ( module )
2026-07-04 00:45:21 +00:00
if ! ok {
continue
}
key := manifestMetadataKey ( module )
if key == "" {
continue
}
metadata [ key ] = moduleMetadata
}
if len ( metadata ) > 0 {
output . Manifest . ArtifactLanes [ i ] . Metadata = metadata
}
return
}
}
2026-07-05 13:23:56 +00:00
func attachModuleManifestMetadata ( output * RunOutput , moduleKey string , module any ) {
if output == nil {
return
}
moduleMetadata , ok := moduleManifestMetadata ( module )
if ! ok {
return
}
if output . Manifest . ModuleMetadata == nil {
output . Manifest . ModuleMetadata = make ( map [ string ] map [ string ] any )
}
output . Manifest . ModuleMetadata [ moduleKey ] = moduleMetadata
}
2026-07-04 00:45:21 +00:00
func manifestMetadataKey ( module any ) string {
switch module . ( type ) {
case contracts . Extractor :
return "extractor"
case contracts . Merger :
return "merger"
case contracts . Normalizer :
return "normalizer"
default :
return ""
}
}
2026-07-05 13:23:56 +00:00
func moduleManifestMetadata ( module any ) ( map [ string ] any , bool ) {
provider , ok := module . ( contracts . ManifestMetadataProvider )
if ! ok {
return nil , false
}
moduleMetadata := cloneMetadata ( provider . ManifestMetadata ( ) )
if len ( moduleMetadata ) == 0 {
return nil , false
}
return moduleMetadata , true
}
2026-07-04 00:45:21 +00:00
func outputFilesFromResult ( result contracts . OutputResult ) ( [ ] contracts . OutputFile , error ) {
2026-07-03 21:26:05 -05:00
out := make ( [ ] contracts . OutputFile , 0 , len ( result . Files ) )
for _ , file := range result . Files {
2026-07-04 00:45:21 +00:00
if err := validateOutputFileName ( file . Name ) ; err != nil {
return nil , err
}
out = append ( out , contracts . OutputFile {
Name : file . Name ,
ContentType : file . ContentType ,
Bytes : append ( [ ] byte ( nil ) , file . Bytes ... ) ,
} )
}
return out , nil
}
func validateOutputFileName ( name string ) error {
if strings . TrimSpace ( name ) == "" {
return fmt . Errorf ( "output file name must not be empty" )
}
if strings . Contains ( name , "\\" ) {
return fmt . Errorf ( "output file name %q must use slash-separated relative paths" , name )
}
if path . IsAbs ( name ) {
return fmt . Errorf ( "output file name %q must be relative" , name )
}
if strings . Contains ( name , ".." ) {
return fmt . Errorf ( "output file name %q must not contain .." , name )
}
cleaned := path . Clean ( name )
if cleaned == "." || cleaned != name {
return fmt . Errorf ( "output file name %q must be clean" , name )
}
return nil
}
func cloneMetadata ( metadata map [ string ] any ) map [ string ] any {
if len ( metadata ) == 0 {
return nil
}
out := make ( map [ string ] any , len ( metadata ) )
for key , value := range metadata {
out [ key ] = value
}
return out
}
func cloneLLMProfiles ( profiles [ ] artifacts . LLMProfileManifest ) [ ] artifacts . LLMProfileManifest {
if len ( profiles ) == 0 {
return nil
}
return append ( [ ] artifacts . LLMProfileManifest ( nil ) , profiles ... )
}
func timePtr ( t time . Time ) * time . Time {
return & t
}
2026-07-03 15:46:42 +00:00
func pipelineUsesConfiguredValidators ( pipeline ResolvedPipeline ) bool {
for _ , lane := range pipeline . ArtifactLanes {
if len ( lane . Validators ) > 0 {
return true
}
}
return false
2026-07-03 12:50:09 +00:00
}
2026-07-03 09:03:45 -05:00
func normalizeCandidates ( extractor contracts . Extractor , candidates [ ] artifacts . ArtifactCandidate , nextIndex * int ) ( [ ] artifacts . ArtifactCandidate , error ) {
normalized := make ( [ ] artifacts . ArtifactCandidate , 0 , len ( candidates ) )
2026-07-03 12:50:09 +00:00
for _ , candidate := range candidates {
candidate . Index = * nextIndex
* nextIndex = * nextIndex + 1
if candidate . ExtractorKey == "" {
candidate . ExtractorKey = extractor . Key ( )
} else if candidate . ExtractorKey != extractor . Key ( ) {
return nil , fmt . Errorf ( "candidate extractor_key %q does not match extractor %q" , candidate . ExtractorKey , extractor . Key ( ) )
}
if candidate . ArtifactType == "" {
candidate . ArtifactType = extractor . ArtifactType ( )
} else if candidate . ArtifactType != extractor . ArtifactType ( ) {
return nil , fmt . Errorf ( "candidate artifact_type %q does not match extractor %q artifact type %q" , candidate . ArtifactType , extractor . Key ( ) , extractor . ArtifactType ( ) )
}
if candidate . SchemaVersion == "" {
candidate . SchemaVersion = extractor . SchemaVersion ( )
} else if candidate . SchemaVersion != extractor . SchemaVersion ( ) {
return nil , fmt . Errorf ( "candidate schema_version %q does not match extractor %q schema version %q" , candidate . SchemaVersion , extractor . Key ( ) , extractor . SchemaVersion ( ) )
}
normalized = append ( normalized , candidate )
}
return normalized , nil
}
2026-07-03 11:26:49 -05:00
func validateCandidateEnvelope ( extractor contracts . Extractor , candidates [ ] artifacts . ArtifactCandidate ) error {
seen := make ( map [ int ] struct { } , len ( candidates ) )
for _ , candidate := range candidates {
if _ , ok := seen [ candidate . Index ] ; ok {
return fmt . Errorf ( "candidate index %d is duplicated" , candidate . Index )
}
seen [ candidate . Index ] = struct { } { }
if candidate . ExtractorKey == "" {
return fmt . Errorf ( "candidate index %d extractor_key must not be empty" , candidate . Index )
}
if candidate . ExtractorKey != extractor . Key ( ) {
return fmt . Errorf ( "candidate index %d extractor_key %q does not match extractor %q" , candidate . Index , candidate . ExtractorKey , extractor . Key ( ) )
}
if candidate . ArtifactType == "" {
return fmt . Errorf ( "candidate index %d artifact_type must not be empty" , candidate . Index )
}
if candidate . ArtifactType != extractor . ArtifactType ( ) {
return fmt . Errorf ( "candidate index %d artifact_type %q does not match extractor %q artifact type %q" , candidate . Index , candidate . ArtifactType , extractor . Key ( ) , extractor . ArtifactType ( ) )
}
if candidate . SchemaVersion == "" {
return fmt . Errorf ( "candidate index %d schema_version must not be empty" , candidate . Index )
}
if candidate . SchemaVersion != extractor . SchemaVersion ( ) {
return fmt . Errorf ( "candidate index %d schema_version %q does not match extractor %q schema version %q" , candidate . Index , candidate . SchemaVersion , extractor . Key ( ) , extractor . SchemaVersion ( ) )
}
}
return nil
}
func runValidators ( ctx context . Context , extractorKey string , validators [ ] validatorExecution , doc * source . SourceDocument , candidates [ ] artifacts . ArtifactCandidate , metadata map [ string ] any ) ( [ ] artifacts . ArtifactCandidate , [ ] artifacts . RejectedArtifact , [ ] contracts . Warning , error ) {
2026-07-03 12:50:09 +00:00
eligible := candidates
var rejected [ ] artifacts . RejectedArtifact
var warnings [ ] contracts . Warning
2026-07-03 11:26:49 -05:00
for validatorIndex , execution := range validators {
validator := execution . validator
2026-07-03 08:26:51 -05:00
if validator == nil {
2026-07-03 15:46:42 +00:00
return nil , rejected , warnings , fmt . Errorf ( "extractor %q validator[%d] must not be nil" , extractorKey , validatorIndex )
2026-07-03 08:26:51 -05:00
}
2026-07-03 12:50:09 +00:00
result , err := validator . Validate ( ctx , contracts . ValidationRequest {
Source : doc ,
Candidates : eligible ,
2026-07-03 11:26:49 -05:00
LLMProfile : execution . binding . LLMProfile ,
Options : cloneOptions ( execution . binding . Options ) ,
2026-07-03 12:50:09 +00:00
Metadata : metadata ,
} )
warnings = append ( warnings , result . Warnings ... )
if err != nil {
2026-07-03 15:46:42 +00:00
return nil , rejected , warnings , fmt . Errorf ( "validate extractor %q with validator %q: %w" , extractorKey , validator . Name ( ) , err )
2026-07-03 12:50:09 +00:00
}
if result . ValidatorName != validator . Name ( ) {
return nil , rejected , warnings , fmt . Errorf ( "validator %q returned result for %q" , validator . Name ( ) , result . ValidatorName )
}
2026-07-03 10:01:28 -05:00
if err := validate . EnforceDecisionCardinality ( eligible , result . Decisions ) ; err != nil {
2026-07-03 15:46:42 +00:00
return nil , rejected , warnings , fmt . Errorf ( "validate extractor %q with validator %q: %w" , extractorKey , validator . Name ( ) , err )
2026-07-03 12:50:09 +00:00
}
decisions := make ( map [ int ] contracts . ValidationDecision , len ( result . Decisions ) )
for _ , decision := range result . Decisions {
decisions [ decision . CandidateIndex ] = decision
}
2026-07-03 09:03:45 -05:00
nextEligible := make ( [ ] artifacts . ArtifactCandidate , 0 , len ( eligible ) )
2026-07-03 12:50:09 +00:00
for _ , candidate := range eligible {
decision := decisions [ candidate . Index ]
if decision . Approved {
nextEligible = append ( nextEligible , candidate )
continue
}
rejected = append ( rejected , artifacts . RejectedArtifact {
Candidate : candidate ,
ValidatorName : result . ValidatorName ,
ReasonCode : decision . ReasonCode ,
Message : decision . Message ,
} )
}
eligible = nextEligible
}
return eligible , rejected , warnings , nil
}