Refactor runner for resolved pipelines
This commit is contained in:
@@ -12,55 +12,129 @@ import (
|
|||||||
validate "gitea.maximumdirect.net/eric/notarius/internal/framework/validate"
|
validate "gitea.maximumdirect.net/eric/notarius/internal/framework/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunnerUsesExtractorRegistry(t *testing.T) {
|
func TestRunnerUsesRegistries(t *testing.T) {
|
||||||
var builtKeys []string
|
var built []string
|
||||||
var executedKeys []string
|
var executed []string
|
||||||
registry := NewExtractorRegistry()
|
registries := integrationRegistries(t, &built, &executed)
|
||||||
|
|
||||||
registerIntegrationExtractor(t, registry, "second", &builtKeys, &executedKeys, []contracts.Validator{
|
output, err := New(registries).Run(context.Background(), RunInput{
|
||||||
integrationValidator{name: "reject-second", approve: false},
|
Pipeline: integrationPipeline(),
|
||||||
})
|
SourceID: "source-1",
|
||||||
registerIntegrationExtractor(t, registry, "first", &builtKeys, &executedKeys, []contracts.Validator{
|
RawInput: []byte("source text"),
|
||||||
integrationValidator{name: "approve-first", approve: true},
|
|
||||||
})
|
|
||||||
|
|
||||||
output, err := New(registry).Run(context.Background(), RunInput{
|
|
||||||
Source: integrationSourceDocument(),
|
|
||||||
ExtractorKeys: []string{"second", "first"},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v, want nil", err)
|
t.Fatalf("Run() error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(builtKeys, []string{"second", "first"}) {
|
wantBuilt := []string{"input", "chunk", "extract-first", "merge", "normalize", "extract-second", "merge", "normalize", "output"}
|
||||||
t.Fatalf("built keys = %#v, want configured order", builtKeys)
|
if !reflect.DeepEqual(built, wantBuilt) {
|
||||||
|
t.Fatalf("built = %#v, want %#v", built, wantBuilt)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(executedKeys, []string{"second", "first"}) {
|
if !reflect.DeepEqual(executed, []string{"extract-first:chunk-0", "extract-second:chunk-0"}) {
|
||||||
t.Fatalf("executed keys = %#v, want configured order", executedKeys)
|
t.Fatalf("executed = %#v, want extractor chunk execution", executed)
|
||||||
}
|
}
|
||||||
if got := artifactKeys(output.Approved); !reflect.DeepEqual(got, []string{"first"}) {
|
if got := artifactKeys(output.Approved); !reflect.DeepEqual(got, []string{"extract-first"}) {
|
||||||
t.Fatalf("approved keys = %#v, want [first]", got)
|
t.Fatalf("approved keys = %#v, want [extract-first]", got)
|
||||||
}
|
}
|
||||||
if got := rejectedKeys(output.Rejected); !reflect.DeepEqual(got, []string{"second"}) {
|
if got := rejectedKeys(output.Rejected); !reflect.DeepEqual(got, []string{"extract-second"}) {
|
||||||
t.Fatalf("rejected keys = %#v, want [second]", got)
|
t.Fatalf("rejected keys = %#v, want [extract-second]", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerIntegrationExtractor(t *testing.T, registry *ExtractorRegistry, key string, builtKeys *[]string, executedKeys *[]string, validators []contracts.Validator) {
|
func integrationRegistries(t *testing.T, built, executed *[]string) Registries {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
registries := Registries{
|
||||||
|
Inputs: NewInputAdapterRegistry(),
|
||||||
|
Chunkers: NewChunkerRegistry(),
|
||||||
|
Extractors: NewExtractorRegistry(),
|
||||||
|
Mergers: NewMergerRegistry(),
|
||||||
|
Normalizers: NewNormalizerRegistry(),
|
||||||
|
Outputs: NewOutputEncoderRegistry(),
|
||||||
|
}
|
||||||
|
if err := registries.Inputs.Register("input", func() (contracts.InputAdapter, error) {
|
||||||
|
*built = append(*built, "input")
|
||||||
|
return integrationInput{}, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("register input: %v", err)
|
||||||
|
}
|
||||||
|
if err := registries.Chunkers.Register("chunk", func() (contracts.Chunker, error) {
|
||||||
|
*built = append(*built, "chunk")
|
||||||
|
return integrationChunker{}, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("register chunker: %v", err)
|
||||||
|
}
|
||||||
|
registerIntegrationExtractor(t, registries.Extractors, "extract-first", built, executed, []contracts.Validator{
|
||||||
|
integrationValidator{name: "approve-first", approve: true},
|
||||||
|
})
|
||||||
|
registerIntegrationExtractor(t, registries.Extractors, "extract-second", built, executed, []contracts.Validator{
|
||||||
|
integrationValidator{name: "reject-second", approve: false},
|
||||||
|
})
|
||||||
|
if err := registries.Mergers.Register("merge", func() (contracts.Merger, error) {
|
||||||
|
*built = append(*built, "merge")
|
||||||
|
return integrationMerger{}, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("register merger: %v", err)
|
||||||
|
}
|
||||||
|
if err := registries.Normalizers.Register("normalize", func() (contracts.Normalizer, error) {
|
||||||
|
*built = append(*built, "normalize")
|
||||||
|
return integrationNormalizer{}, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("register normalizer: %v", err)
|
||||||
|
}
|
||||||
|
if err := registries.Outputs.Register("output", func() (contracts.OutputEncoder, error) {
|
||||||
|
*built = append(*built, "output")
|
||||||
|
return integrationOutput{}, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("register output: %v", err)
|
||||||
|
}
|
||||||
|
return registries
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerIntegrationExtractor(t *testing.T, registry *ExtractorRegistry, key string, built, executed *[]string, validators []contracts.Validator) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if err := registry.Register(key, func() (contracts.Extractor, error) {
|
if err := registry.Register(key, func() (contracts.Extractor, error) {
|
||||||
*builtKeys = append(*builtKeys, key)
|
*built = append(*built, key)
|
||||||
return integrationExtractor{key: key, executedKeys: executedKeys, validators: validators}, nil
|
return integrationExtractor{key: key, executed: executed, validators: validators}, nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("Register(%q) error = %v, want nil", key, err)
|
t.Fatalf("Register(%q) error = %v, want nil", key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type integrationInput struct{}
|
||||||
|
|
||||||
|
func (input integrationInput) Key() string {
|
||||||
|
return "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (input integrationInput) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) {
|
||||||
|
return integrationSourceDocument(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type integrationChunker struct{}
|
||||||
|
|
||||||
|
func (chunker integrationChunker) Key() string {
|
||||||
|
return "chunk"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chunker integrationChunker) Chunk(ctx context.Context, req contracts.ChunkRequest) (contracts.ChunkResult, error) {
|
||||||
|
return contracts.ChunkResult{
|
||||||
|
Chunks: []contracts.SourceChunk{
|
||||||
|
{
|
||||||
|
ID: "chunk-0",
|
||||||
|
SourceID: req.Source.ID,
|
||||||
|
Index: 0,
|
||||||
|
Units: req.Source.Units,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type integrationExtractor struct {
|
type integrationExtractor struct {
|
||||||
key string
|
key string
|
||||||
executedKeys *[]string
|
executed *[]string
|
||||||
validators []contracts.Validator
|
validators []contracts.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (extractor integrationExtractor) Key() string {
|
func (extractor integrationExtractor) Key() string {
|
||||||
@@ -80,7 +154,7 @@ func (extractor integrationExtractor) Validators() []contracts.Validator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (extractor integrationExtractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) {
|
func (extractor integrationExtractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) {
|
||||||
*extractor.executedKeys = append(*extractor.executedKeys, extractor.key)
|
*extractor.executed = append(*extractor.executed, extractor.key+":"+req.Chunk.ID)
|
||||||
return contracts.ExtractionResult{
|
return contracts.ExtractionResult{
|
||||||
Candidates: []artifacts.ArtifactCandidate{
|
Candidates: []artifacts.ArtifactCandidate{
|
||||||
{Payload: []byte(`{"value":true}`)},
|
{Payload: []byte(`{"value":true}`)},
|
||||||
@@ -88,6 +162,40 @@ func (extractor integrationExtractor) Extract(ctx context.Context, req contracts
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type integrationNormalizer struct{}
|
||||||
|
|
||||||
|
type integrationMerger struct{}
|
||||||
|
|
||||||
|
func (merger integrationMerger) Key() string {
|
||||||
|
return "merge"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (merger integrationMerger) Merge(ctx context.Context, req contracts.MergeRequest) (contracts.MergeResult, error) {
|
||||||
|
var candidates []artifacts.ArtifactCandidate
|
||||||
|
for _, chunkArtifacts := range req.ChunkArtifacts {
|
||||||
|
candidates = append(candidates, chunkArtifacts.Candidates...)
|
||||||
|
}
|
||||||
|
return contracts.MergeResult{Candidates: candidates}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (normalizer integrationNormalizer) Key() string {
|
||||||
|
return "normalize"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (normalizer integrationNormalizer) Normalize(ctx context.Context, req contracts.NormalizeRequest) (contracts.NormalizeResult, error) {
|
||||||
|
return contracts.NormalizeResult{Candidates: req.Candidates}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type integrationOutput struct{}
|
||||||
|
|
||||||
|
func (output integrationOutput) Key() string {
|
||||||
|
return "output"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (output integrationOutput) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
|
||||||
|
return contracts.OutputResult{Bytes: []byte(`{}`), ContentType: "application/json"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type integrationValidator struct {
|
type integrationValidator struct {
|
||||||
name string
|
name string
|
||||||
approve bool
|
approve bool
|
||||||
@@ -112,6 +220,30 @@ func (validator integrationValidator) Validate(ctx context.Context, req contract
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func integrationPipeline() ResolvedPipeline {
|
||||||
|
return ResolvedPipeline{
|
||||||
|
ID: "pipeline-1",
|
||||||
|
Digest: "sha256:pipeline",
|
||||||
|
Input: Binding("input"),
|
||||||
|
Chunk: Binding("chunk"),
|
||||||
|
ArtifactLanes: []ResolvedArtifactLane{
|
||||||
|
{
|
||||||
|
ID: "first",
|
||||||
|
Extract: Binding("extract-first"),
|
||||||
|
Merge: Binding("merge"),
|
||||||
|
Normalize: Binding("normalize"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "second",
|
||||||
|
Extract: Binding("extract-second"),
|
||||||
|
Merge: Binding("merge"),
|
||||||
|
Normalize: Binding("normalize"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Output: Binding("output"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func integrationSourceDocument() *source.SourceDocument {
|
func integrationSourceDocument() *source.SourceDocument {
|
||||||
return &source.SourceDocument{
|
return &source.SourceDocument{
|
||||||
ID: "source-1",
|
ID: "source-1",
|
||||||
|
|||||||
@@ -10,29 +10,40 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/validate"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExtractorFactory interface {
|
type Registries struct {
|
||||||
Build(key string) (contracts.Extractor, error)
|
Inputs *InputAdapterRegistry
|
||||||
|
Chunkers *ChunkerRegistry
|
||||||
|
Extractors *ExtractorRegistry
|
||||||
|
Mergers *MergerRegistry
|
||||||
|
Normalizers *NormalizerRegistry
|
||||||
|
Validators *ValidatorRegistry
|
||||||
|
Outputs *OutputEncoderRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
extractors ExtractorFactory
|
registries Registries
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(extractors ExtractorFactory) *Runner {
|
func New(registries Registries) *Runner {
|
||||||
return &Runner{extractors: extractors}
|
return &Runner{registries: registries}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunInput struct {
|
type RunInput struct {
|
||||||
Source *source.SourceDocument
|
Pipeline ResolvedPipeline
|
||||||
ExtractorKeys []string
|
SourceID string
|
||||||
LLMClient contracts.StructuredLLMClient
|
Path string
|
||||||
Metadata map[string]any
|
RawInput []byte
|
||||||
|
LLMClient contracts.StructuredLLMClient
|
||||||
|
Metadata map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunOutput struct {
|
type RunOutput struct {
|
||||||
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
Manifest artifacts.RunManifest `json:"manifest"`
|
||||||
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
||||||
|
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
||||||
|
EncodedOutput []byte `json:"-"`
|
||||||
|
ContentType string `json:"content_type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
||||||
@@ -40,54 +51,283 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
return output, fmt.Errorf("runner must not be nil")
|
return output, fmt.Errorf("runner must not be nil")
|
||||||
}
|
}
|
||||||
if r.extractors == nil {
|
if err := validateRunInput(input); err != nil {
|
||||||
return output, fmt.Errorf("runner extractor factory must not be nil")
|
return output, err
|
||||||
}
|
}
|
||||||
if err := source.ValidateDocument(input.Source); err != nil {
|
if err := r.validateRegistries(input.Pipeline); err != nil {
|
||||||
return output, fmt.Errorf("validate source document: %w", err)
|
return output, err
|
||||||
}
|
}
|
||||||
if len(input.ExtractorKeys) == 0 {
|
|
||||||
return output, fmt.Errorf("extractor keys must not be empty")
|
output.Manifest = manifestFromPipeline(input.Pipeline)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
doc, err := adapter.Parse(ctx, contracts.ParseRequest{
|
||||||
|
SourceID: input.SourceID,
|
||||||
|
Path: input.Path,
|
||||||
|
Raw: input.RawInput,
|
||||||
|
Metadata: input.Metadata,
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
chunkResult, err := chunker.Chunk(ctx, contracts.ChunkRequest{
|
||||||
|
Source: doc,
|
||||||
|
Metadata: input.Metadata,
|
||||||
|
})
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
|
|
||||||
nextCandidateIndex := 0
|
nextCandidateIndex := 0
|
||||||
for _, extractorKey := range input.ExtractorKeys {
|
for _, lane := range input.Pipeline.ArtifactLanes {
|
||||||
extractor, err := r.extractors.Build(extractorKey)
|
if err := r.runLane(ctx, input, doc, chunkResult.Chunks, lane, &output, &nextCandidateIndex); err != nil {
|
||||||
if err != nil {
|
return failOutput(output), err
|
||||||
return output, fmt.Errorf("build extractor %q: %w", extractorKey, err)
|
|
||||||
}
|
|
||||||
if extractor == nil {
|
|
||||||
return output, fmt.Errorf("build extractor %q: returned nil extractor", extractorKey)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output.Rejected) > 0 {
|
||||||
|
output.Manifest.ValidationStatus = "rejected"
|
||||||
|
} else {
|
||||||
|
output.Manifest.ValidationStatus = "approved"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
encoded, err := encoder.Encode(ctx, contracts.OutputRequest{
|
||||||
|
Manifest: output.Manifest,
|
||||||
|
Approved: output.Approved,
|
||||||
|
Rejected: output.Rejected,
|
||||||
|
Warnings: output.Warnings,
|
||||||
|
Metadata: input.Metadata,
|
||||||
|
})
|
||||||
|
output.Warnings = append(output.Warnings, encoded.Warnings...)
|
||||||
|
if err != nil {
|
||||||
|
return failOutput(output), fmt.Errorf("encode output with encoder %q: %w", encoder.Key(), err)
|
||||||
|
}
|
||||||
|
output.EncodedOutput = encoded.Bytes
|
||||||
|
output.ContentType = encoded.ContentType
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var validators []contracts.Validator
|
||||||
|
if len(lane.Validators) > 0 {
|
||||||
|
validators, err = r.buildConfiguredValidators(lane)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validators = extractor.Validators()
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkArtifacts := make([]contracts.ChunkArtifacts, 0, len(chunks))
|
||||||
|
for index := range chunks {
|
||||||
|
chunk := chunks[index]
|
||||||
result, err := extractor.Extract(ctx, contracts.ExtractionRequest{
|
result, err := extractor.Extract(ctx, contracts.ExtractionRequest{
|
||||||
Source: input.Source,
|
Source: doc,
|
||||||
|
Chunk: &chunk,
|
||||||
LLMClient: input.LLMClient,
|
LLMClient: input.LLMClient,
|
||||||
Metadata: input.Metadata,
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
output.Warnings = append(output.Warnings, result.Warnings...)
|
output.Warnings = append(output.Warnings, result.Warnings...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return output, fmt.Errorf("extract with extractor %q: %w", extractor.Key(), err)
|
return fmt.Errorf("extract lane %q chunk %q with extractor %q: %w", lane.ID, chunk.ID, extractor.Key(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates, err := normalizeCandidates(extractor, result.Candidates, &nextCandidateIndex)
|
candidates, err := normalizeCandidates(extractor, result.Candidates, nextCandidateIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return output, err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
approved, rejected, warnings, err := runValidators(ctx, extractor, input.Source, candidates, input.Metadata)
|
|
||||||
output.Warnings = append(output.Warnings, warnings...)
|
|
||||||
output.Rejected = append(output.Rejected, rejected...)
|
|
||||||
if err != nil {
|
|
||||||
return output, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, candidate := range approved {
|
|
||||||
output.Approved = append(output.Approved, artifacts.ArtifactFromCandidate(candidate))
|
|
||||||
}
|
}
|
||||||
|
chunkArtifacts = append(chunkArtifacts, contracts.ChunkArtifacts{
|
||||||
|
Chunk: chunk,
|
||||||
|
Candidates: candidates,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return output, nil
|
mergeResult, err := merger.Merge(ctx, contracts.MergeRequest{
|
||||||
|
Source: doc,
|
||||||
|
LaneID: lane.ID,
|
||||||
|
ChunkArtifacts: chunkArtifacts,
|
||||||
|
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,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) buildConfiguredValidators(lane ResolvedArtifactLane) ([]contracts.Validator, error) {
|
||||||
|
validators := make([]contracts.Validator, 0, len(lane.Validators))
|
||||||
|
for _, binding := range lane.Validators {
|
||||||
|
validator, err := r.registries.Validators.Build(binding.Module)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build validator %q for lane %q: %w", binding.Module, lane.ID, err)
|
||||||
|
}
|
||||||
|
validators = append(validators, validator)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func manifestFromPipeline(pipeline ResolvedPipeline) artifacts.RunManifest {
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
manifest.ArtifactLanes = append(manifest.ArtifactLanes, laneManifest)
|
||||||
|
}
|
||||||
|
return manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func failOutput(output RunOutput) RunOutput {
|
||||||
|
if output.Manifest.PipelineID != "" {
|
||||||
|
output.Manifest.ValidationStatus = "failed"
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipelineUsesConfiguredValidators(pipeline ResolvedPipeline) bool {
|
||||||
|
for _, lane := range pipeline.ArtifactLanes {
|
||||||
|
if len(lane.Validators) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCandidates(extractor contracts.Extractor, candidates []artifacts.ArtifactCandidate, nextIndex *int) ([]artifacts.ArtifactCandidate, error) {
|
func normalizeCandidates(extractor contracts.Extractor, candidates []artifacts.ArtifactCandidate, nextIndex *int) ([]artifacts.ArtifactCandidate, error) {
|
||||||
@@ -119,14 +359,14 @@ func normalizeCandidates(extractor contracts.Extractor, candidates []artifacts.A
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runValidators(ctx context.Context, extractor contracts.Extractor, doc *source.SourceDocument, candidates []artifacts.ArtifactCandidate, metadata map[string]any) ([]artifacts.ArtifactCandidate, []artifacts.RejectedArtifact, []contracts.Warning, error) {
|
func runValidators(ctx context.Context, extractorKey string, validators []contracts.Validator, doc *source.SourceDocument, candidates []artifacts.ArtifactCandidate, metadata map[string]any) ([]artifacts.ArtifactCandidate, []artifacts.RejectedArtifact, []contracts.Warning, error) {
|
||||||
eligible := candidates
|
eligible := candidates
|
||||||
var rejected []artifacts.RejectedArtifact
|
var rejected []artifacts.RejectedArtifact
|
||||||
var warnings []contracts.Warning
|
var warnings []contracts.Warning
|
||||||
|
|
||||||
for validatorIndex, validator := range extractor.Validators() {
|
for validatorIndex, validator := range validators {
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
return nil, rejected, warnings, fmt.Errorf("extractor %q validator[%d] must not be nil", extractor.Key(), validatorIndex)
|
return nil, rejected, warnings, fmt.Errorf("extractor %q validator[%d] must not be nil", extractorKey, validatorIndex)
|
||||||
}
|
}
|
||||||
result, err := validator.Validate(ctx, contracts.ValidationRequest{
|
result, err := validator.Validate(ctx, contracts.ValidationRequest{
|
||||||
Source: doc,
|
Source: doc,
|
||||||
@@ -135,13 +375,13 @@ func runValidators(ctx context.Context, extractor contracts.Extractor, doc *sour
|
|||||||
})
|
})
|
||||||
warnings = append(warnings, result.Warnings...)
|
warnings = append(warnings, result.Warnings...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, rejected, warnings, fmt.Errorf("validate extractor %q with validator %q: %w", extractor.Key(), validator.Name(), err)
|
return nil, rejected, warnings, fmt.Errorf("validate extractor %q with validator %q: %w", extractorKey, validator.Name(), err)
|
||||||
}
|
}
|
||||||
if result.ValidatorName != validator.Name() {
|
if result.ValidatorName != validator.Name() {
|
||||||
return nil, rejected, warnings, fmt.Errorf("validator %q returned result for %q", validator.Name(), result.ValidatorName)
|
return nil, rejected, warnings, fmt.Errorf("validator %q returned result for %q", validator.Name(), result.ValidatorName)
|
||||||
}
|
}
|
||||||
if err := validate.EnforceDecisionCardinality(eligible, result.Decisions); err != nil {
|
if err := validate.EnforceDecisionCardinality(eligible, result.Decisions); err != nil {
|
||||||
return nil, rejected, warnings, fmt.Errorf("validate extractor %q with validator %q: %w", extractor.Key(), validator.Name(), err)
|
return nil, rejected, warnings, fmt.Errorf("validate extractor %q with validator %q: %w", extractorKey, validator.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
decisions := make(map[int]contracts.ValidationDecision, len(result.Decisions))
|
decisions := make(map[int]contracts.ValidationDecision, len(result.Decisions))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user