Transport structured diagnostics through the pipeline
This commit is contained in:
107
internal/framework/pipeline/diagnostics.go
Normal file
107
internal/framework/pipeline/diagnostics.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
frameworkdiagnostics "gitea.maximumdirect.net/eric/notarius/internal/framework/diagnostics"
|
||||
)
|
||||
|
||||
func terminalDiagnosticGroups(terminal producerAttemptTerminal, origin contracts.DiagnosticOrigin, chunk *source.Chunk) ([]contracts.DiagnosticGroup, error) {
|
||||
groups, err := promoteProducerDiagnostics(terminal.Diagnostics, origin, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, record := range terminal.Validation.Diagnostics() {
|
||||
validatorOrigin := origin
|
||||
validatorOrigin.ValidatorKey = record.validatorName
|
||||
promoted, err := promoteProducerDiagnostics([]contracts.ProducerDiagnostic{record.diagnostic}, validatorOrigin, chunk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validator %q diagnostic: %w", record.validatorName, err)
|
||||
}
|
||||
groups = append(groups, promoted...)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func promoteProducerDiagnostics(diagnostics []contracts.ProducerDiagnostic, origin contracts.DiagnosticOrigin, chunk *source.Chunk) ([]contracts.DiagnosticGroup, error) {
|
||||
if err := contracts.ValidateProducerDiagnostics(diagnostics); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(diagnostics) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
groups := make([]contracts.DiagnosticGroup, len(diagnostics))
|
||||
for index, diagnostic := range diagnostics {
|
||||
group := contracts.DiagnosticGroup{
|
||||
Disposition: diagnostic.Disposition,
|
||||
Category: diagnostic.Category,
|
||||
ReasonCode: diagnostic.ReasonCode,
|
||||
Origin: origin,
|
||||
OccurrenceCount: diagnostic.OccurrenceCount,
|
||||
Samples: cloneDiagnosticSamples(diagnostic.Samples, chunk),
|
||||
OmittedSampleCount: diagnostic.OmittedSampleCount,
|
||||
}
|
||||
if err := group.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups[index] = group
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func cloneDiagnosticSamples(samples []contracts.DiagnosticSample, chunk *source.Chunk) []contracts.DiagnosticSample {
|
||||
if len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]contracts.DiagnosticSample, len(samples))
|
||||
for index, sample := range samples {
|
||||
if chunk != nil {
|
||||
chunkIndex := chunk.Index
|
||||
sample.ChunkID = chunk.ID
|
||||
sample.ChunkIndex = &chunkIndex
|
||||
} else if sample.ChunkIndex != nil {
|
||||
chunkIndex := *sample.ChunkIndex
|
||||
sample.ChunkIndex = &chunkIndex
|
||||
}
|
||||
cloned[index] = sample
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func appendDiagnosticGroups(output *RunOutput, groups []contracts.DiagnosticGroup) {
|
||||
if output == nil || len(groups) == 0 {
|
||||
return
|
||||
}
|
||||
cloned := contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: groups}).Groups
|
||||
output.diagnosticGroups = append(output.diagnosticGroups, cloned...)
|
||||
seen := make(map[contracts.Warning]struct{}, len(output.Warnings))
|
||||
for _, warning := range output.Warnings {
|
||||
seen[warning] = struct{}{}
|
||||
}
|
||||
for _, group := range cloned {
|
||||
for _, sample := range group.Samples {
|
||||
warning := contracts.Warning{Scope: sample.Scope, ReasonCode: group.ReasonCode, Message: sample.Message}
|
||||
if _, exists := seen[warning]; exists {
|
||||
continue
|
||||
}
|
||||
output.Warnings = append(output.Warnings, warning)
|
||||
seen[warning] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finalizeDiagnostics(output *RunOutput) error {
|
||||
if output == nil {
|
||||
return nil
|
||||
}
|
||||
aggregator := frameworkdiagnostics.Aggregator{}
|
||||
for _, group := range output.diagnosticGroups {
|
||||
if err := aggregator.Add(group); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
output.Diagnostics = aggregator.Collection()
|
||||
return nil
|
||||
}
|
||||
78
internal/framework/pipeline/diagnostics_test.go
Normal file
78
internal/framework/pipeline/diagnostics_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestTerminalDiagnosticGroupsDiscardSupersededAttemptDiagnostics(t *testing.T) {
|
||||
terminal, err := runProducerAttempts(context.Background(), producerAttemptConfig{Retries: 1, Policy: DefaultValidationPolicy()}, func(_ context.Context, request producerAttemptRequest) (producerAttemptOutput, error) {
|
||||
if request.Number == 1 {
|
||||
return producerAttemptOutput{Value: "first", Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("discarded", "discarded")}, Candidate: attemptCandidate(t, "first")}, nil
|
||||
}
|
||||
return producerAttemptOutput{Value: "second", Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("terminal", "terminal")}}, nil
|
||||
}, func(_ context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
if output.Value == "first" {
|
||||
return validationReport{records: []validationRecord{{validatorName: "validator", outcome: validationRejected, reasonCode: "invalid", correctionGuidance: "Correct it."}}}, nil
|
||||
}
|
||||
return validationReport{}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runProducerAttempts() error = %v", err)
|
||||
}
|
||||
groups, err := terminalDiagnosticGroups(terminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageMerge, StepID: "step", LaneID: "lane", ModuleKey: "module"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("terminalDiagnosticGroups() error = %v", err)
|
||||
}
|
||||
if len(groups) != 1 || groups[0].Samples[0].Scope != "terminal" {
|
||||
t.Fatalf("groups = %#v, want only terminal attempt diagnostics", groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalDiagnosticGroupsPreserveValidatorOrigin(t *testing.T) {
|
||||
terminal := producerAttemptTerminal{Validation: validationReport{records: []validationRecord{{
|
||||
validatorName: "dnd/spells/source-relatedness",
|
||||
outcome: validationApproved,
|
||||
diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("spells[0]", "not found")},
|
||||
}}}}
|
||||
groups, err := terminalDiagnosticGroups(terminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("terminalDiagnosticGroups() error = %v", err)
|
||||
}
|
||||
if len(groups) != 1 || groups[0].Origin.ValidatorKey != "dnd/spells/source-relatedness" {
|
||||
t.Fatalf("groups = %#v, want validator origin", groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendDiagnosticGroupsProjectsOnlyMissingLegacyWarnings(t *testing.T) {
|
||||
output := RunOutput{Warnings: []contracts.Warning{{Scope: "spells[0]", ReasonCode: "source_unrelated", Message: "not found"}}}
|
||||
appendDiagnosticGroups(&output, []contracts.DiagnosticGroup{{
|
||||
Disposition: contracts.DiagnosticDispositionAdvisory,
|
||||
Category: contracts.DiagnosticCategoryDataQuality,
|
||||
ReasonCode: "source_unrelated",
|
||||
Origin: contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"},
|
||||
OccurrenceCount: 2,
|
||||
Samples: []contracts.DiagnosticSample{{Scope: "spells[0]", Message: "not found"}, {Scope: "spells[1]", Message: "also not found"}},
|
||||
}})
|
||||
if len(output.Warnings) != 2 {
|
||||
t.Fatalf("legacy warnings = %#v, want one original and one projected record", output.Warnings)
|
||||
}
|
||||
if err := finalizeDiagnostics(&output); err != nil {
|
||||
t.Fatalf("finalizeDiagnostics() error = %v", err)
|
||||
}
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].OccurrenceCount != 2 {
|
||||
t.Fatalf("diagnostics = %#v, want grouped collection", output.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func producerDiagnostic(scope string, message string) contracts.ProducerDiagnostic {
|
||||
return contracts.ProducerDiagnostic{
|
||||
Disposition: contracts.DiagnosticDispositionAdvisory,
|
||||
Category: contracts.DiagnosticCategoryDataQuality,
|
||||
ReasonCode: "source_unrelated",
|
||||
OccurrenceCount: 1,
|
||||
Samples: []contracts.DiagnosticSample{{Scope: scope, Message: message}},
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func RegisterExtractorBuilder[T any](registry *ExtractorRegistry, spec ModuleSpe
|
||||
if err != nil {
|
||||
return erasedTypedResult{}, fmt.Errorf("clone extraction model candidate: %w", err)
|
||||
}
|
||||
return erasedTypedResult{Value: result.Value, Warnings: cloneWarnings(result.Warnings), ModelCandidate: candidate}, nil
|
||||
return erasedTypedResult{Value: result.Value, Warnings: cloneWarnings(result.Warnings), Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), ModelCandidate: candidate}, nil
|
||||
}}
|
||||
if registry.typedEntries == nil {
|
||||
registry.typedEntries = map[string]typedExtractorEntry{}
|
||||
|
||||
@@ -97,7 +97,7 @@ func RegisterMergerBuilder[T any](registry *MergerRegistry, spec ModuleSpec, val
|
||||
if err != nil {
|
||||
return erasedTypedResult{}, fmt.Errorf("clone merge model candidate: %w", err)
|
||||
}
|
||||
return erasedTypedResult{Value: result.Value, Warnings: cloneWarnings(result.Warnings), ModelCandidate: candidate}, nil
|
||||
return erasedTypedResult{Value: result.Value, Warnings: cloneWarnings(result.Warnings), Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), ModelCandidate: candidate}, nil
|
||||
},
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -88,7 +88,7 @@ func RegisterNormalizerBuilder[T any](registry *NormalizerRegistry, spec ModuleS
|
||||
if err != nil {
|
||||
return erasedTypedResult{}, fmt.Errorf("clone normalize model candidate: %w", err)
|
||||
}
|
||||
return erasedTypedResult{Value: result.Value, Warnings: cloneWarnings(result.Warnings), Retry: cloneNormalizeRetry(result.Retry), ModelCandidate: candidate}, nil
|
||||
return erasedTypedResult{Value: result.Value, Warnings: cloneWarnings(result.Warnings), Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), Retry: cloneNormalizeRetry(result.Retry), ModelCandidate: candidate}, nil
|
||||
},
|
||||
}
|
||||
return nil
|
||||
@@ -99,9 +99,10 @@ func cloneNormalizeRetry(retry *contracts.NormalizeRetry) *contracts.NormalizeRe
|
||||
return nil
|
||||
}
|
||||
return &contracts.NormalizeRetry{
|
||||
ReasonCode: retry.ReasonCode,
|
||||
Message: retry.Message,
|
||||
FallbackWarnings: cloneWarnings(retry.FallbackWarnings),
|
||||
ReasonCode: retry.ReasonCode,
|
||||
Message: retry.Message,
|
||||
FallbackWarnings: cloneWarnings(retry.FallbackWarnings),
|
||||
FallbackDiagnostics: contracts.CloneProducerDiagnostics(retry.FallbackDiagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,24 +52,26 @@ type producerAttemptRequest struct {
|
||||
// the current value as a safe fallback if its shared budget is exhausted.
|
||||
// Artifact-specific adapters are responsible for validating and populating it.
|
||||
type producerRetryDirective struct {
|
||||
FallbackWarnings []contracts.Warning
|
||||
FallbackWarnings []contracts.Warning
|
||||
FallbackDiagnostics []contracts.ProducerDiagnostic
|
||||
}
|
||||
|
||||
func (directive *producerRetryDirective) clone() *producerRetryDirective {
|
||||
if directive == nil {
|
||||
return nil
|
||||
}
|
||||
return &producerRetryDirective{FallbackWarnings: cloneWarnings(directive.FallbackWarnings)}
|
||||
return &producerRetryDirective{FallbackWarnings: cloneWarnings(directive.FallbackWarnings), FallbackDiagnostics: contracts.CloneProducerDiagnostics(directive.FallbackDiagnostics)}
|
||||
}
|
||||
|
||||
// producerAttemptOutput is intentionally artifact-neutral. Value remains
|
||||
// opaque to the state machine; Candidate is the attempt-local response that
|
||||
// may support semantic correction.
|
||||
type producerAttemptOutput struct {
|
||||
Value any
|
||||
Candidate *contracts.ModelCandidate
|
||||
Warnings []contracts.Warning
|
||||
Retry *producerRetryDirective
|
||||
Value any
|
||||
Candidate *contracts.ModelCandidate
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
Retry *producerRetryDirective
|
||||
}
|
||||
|
||||
func (output producerAttemptOutput) clone() (producerAttemptOutput, error) {
|
||||
@@ -78,10 +80,11 @@ func (output producerAttemptOutput) clone() (producerAttemptOutput, error) {
|
||||
return producerAttemptOutput{}, fmt.Errorf("clone model candidate: %w", err)
|
||||
}
|
||||
return producerAttemptOutput{
|
||||
Value: output.Value,
|
||||
Candidate: candidate,
|
||||
Warnings: cloneWarnings(output.Warnings),
|
||||
Retry: output.Retry.clone(),
|
||||
Value: output.Value,
|
||||
Candidate: candidate,
|
||||
Warnings: cloneWarnings(output.Warnings),
|
||||
Diagnostics: contracts.CloneProducerDiagnostics(output.Diagnostics),
|
||||
Retry: output.Retry.clone(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -114,6 +117,7 @@ type producerAttemptTerminal struct {
|
||||
Action producerTerminalAction
|
||||
Value any
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
Rejection *contracts.RejectedOutput
|
||||
Validation validationReport
|
||||
ValidationIncomplete bool
|
||||
@@ -122,6 +126,7 @@ type producerAttemptTerminal struct {
|
||||
|
||||
func (terminal producerAttemptTerminal) clone() producerAttemptTerminal {
|
||||
terminal.Warnings = cloneWarnings(terminal.Warnings)
|
||||
terminal.Diagnostics = contracts.CloneProducerDiagnostics(terminal.Diagnostics)
|
||||
if terminal.Rejection != nil {
|
||||
rejection := *terminal.Rejection
|
||||
rejection.Validation = cloneValidationSummaryPtr(rejection.Validation)
|
||||
@@ -206,6 +211,7 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
||||
}
|
||||
if output.Retry != nil {
|
||||
output.Warnings = append(output.Warnings, cloneWarnings(output.Retry.FallbackWarnings)...)
|
||||
output.Diagnostics = append(output.Diagnostics, contracts.CloneProducerDiagnostics(output.Retry.FallbackDiagnostics)...)
|
||||
}
|
||||
correctionCandidate, err := contracts.CloneModelCandidate(output.Candidate)
|
||||
if err != nil {
|
||||
@@ -255,13 +261,13 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
||||
if config.Policy.ValidatorFailure == ValidatorFailureWarnContinue {
|
||||
warnings := terminalWarnings(output, report)
|
||||
warnings = append(warnings, incompleteValidationWarnings(report)...)
|
||||
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, Warnings: warnings, Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, Warnings: warnings, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
}
|
||||
return failedProducerAttempt(provenance), validatorFailureError(*incomplete)
|
||||
}
|
||||
|
||||
provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptAccepted, Validation: report})
|
||||
return producerAttemptTerminal{Action: producerTerminalAccepted, Value: output.Value, Warnings: terminalWarnings(output, report), Validation: report, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
return producerAttemptTerminal{Action: producerTerminalAccepted, Value: output.Value, Warnings: terminalWarnings(output, report), Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
}
|
||||
|
||||
return failedProducerAttempt(provenance), errors.New("producer attempt budget was not exhausted deterministically")
|
||||
@@ -290,7 +296,7 @@ func applySemanticTerminalPolicy(policy ValidationPolicy, provenance []producerA
|
||||
rejected := contracts.RejectedOutput{ValidatorName: rejection.validatorName, ReasonCode: rejection.reasonCode, Message: rejection.message, AttemptCount: number, DiagnosticArtifactPath: rejection.diagnosticPath}
|
||||
switch policy.SemanticRejection {
|
||||
case SemanticRejectionRejectOutput:
|
||||
return producerAttemptTerminal{Action: producerTerminalRejected, Warnings: terminalWarnings(output, report), Rejection: &rejected, Validation: report, ValidationIncomplete: firstIncompleteValidation(report) != nil, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
return producerAttemptTerminal{Action: producerTerminalRejected, Warnings: terminalWarnings(output, report), Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Rejection: &rejected, Validation: report, ValidationIncomplete: firstIncompleteValidation(report) != nil, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
case SemanticRejectionFailRun:
|
||||
return failedProducerAttempt(provenance), fmt.Errorf("producer candidate rejected after %d attempt(s): %s", number, rejection.message)
|
||||
default:
|
||||
@@ -314,6 +320,10 @@ func terminalWarnings(output producerAttemptOutput, report validationReport) []c
|
||||
return warnings
|
||||
}
|
||||
|
||||
func cloneProducerDiagnostics(diagnostics []contracts.ProducerDiagnostic) []contracts.ProducerDiagnostic {
|
||||
return contracts.CloneProducerDiagnostics(diagnostics)
|
||||
}
|
||||
|
||||
// incompleteValidationWarnings reports only validators that exhausted their
|
||||
// execution budget. It never reports rejected candidates, and it uses fixed
|
||||
// text so provider errors and correction content cannot cross this boundary.
|
||||
|
||||
@@ -51,6 +51,7 @@ type RunInput struct {
|
||||
LLMProfiles []artifacts.LLMProfileManifest
|
||||
Metadata map[string]any
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
ChunkCacheMode ChunkCacheMode
|
||||
ChunkPlans ChunkPlanStore
|
||||
Checkpoints CheckpointRecorder
|
||||
@@ -69,16 +70,18 @@ type RunInput struct {
|
||||
}
|
||||
|
||||
type RunOutput struct {
|
||||
Manifest artifacts.RunManifest `json:"manifest"`
|
||||
ChunkPlan *artifacts.ChunkPlanSummary `json:"chunk_plan,omitempty"`
|
||||
NormalizeOutputs []contracts.SerializedOutput `json:"normalize_outputs,omitempty"`
|
||||
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
||||
OutputFiles []contracts.OutputFile `json:"-"`
|
||||
CheckpointEvents []CheckpointEvent `json:"checkpoint_events,omitempty"`
|
||||
ValidationSummaries []artifacts.ValidationSummary `json:"validation_summaries,omitempty"`
|
||||
Manifest artifacts.RunManifest `json:"manifest"`
|
||||
ChunkPlan *artifacts.ChunkPlanSummary `json:"chunk_plan,omitempty"`
|
||||
NormalizeOutputs []contracts.SerializedOutput `json:"normalize_outputs,omitempty"`
|
||||
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
||||
Diagnostics contracts.DiagnosticCollection `json:"diagnostics,omitempty"`
|
||||
OutputFiles []contracts.OutputFile `json:"-"`
|
||||
CheckpointEvents []CheckpointEvent `json:"checkpoint_events,omitempty"`
|
||||
ValidationSummaries []artifacts.ValidationSummary `json:"validation_summaries,omitempty"`
|
||||
|
||||
normalizeReuseEligibility map[generatedOutputKey]bool
|
||||
diagnosticGroups []contracts.DiagnosticGroup
|
||||
}
|
||||
|
||||
func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err error) {
|
||||
@@ -130,6 +133,11 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
output.Manifest.LLMProfiles = mergeLLMProfileManifests(input.LLMProfiles, llmProfileManifests(input.llmClient))
|
||||
}()
|
||||
output.Warnings = append(output.Warnings, cloneWarnings(input.Warnings)...)
|
||||
inputDiagnostics, diagnosticErr := promoteProducerDiagnostics(input.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageReferences}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return failOutput(output), fmt.Errorf("promote input diagnostics: %w", diagnosticErr)
|
||||
}
|
||||
appendDiagnosticGroups(&output, inputDiagnostics)
|
||||
if err := writeDebugTimed(debugRecorder, "run.json", debugTimedEnvelope{
|
||||
Stage: "run",
|
||||
StartedAt: startedTime(input.StartedAt),
|
||||
@@ -249,6 +257,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(*chunkResult.validation))
|
||||
}
|
||||
output.Warnings = append(output.Warnings, chunkResult.warnings...)
|
||||
appendDiagnosticGroups(&output, chunkResult.diagnostics)
|
||||
chunkDebugPayload := map[string]any{
|
||||
"cache_mode": chunkMode,
|
||||
"lookup": chunkResult.lookup,
|
||||
@@ -311,6 +320,9 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
}
|
||||
populateOutputManifest(&output)
|
||||
output.Manifest.CompletedAt = timePtr(time.Now().UTC())
|
||||
if err := finalizeDiagnostics(&output); err != nil {
|
||||
return failOutput(output), fmt.Errorf("aggregate diagnostics: %w", err)
|
||||
}
|
||||
|
||||
encoder := input.Prepared.output
|
||||
if err := attachModuleManifestMetadata(&output, "output", encoder); err != nil {
|
||||
@@ -374,6 +386,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
NormalizeOutputs: cloneSerializedOutputs(output.NormalizeOutputs),
|
||||
Rejected: cloneRejectedOutputs(output.Rejected),
|
||||
Warnings: output.Warnings,
|
||||
Diagnostics: contracts.CloneDiagnosticCollection(output.Diagnostics),
|
||||
LLMProfile: input.pipeline.Output.LLMProfile,
|
||||
StructuredOutputRepairAttempts: cloneStructuredOutputRepairAttempts(input.pipeline.Output.StructuredOutputRepairAttempts),
|
||||
Metadata: outputMetadata,
|
||||
|
||||
@@ -12,24 +12,26 @@ import (
|
||||
)
|
||||
|
||||
type chunkPlanExecution struct {
|
||||
accepted bool
|
||||
chunks []source.Chunk
|
||||
plan *source.ChunkPlan
|
||||
warnings []contracts.Warning
|
||||
rejection *contracts.RejectedOutput
|
||||
lookup ChunkPlanDecision
|
||||
record *ChunkPlanRecord
|
||||
action string
|
||||
summary artifacts.ChunkPlanSummary
|
||||
validation *artifacts.ValidationSummary
|
||||
accepted bool
|
||||
chunks []source.Chunk
|
||||
plan *source.ChunkPlan
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejection *contracts.RejectedOutput
|
||||
lookup ChunkPlanDecision
|
||||
record *ChunkPlanRecord
|
||||
action string
|
||||
summary artifacts.ChunkPlanSummary
|
||||
validation *artifacts.ValidationSummary
|
||||
}
|
||||
|
||||
type generatedChunkPlanCandidate struct {
|
||||
plan source.ChunkPlan
|
||||
chunks []source.Chunk
|
||||
record ChunkPlanRecord
|
||||
producerWarnings []contracts.Warning
|
||||
terminal *attemptTerminalRecorder
|
||||
plan source.ChunkPlan
|
||||
chunks []source.Chunk
|
||||
record ChunkPlanRecord
|
||||
producerWarnings []contracts.Warning
|
||||
producerDiagnostics []contracts.ProducerDiagnostic
|
||||
terminal *attemptTerminalRecorder
|
||||
}
|
||||
|
||||
func effectiveChunkCacheMode(mode ChunkCacheMode) ChunkCacheMode {
|
||||
@@ -163,7 +165,7 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
},
|
||||
Warnings: cloneWarnings(chunkResult.Warnings), CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
return producerAttemptOutput{Value: generatedChunkPlanCandidate{plan: plan, chunks: chunks, record: candidate, producerWarnings: cloneWarnings(chunkResult.Warnings), terminal: &attemptTerminal}, Candidate: chunkResult.ModelCandidate, Warnings: cloneWarnings(chunkResult.Warnings)}, nil
|
||||
return producerAttemptOutput{Value: generatedChunkPlanCandidate{plan: plan, chunks: chunks, record: candidate, producerWarnings: cloneWarnings(chunkResult.Warnings), producerDiagnostics: contracts.CloneProducerDiagnostics(chunkResult.Diagnostics), terminal: &attemptTerminal}, Candidate: chunkResult.ModelCandidate, Warnings: cloneWarnings(chunkResult.Warnings), Diagnostics: contracts.CloneProducerDiagnostics(chunkResult.Diagnostics)}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(generatedChunkPlanCandidate)
|
||||
if !ok {
|
||||
@@ -190,6 +192,11 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
diagnostics, diagnosticErr := terminalDiagnosticGroups(terminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageChunk, ModuleKey: chunker.Key()}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return result, fmt.Errorf("promote chunk diagnostics: %w", diagnosticErr)
|
||||
}
|
||||
result.diagnostics = diagnostics
|
||||
if terminal.Action == producerTerminalRejected {
|
||||
result.rejection = terminal.Rejection
|
||||
if result.rejection != nil {
|
||||
|
||||
@@ -367,7 +367,8 @@ func TestRunnerBoundsExtractJobsAndStabilizesReverseCompletion(t *testing.T) {
|
||||
case <-ctx.Done():
|
||||
return erasedTypedResult{}, ctx.Err()
|
||||
}
|
||||
return erasedTypedResult{Value: typedValueForLane(lane, request.Chunk.Index), Warnings: []contracts.Warning{{Scope: fmt.Sprintf("lane-%d/chunk-%d", lane, request.Chunk.Index), ReasonCode: "observed", Message: "ordered"}}}, nil
|
||||
scope := fmt.Sprintf("lane-%d/chunk-%d", lane, request.Chunk.Index)
|
||||
return erasedTypedResult{Value: typedValueForLane(lane, request.Chunk.Index), Warnings: []contracts.Warning{{Scope: scope, ReasonCode: "observed", Message: "ordered"}}, Diagnostics: []contracts.ProducerDiagnostic{{Disposition: contracts.DiagnosticDispositionObservation, Category: contracts.DiagnosticCategoryNormalization, ReasonCode: "observed", OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: scope, Message: "ordered"}}}}}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -404,6 +405,24 @@ func TestRunnerBoundsExtractJobsAndStabilizesReverseCompletion(t *testing.T) {
|
||||
if !reflect.DeepEqual(gotScopes, wantScopes) {
|
||||
t.Fatalf("warning order = %#v, want %#v", gotScopes, wantScopes)
|
||||
}
|
||||
if len(result.output.Diagnostics.Groups) != 2 {
|
||||
t.Fatalf("diagnostic groups = %#v, want one deterministic group per lane", result.output.Diagnostics.Groups)
|
||||
}
|
||||
for lane, group := range result.output.Diagnostics.Groups {
|
||||
if group.Origin.LaneID != prepared.Steps[0].lanes[lane].resolved.ID {
|
||||
t.Fatalf("group origin = %#v, want configured lane %q", group.Origin, prepared.Steps[0].lanes[lane].resolved.ID)
|
||||
}
|
||||
indexes := make([]int, len(group.Samples))
|
||||
for index, sample := range group.Samples {
|
||||
if sample.ChunkIndex == nil {
|
||||
t.Fatalf("sample = %#v, want chunk index", sample)
|
||||
}
|
||||
indexes[index] = *sample.ChunkIndex
|
||||
}
|
||||
if !reflect.DeepEqual(indexes, []int{0, 1, 2}) {
|
||||
t.Fatalf("group sample chunk order = %#v, want canonical chunk order", indexes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerStartsLaneContinuationWhileOtherLaneExtractsRemain(t *testing.T) {
|
||||
|
||||
@@ -25,6 +25,7 @@ type laneExtractState struct {
|
||||
values []erasedExtractArtifact
|
||||
serialized []CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejected []contracts.RejectedOutput
|
||||
incomplete []int
|
||||
validationSummaries []artifacts.ValidationSummary
|
||||
@@ -39,6 +40,7 @@ type finalizedExtractResults struct {
|
||||
accepted []erasedExtractArtifact
|
||||
serialized []CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejected []contracts.RejectedOutput
|
||||
incomplete []int
|
||||
validationSummaries []artifacts.ValidationSummary
|
||||
@@ -68,6 +70,7 @@ type extractJobResult struct {
|
||||
value erasedExtractArtifact
|
||||
serialized CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejected *contracts.RejectedOutput
|
||||
validationIncomplete bool
|
||||
validationSummary *artifacts.ValidationSummary
|
||||
@@ -458,7 +461,7 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
return producerAttemptOutput{}, terminal.record(payload, attemptErr)
|
||||
}
|
||||
serializedCandidate.ChunkID, serializedCandidate.ChunkIndex, serializedCandidate.ChunkRef = artifact.ChunkID, artifact.ChunkIndex, artifact.ChunkRef
|
||||
return producerAttemptOutput{Value: extractAttemptValue{artifact: artifact, serialized: serializedCandidate, terminal: &terminal}, Candidate: extracted.ModelCandidate, Warnings: attemptWarnings}, nil
|
||||
return producerAttemptOutput{Value: extractAttemptValue{artifact: artifact, serialized: serializedCandidate, terminal: &terminal}, Candidate: extracted.ModelCandidate, Warnings: attemptWarnings, Diagnostics: contracts.CloneProducerDiagnostics(extracted.Diagnostics)}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(extractAttemptValue)
|
||||
if !ok {
|
||||
@@ -481,6 +484,14 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
result.err = errors.Join(result.err, debugErr)
|
||||
return result
|
||||
}
|
||||
if err == nil {
|
||||
diagnostics, diagnosticErr := terminalDiagnosticGroups(terminalResult, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageExtract, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Extract.Module}, &chunk)
|
||||
if diagnosticErr != nil {
|
||||
result.err = fmt.Errorf("promote extract diagnostics: %w", diagnosticErr)
|
||||
return result
|
||||
}
|
||||
result.diagnostics = diagnostics
|
||||
}
|
||||
if err == nil && terminalResult.Action == producerTerminalRejected {
|
||||
result.rejected = terminalResult.Rejection
|
||||
if result.rejected != nil {
|
||||
@@ -532,11 +543,13 @@ func finalizeLaneExtract(checkpoints CheckpointRecorder, stepID string, state *l
|
||||
if result.rejected != nil {
|
||||
state.rejected = append(state.rejected, *result.rejected)
|
||||
state.warnings = append(state.warnings, result.warnings...)
|
||||
state.diagnostics = append(state.diagnostics, result.diagnostics...)
|
||||
continue
|
||||
}
|
||||
state.values = append(state.values, result.value)
|
||||
state.serialized = append(state.serialized, result.serialized)
|
||||
state.warnings = append(state.warnings, result.warnings...)
|
||||
state.diagnostics = append(state.diagnostics, result.diagnostics...)
|
||||
if result.validationIncomplete {
|
||||
state.incomplete = append(state.incomplete, result.chunkIndex)
|
||||
}
|
||||
@@ -563,6 +576,7 @@ func (r *Runner) continueLane(ctx context.Context, input RunInput, checkpoints C
|
||||
accepted: state.values,
|
||||
serialized: state.serialized,
|
||||
warnings: state.warnings,
|
||||
diagnostics: state.diagnostics,
|
||||
rejected: state.rejected,
|
||||
incomplete: state.incomplete,
|
||||
validationSummaries: state.validationSummaries,
|
||||
@@ -570,6 +584,7 @@ func (r *Runner) continueLane(ctx context.Context, input RunInput, checkpoints C
|
||||
reuseEligible: state.reuseEligible,
|
||||
}
|
||||
local.Warnings = append(local.Warnings, cloneWarnings(results.warnings)...)
|
||||
local.diagnosticGroups = append(local.diagnosticGroups, contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: results.diagnostics}).Groups...)
|
||||
local.Rejected = append(local.Rejected, cloneRejectedOutputs(results.rejected)...)
|
||||
local.ValidationSummaries = append(local.ValidationSummaries, cloneValidationSummaries(results.validationSummaries)...)
|
||||
if err := writeDebugTimed(input.Debug, path.Join("extract", fileio.EncodePathComponent(lane.ID), "input.json"), debugTimedEnvelope{Stage: string(StageExtract), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Extract.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": results.decision.Reused, "decision": results.decision, "source": debugSourceDocumentEnvelope(doc), "chunks": debugSourceChunkEnvelopes(chunks), "options": redactSensitiveMap(lane.Extract.Options), "metadata": redactSensitiveMap(input.Metadata)}}); err != nil {
|
||||
@@ -641,6 +656,7 @@ func mergeLaneOutput(dst *RunOutput, src RunOutput) error {
|
||||
dst.NormalizeOutputs = append(dst.NormalizeOutputs, cloneSerializedOutputs(src.NormalizeOutputs)...)
|
||||
dst.Rejected = append(dst.Rejected, cloneRejectedOutputs(src.Rejected)...)
|
||||
dst.Warnings = append(dst.Warnings, cloneWarnings(src.Warnings)...)
|
||||
appendDiagnosticGroups(dst, src.diagnosticGroups)
|
||||
dst.CheckpointEvents = append(dst.CheckpointEvents, src.CheckpointEvents...)
|
||||
dst.ValidationSummaries = append(dst.ValidationSummaries, cloneValidationSummaries(src.ValidationSummaries)...)
|
||||
if len(src.normalizeReuseEligibility) > 0 {
|
||||
|
||||
@@ -295,7 +295,7 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
if encodeErr != nil {
|
||||
return producerAttemptOutput{}, terminal.record(map[string]any{"warnings": debugWarningEnvelopes(warnings)}, fmt.Errorf("serialize merge candidate for lane %q: %w", lane.ID, encodeErr))
|
||||
}
|
||||
return producerAttemptOutput{Value: mergeAttemptValue{artifact: candidate, candidate: serializedCandidate, terminal: &terminal}, Candidate: result.ModelCandidate, Warnings: warnings}, nil
|
||||
return producerAttemptOutput{Value: mergeAttemptValue{artifact: candidate, candidate: serializedCandidate, terminal: &terminal}, Candidate: result.ModelCandidate, Warnings: warnings, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics)}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(mergeAttemptValue)
|
||||
if !ok {
|
||||
@@ -322,6 +322,10 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
}
|
||||
return stageResult, runErr
|
||||
}
|
||||
terminalDiagnostics, diagnosticErr := terminalDiagnosticGroups(terminalResult, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageMerge, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return stageResult, fmt.Errorf("promote merge diagnostics: %w", diagnosticErr)
|
||||
}
|
||||
if terminalResult.Action == producerTerminalRejected {
|
||||
rejected := terminalResult.Rejection
|
||||
if rejected == nil {
|
||||
@@ -331,6 +335,7 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
rejected.Validation = &terminalSummary
|
||||
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(terminalSummary))
|
||||
output.Warnings = append(output.Warnings, terminalResult.Warnings...)
|
||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||
output.Rejected = append(output.Rejected, *rejected)
|
||||
if stageResult.reuseEligible {
|
||||
if err := checkpointMergeRejected(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps, *rejected); err != nil {
|
||||
@@ -367,6 +372,7 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
stageResult.reuseEligible = false
|
||||
}
|
||||
output.Warnings = append(output.Warnings, mergeWarnings...)
|
||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||
if stageResult.reuseEligible {
|
||||
if err := recordMerge(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps, serializedMerge, mergeWarnings); err != nil {
|
||||
return stageResult, err
|
||||
@@ -446,7 +452,7 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
}
|
||||
anotherAttempt := request.Number <= lane.Normalize.Retries
|
||||
attemptValue.retry = map[string]any{"reason_code": result.Retry.ReasonCode, "message": result.Retry.Message, "another_attempt": anotherAttempt, "fallback_accepted": !anotherAttempt}
|
||||
directive = &producerRetryDirective{FallbackWarnings: cloneWarnings(result.Retry.FallbackWarnings)}
|
||||
directive = &producerRetryDirective{FallbackWarnings: cloneWarnings(result.Retry.FallbackWarnings), FallbackDiagnostics: contracts.CloneProducerDiagnostics(result.Retry.FallbackDiagnostics)}
|
||||
if anotherAttempt {
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(warnings), "retry": attemptValue.retry}
|
||||
if debugErr := terminal.record(payload, nil); debugErr != nil {
|
||||
@@ -454,7 +460,7 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
}
|
||||
}
|
||||
}
|
||||
return producerAttemptOutput{Value: attemptValue, Candidate: result.ModelCandidate, Warnings: warnings, Retry: directive}, nil
|
||||
return producerAttemptOutput{Value: attemptValue, Candidate: result.ModelCandidate, Warnings: warnings, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), Retry: directive}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(normalizeAttemptValue)
|
||||
if !ok {
|
||||
@@ -484,6 +490,10 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
}
|
||||
return stageResult, runErr
|
||||
}
|
||||
terminalDiagnostics, diagnosticErr := terminalDiagnosticGroups(terminalResult, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return stageResult, fmt.Errorf("promote normalize diagnostics: %w", diagnosticErr)
|
||||
}
|
||||
if terminalResult.Action == producerTerminalRejected {
|
||||
rejected := terminalResult.Rejection
|
||||
if rejected == nil {
|
||||
@@ -493,6 +503,7 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
rejected.Validation = &terminalSummary
|
||||
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(terminalSummary))
|
||||
output.Warnings = append(output.Warnings, terminalResult.Warnings...)
|
||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||
output.Rejected = append(output.Rejected, *rejected)
|
||||
if stageResult.reuseEligible {
|
||||
if err := checkpointNormalizeRejected(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps, *rejected); err != nil {
|
||||
@@ -527,6 +538,7 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
normalizeWarnings = cloneWarnings(terminalResult.Warnings)
|
||||
output.ValidationSummaries = append(output.ValidationSummaries, terminalSummary)
|
||||
output.Warnings = append(output.Warnings, normalizeWarnings...)
|
||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||
if terminalResult.ValidationIncomplete {
|
||||
stageResult.reuseEligible = false
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type erasedMergeArtifact struct {
|
||||
type erasedTypedResult struct {
|
||||
Value any
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
Retry *contracts.NormalizeRetry
|
||||
ModelCandidate *contracts.ModelCandidate
|
||||
}
|
||||
|
||||
@@ -30,12 +30,14 @@ type validationRecord struct {
|
||||
message string
|
||||
diagnosticPath string
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.ProducerDiagnostic
|
||||
correctionGuidance string
|
||||
failure error
|
||||
}
|
||||
|
||||
func (record validationRecord) clone() validationRecord {
|
||||
record.warnings = cloneWarnings(record.warnings)
|
||||
record.diagnostics = contracts.CloneProducerDiagnostics(record.diagnostics)
|
||||
return record
|
||||
}
|
||||
|
||||
@@ -62,6 +64,24 @@ func (report validationReport) Warnings() []contracts.Warning {
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (report validationReport) Diagnostics() []validationDiagnosticRecord {
|
||||
var diagnostics []validationDiagnosticRecord
|
||||
for _, record := range report.records {
|
||||
if record.outcome != validationApproved && record.outcome != validationRejected {
|
||||
continue
|
||||
}
|
||||
for _, diagnostic := range record.diagnostics {
|
||||
diagnostics = append(diagnostics, validationDiagnosticRecord{validatorName: record.validatorName, diagnostic: cloneProducerDiagnostics([]contracts.ProducerDiagnostic{diagnostic})[0]})
|
||||
}
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
type validationDiagnosticRecord struct {
|
||||
validatorName string
|
||||
diagnostic contracts.ProducerDiagnostic
|
||||
}
|
||||
|
||||
func (report validationReport) FirstRejection() *validationRecord {
|
||||
for _, record := range report.records {
|
||||
if record.outcome == validationRejected {
|
||||
@@ -187,14 +207,14 @@ func executeValidationChain(ctx context.Context, chain preparedValidatorChain, i
|
||||
continue
|
||||
}
|
||||
if invocation.result.Approved {
|
||||
report.records = append(report.records, validationRecord{validatorName: binding.Module, outcome: validationApproved, attemptCount: attempt, warnings: cloneWarnings(invocation.result.Warnings), diagnosticPath: invocation.result.DiagnosticArtifactPath})
|
||||
report.records = append(report.records, validationRecord{validatorName: binding.Module, outcome: validationApproved, attemptCount: attempt, warnings: cloneWarnings(invocation.result.Warnings), diagnostics: cloneProducerDiagnostics(invocation.result.Diagnostics), diagnosticPath: invocation.result.DiagnosticArtifactPath})
|
||||
break
|
||||
}
|
||||
message := invocation.result.Message
|
||||
if message == "" {
|
||||
message = "output rejected"
|
||||
}
|
||||
report.records = append(report.records, validationRecord{validatorName: binding.Module, outcome: validationRejected, attemptCount: attempt, reasonCode: invocation.result.ReasonCode, message: message, diagnosticPath: invocation.result.DiagnosticArtifactPath, warnings: cloneWarnings(invocation.result.Warnings), correctionGuidance: invocation.result.CorrectionGuidance})
|
||||
report.records = append(report.records, validationRecord{validatorName: binding.Module, outcome: validationRejected, attemptCount: attempt, reasonCode: invocation.result.ReasonCode, message: message, diagnosticPath: invocation.result.DiagnosticArtifactPath, warnings: cloneWarnings(invocation.result.Warnings), diagnostics: cloneProducerDiagnostics(invocation.result.Diagnostics), correctionGuidance: invocation.result.CorrectionGuidance})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user