Transport structured diagnostics through the pipeline

This commit is contained in:
2026-08-27 15:29:10 +00:00
parent acb04954eb
commit ba569594a1
20 changed files with 578 additions and 65 deletions

View File

@@ -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.