Finalize structured diagnostic aggregation
This commit is contained in:
@@ -28,14 +28,14 @@ type CheckpointRecorder interface {
|
||||
SourceSucceeded(moduleKey string, doc *source.SourceDocument) error
|
||||
SourceFailed(moduleKey string, err error) error
|
||||
ExtractRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
||||
ExtractSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error
|
||||
ExtractSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error
|
||||
ExtractFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||
MergeRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
||||
MergeSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error
|
||||
MergeSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
|
||||
MergeRejected(laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||
MergeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||
NormalizeRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
||||
NormalizeSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error
|
||||
NormalizeSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
|
||||
NormalizeRejected(laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||
NormalizeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||
}
|
||||
@@ -45,14 +45,14 @@ type CheckpointRecorder interface {
|
||||
// remain available for callers that do not have step context.
|
||||
type StepCheckpointRecorder interface {
|
||||
ExtractRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
||||
ExtractSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error
|
||||
ExtractSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error
|
||||
ExtractFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||
MergeRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
||||
MergeSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error
|
||||
MergeSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
|
||||
MergeRejectedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||
MergeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||
NormalizeRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
||||
NormalizeSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error
|
||||
NormalizeSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
|
||||
NormalizeRejectedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||
NormalizeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||
}
|
||||
@@ -293,16 +293,13 @@ type CheckpointDiagnostic struct {
|
||||
type ExtractCheckpoint struct {
|
||||
Outputs []CheckpointArtifact
|
||||
Rejected []contracts.RejectedOutput
|
||||
Warnings []contracts.Warning
|
||||
}
|
||||
|
||||
type MergeCheckpoint struct {
|
||||
Output CheckpointArtifact
|
||||
Warnings []contracts.Warning
|
||||
Output CheckpointArtifact
|
||||
}
|
||||
type NormalizeCheckpoint struct {
|
||||
Output CheckpointArtifact
|
||||
Warnings []contracts.Warning
|
||||
Output CheckpointArtifact
|
||||
}
|
||||
|
||||
type CheckpointLoader interface {
|
||||
@@ -335,14 +332,14 @@ func (noopCheckpointRecorder) SourceFailed(string, error) error
|
||||
func (noopCheckpointRecorder) ExtractRunning(string, string, []CheckpointFingerprint) error {
|
||||
return nil
|
||||
}
|
||||
func (noopCheckpointRecorder) ExtractSucceeded(string, string, []CheckpointFingerprint, []CheckpointArtifact, []contracts.RejectedOutput, []contracts.Warning) error {
|
||||
func (noopCheckpointRecorder) ExtractSucceeded(string, string, []CheckpointFingerprint, []CheckpointArtifact, []contracts.RejectedOutput) error {
|
||||
return nil
|
||||
}
|
||||
func (noopCheckpointRecorder) ExtractFailed(string, string, []CheckpointFingerprint, error) error {
|
||||
return nil
|
||||
}
|
||||
func (noopCheckpointRecorder) MergeRunning(string, string, []CheckpointFingerprint) error { return nil }
|
||||
func (noopCheckpointRecorder) MergeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact, []contracts.Warning) error {
|
||||
func (noopCheckpointRecorder) MergeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact) error {
|
||||
return nil
|
||||
}
|
||||
func (noopCheckpointRecorder) MergeRejected(string, string, []CheckpointFingerprint, contracts.RejectedOutput) error {
|
||||
@@ -354,7 +351,7 @@ func (noopCheckpointRecorder) MergeFailed(string, string, []CheckpointFingerprin
|
||||
func (noopCheckpointRecorder) NormalizeRunning(string, string, []CheckpointFingerprint) error {
|
||||
return nil
|
||||
}
|
||||
func (noopCheckpointRecorder) NormalizeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact, []contracts.Warning) error {
|
||||
func (noopCheckpointRecorder) NormalizeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact) error {
|
||||
return nil
|
||||
}
|
||||
func (noopCheckpointRecorder) NormalizeRejected(string, string, []CheckpointFingerprint, contracts.RejectedOutput) error {
|
||||
@@ -387,11 +384,11 @@ func checkpointExtractRunning(recorder CheckpointRecorder, stepID, laneID, modul
|
||||
}
|
||||
return recorder.ExtractRunning(laneID, moduleKey, deps)
|
||||
}
|
||||
func checkpointExtractSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
||||
func checkpointExtractSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||
return stepAware.ExtractSucceededForStep(stepID, laneID, moduleKey, deps, outputs, rejected, warnings)
|
||||
return stepAware.ExtractSucceededForStep(stepID, laneID, moduleKey, deps, outputs, rejected)
|
||||
}
|
||||
return recorder.ExtractSucceeded(laneID, moduleKey, deps, outputs, rejected, warnings)
|
||||
return recorder.ExtractSucceeded(laneID, moduleKey, deps, outputs, rejected)
|
||||
}
|
||||
func checkpointExtractFailed(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, err error) error {
|
||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||
@@ -405,11 +402,11 @@ func checkpointMergeRunning(recorder CheckpointRecorder, stepID, laneID, moduleK
|
||||
}
|
||||
return recorder.MergeRunning(laneID, moduleKey, deps)
|
||||
}
|
||||
func checkpointMergeSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
func checkpointMergeSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||
return stepAware.MergeSucceededForStep(stepID, laneID, moduleKey, deps, output, warnings)
|
||||
return stepAware.MergeSucceededForStep(stepID, laneID, moduleKey, deps, output)
|
||||
}
|
||||
return recorder.MergeSucceeded(laneID, moduleKey, deps, output, warnings)
|
||||
return recorder.MergeSucceeded(laneID, moduleKey, deps, output)
|
||||
}
|
||||
func checkpointMergeRejected(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||
@@ -429,11 +426,11 @@ func checkpointNormalizeRunning(recorder CheckpointRecorder, stepID, laneID, mod
|
||||
}
|
||||
return recorder.NormalizeRunning(laneID, moduleKey, deps)
|
||||
}
|
||||
func checkpointNormalizeSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
func checkpointNormalizeSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||
return stepAware.NormalizeSucceededForStep(stepID, laneID, moduleKey, deps, output, warnings)
|
||||
return stepAware.NormalizeSucceededForStep(stepID, laneID, moduleKey, deps, output)
|
||||
}
|
||||
return recorder.NormalizeSucceeded(laneID, moduleKey, deps, output, warnings)
|
||||
return recorder.NormalizeSucceeded(laneID, moduleKey, deps, output)
|
||||
}
|
||||
func checkpointNormalizeRejected(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||
|
||||
@@ -24,7 +24,6 @@ type ChunkPlanRecord struct {
|
||||
PlanDigest string `json:"plan_digest"`
|
||||
Plan source.ChunkPlan `json:"plan"`
|
||||
Producer ChunkPlanProducer `json:"producer"`
|
||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
||||
Diagnostics []contracts.ProducerDiagnostic `json:"diagnostics,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -52,11 +52,10 @@ type debugTimedEnvelope struct {
|
||||
}
|
||||
|
||||
type debugBinaryEnvelope struct {
|
||||
ContentBase64 string `json:"content_base64,omitempty"`
|
||||
ContentDigest string `json:"content_digest,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
||||
ContentBase64 string `json:"content_base64,omitempty"`
|
||||
ContentDigest string `json:"content_digest,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type debugSourceInput struct {
|
||||
@@ -483,14 +482,13 @@ func writeProducerTerminalDebug(recorder DebugRecorder, name string, terminal pr
|
||||
})
|
||||
}
|
||||
|
||||
func debugContentEnvelope(content []byte, mediaType string, metadata map[string]any, warnings []contracts.Warning) debugBinaryEnvelope {
|
||||
func debugContentEnvelope(content []byte, mediaType string, metadata map[string]any, _ any) debugBinaryEnvelope {
|
||||
content = redactSecretBytes(content)
|
||||
return debugBinaryEnvelope{
|
||||
ContentBase64: base64.StdEncoding.EncodeToString(content),
|
||||
ContentDigest: debugContentDigest(content),
|
||||
MediaType: mediaType,
|
||||
Metadata: redactSensitiveMap(metadata),
|
||||
Warnings: cloneWarnings(warnings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,20 +729,9 @@ func debugValidationResultEnvelope(result contracts.ValidationResult) contracts.
|
||||
result.Message = string(redactSecretBytes([]byte(result.Message)))
|
||||
result.CorrectionGuidance = ""
|
||||
result.DiagnosticArtifactPath = string(redactSecretBytes([]byte(result.DiagnosticArtifactPath)))
|
||||
for i := range result.Warnings {
|
||||
result.Warnings[i].Message = string(redactSecretBytes([]byte(result.Warnings[i].Message)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func debugWarningEnvelopes(warnings []contracts.Warning) []contracts.Warning {
|
||||
out := cloneWarnings(warnings)
|
||||
for i := range out {
|
||||
out[i].Message = string(redactSecretBytes([]byte(out[i].Message)))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func debugRejectedOutputEnvelope(rejected contracts.RejectedOutput) contracts.RejectedOutput {
|
||||
rejected.Message = string(redactSecretBytes([]byte(rejected.Message)))
|
||||
rejected.DiagnosticArtifactPath = string(redactSecretBytes([]byte(rejected.DiagnosticArtifactPath)))
|
||||
|
||||
@@ -107,20 +107,6 @@ func appendDiagnosticGroups(output *RunOutput, groups []contracts.DiagnosticGrou
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -46,8 +46,8 @@ func TestTerminalDiagnosticGroupsPreserveValidatorOrigin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendDiagnosticGroupsProjectsOnlyMissingLegacyWarnings(t *testing.T) {
|
||||
output := RunOutput{Warnings: []contracts.Warning{{Scope: "spells[0]", ReasonCode: "source_unrelated", Message: "not found"}}}
|
||||
func TestAppendDiagnosticGroupsRetainsOnlyStructuredDiagnostics(t *testing.T) {
|
||||
output := RunOutput{}
|
||||
appendDiagnosticGroups(&output, []contracts.DiagnosticGroup{{
|
||||
Disposition: contracts.DiagnosticDispositionAdvisory,
|
||||
Category: contracts.DiagnosticCategoryDataQuality,
|
||||
@@ -56,9 +56,6 @@ func TestAppendDiagnosticGroupsProjectsOnlyMissingLegacyWarnings(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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), Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), ModelCandidate: candidate}, nil
|
||||
return erasedTypedResult{Value: result.Value, 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), Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), ModelCandidate: candidate}, nil
|
||||
return erasedTypedResult{Value: result.Value, 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), Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), Retry: cloneNormalizeRetry(result.Retry), ModelCandidate: candidate}, nil
|
||||
return erasedTypedResult{Value: result.Value, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), Retry: cloneNormalizeRetry(result.Retry), ModelCandidate: candidate}, nil
|
||||
},
|
||||
}
|
||||
return nil
|
||||
@@ -101,7 +101,6 @@ func cloneNormalizeRetry(retry *contracts.NormalizeRetry) *contracts.NormalizeRe
|
||||
return &contracts.NormalizeRetry{
|
||||
ReasonCode: retry.ReasonCode,
|
||||
Message: retry.Message,
|
||||
FallbackWarnings: cloneWarnings(retry.FallbackWarnings),
|
||||
FallbackDiagnostics: contracts.CloneProducerDiagnostics(retry.FallbackDiagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,23 @@ import (
|
||||
)
|
||||
|
||||
type retryingNotesNormalizer struct {
|
||||
warnings []contracts.Warning
|
||||
retry *contracts.NormalizeRetry
|
||||
retry *contracts.NormalizeRetry
|
||||
}
|
||||
|
||||
func (retryingNotesNormalizer) Key() string { return "test/retry-normalize" }
|
||||
func (retryingNotesNormalizer) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
||||
func (n retryingNotesNormalizer) Normalize(_ context.Context, req contracts.TypedNormalizeRequest[codecNotes]) (contracts.TypedNormalizeResult[codecNotes], error) {
|
||||
return contracts.TypedNormalizeResult[codecNotes]{Value: req.MergeOutput.Value, Warnings: n.warnings, Retry: n.retry}, nil
|
||||
return contracts.TypedNormalizeResult[codecNotes]{Value: req.MergeOutput.Value, Retry: n.retry}, nil
|
||||
}
|
||||
|
||||
func TestNormalizerRegistryErasureClonesRetryDirective(t *testing.T) {
|
||||
warnings := []contracts.Warning{{Scope: "attempt", ReasonCode: "ordinary", Message: "ordinary warning"}}
|
||||
retry := &contracts.NormalizeRetry{
|
||||
ReasonCode: "retryable",
|
||||
Message: "safe fallback available",
|
||||
FallbackWarnings: []contracts.Warning{{Scope: "fallback", ReasonCode: "omitted", Message: "fallback warning"}},
|
||||
ReasonCode: "retryable",
|
||||
Message: "safe fallback available",
|
||||
}
|
||||
registry := NewNormalizerRegistry()
|
||||
if err := RegisterNormalizer(registry, ModuleSpec{Key: "test/retry-normalize", Stage: StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, ArtifactKind: "test/notes"}, func() (contracts.Normalizer[codecNotes], error) {
|
||||
return retryingNotesNormalizer{warnings: warnings, retry: retry}, nil
|
||||
return retryingNotesNormalizer{retry: retry}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("RegisterNormalizer() error = %v", err)
|
||||
}
|
||||
@@ -43,10 +40,8 @@ func TestNormalizerRegistryErasureClonesRetryDirective(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("normalize() error = %v", err)
|
||||
}
|
||||
warnings[0].Message = "mutated"
|
||||
retry.Message = "mutated"
|
||||
retry.FallbackWarnings[0].Message = "mutated"
|
||||
if result.Retry == nil || result.Warnings[0].Message != "ordinary warning" || result.Retry.Message != "safe fallback available" || result.Retry.FallbackWarnings[0].Message != "fallback warning" {
|
||||
t.Fatalf("erased retry result = %#v, want independent warning data", result)
|
||||
if result.Retry == nil || result.Retry.Message != "safe fallback available" {
|
||||
t.Fatalf("erased retry result = %#v, want independent retry data", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ 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
|
||||
FallbackDiagnostics []contracts.ProducerDiagnostic
|
||||
}
|
||||
|
||||
@@ -60,7 +59,7 @@ func (directive *producerRetryDirective) clone() *producerRetryDirective {
|
||||
if directive == nil {
|
||||
return nil
|
||||
}
|
||||
return &producerRetryDirective{FallbackWarnings: cloneWarnings(directive.FallbackWarnings), FallbackDiagnostics: contracts.CloneProducerDiagnostics(directive.FallbackDiagnostics)}
|
||||
return &producerRetryDirective{FallbackDiagnostics: contracts.CloneProducerDiagnostics(directive.FallbackDiagnostics)}
|
||||
}
|
||||
|
||||
// producerAttemptOutput is intentionally artifact-neutral. Value remains
|
||||
@@ -69,7 +68,6 @@ func (directive *producerRetryDirective) clone() *producerRetryDirective {
|
||||
type producerAttemptOutput struct {
|
||||
Value any
|
||||
Candidate *contracts.ModelCandidate
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
Retry *producerRetryDirective
|
||||
}
|
||||
@@ -82,7 +80,6 @@ func (output producerAttemptOutput) clone() (producerAttemptOutput, error) {
|
||||
return producerAttemptOutput{
|
||||
Value: output.Value,
|
||||
Candidate: candidate,
|
||||
Warnings: cloneWarnings(output.Warnings),
|
||||
Diagnostics: contracts.CloneProducerDiagnostics(output.Diagnostics),
|
||||
Retry: output.Retry.clone(),
|
||||
}, nil
|
||||
@@ -116,7 +113,6 @@ func (provenance producerAttemptProvenance) clone() producerAttemptProvenance {
|
||||
type producerAttemptTerminal struct {
|
||||
Action producerTerminalAction
|
||||
Value any
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
Rejection *contracts.RejectedOutput
|
||||
Validation validationReport
|
||||
@@ -125,7 +121,6 @@ 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
|
||||
@@ -210,7 +205,6 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
||||
continue
|
||||
}
|
||||
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)
|
||||
@@ -259,14 +253,13 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
||||
if incomplete := firstIncompleteValidation(report); incomplete != nil {
|
||||
provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptIncompleteAccepted, Validation: report})
|
||||
if config.Policy.ValidatorFailure == ValidatorFailureWarnContinue {
|
||||
warnings := terminalWarnings(output, report)
|
||||
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, Warnings: warnings, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, 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), Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
return producerAttemptTerminal{Action: producerTerminalAccepted, Value: output.Value, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
}
|
||||
|
||||
return failedProducerAttempt(provenance), errors.New("producer attempt budget was not exhausted deterministically")
|
||||
@@ -295,7 +288,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), Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Rejection: &rejected, Validation: report, ValidationIncomplete: firstIncompleteValidation(report) != nil, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
return producerAttemptTerminal{Action: producerTerminalRejected, 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:
|
||||
@@ -313,12 +306,6 @@ func firstIncompleteValidation(report validationReport) *validationRecord {
|
||||
return nil
|
||||
}
|
||||
|
||||
func terminalWarnings(output producerAttemptOutput, report validationReport) []contracts.Warning {
|
||||
warnings := cloneWarnings(output.Warnings)
|
||||
warnings = append(warnings, report.Warnings()...)
|
||||
return warnings
|
||||
}
|
||||
|
||||
func cloneProducerDiagnostics(diagnostics []contracts.ProducerDiagnostic) []contracts.ProducerDiagnostic {
|
||||
return contracts.CloneProducerDiagnostics(diagnostics)
|
||||
}
|
||||
|
||||
@@ -234,27 +234,27 @@ func TestRunProducerAttemptsUsesModuleRetryBudgetAndFallback(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("fallback", func(t *testing.T) {
|
||||
fallbackWarning := contracts.Warning{ReasonCode: "fallback", Message: "fallback warning"}
|
||||
fallbackDiagnostic := contracts.ProducerDiagnostic{Disposition: contracts.DiagnosticDispositionWarning, Category: contracts.DiagnosticCategoryFallback, ReasonCode: "fallback", OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: "fallback", Message: "fallback warning"}}}
|
||||
terminal, err := runProducerAttempts(context.Background(), producerAttemptConfig{Policy: DefaultValidationPolicy()}, func(context.Context, producerAttemptRequest) (producerAttemptOutput, error) {
|
||||
return producerAttemptOutput{Value: "fallback", Retry: &producerRetryDirective{FallbackWarnings: []contracts.Warning{fallbackWarning}}}, nil
|
||||
return producerAttemptOutput{Value: "fallback", Retry: &producerRetryDirective{FallbackDiagnostics: []contracts.ProducerDiagnostic{fallbackDiagnostic}}}, nil
|
||||
}, approveAttempt)
|
||||
if err != nil {
|
||||
t.Fatalf("runProducerAttempts() error = %v", err)
|
||||
}
|
||||
if terminal.Action != producerTerminalAccepted || terminal.Value != "fallback" || !reflect.DeepEqual(terminal.Warnings, []contracts.Warning{fallbackWarning}) {
|
||||
if terminal.Action != producerTerminalAccepted || terminal.Value != "fallback" || !reflect.DeepEqual(terminal.Diagnostics, []contracts.ProducerDiagnostic{fallbackDiagnostic}) {
|
||||
t.Fatalf("terminal = %#v", terminal)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunProducerAttemptsRejectionWinsOverValidatorFailure(t *testing.T) {
|
||||
firstWarnings := []contracts.Warning{{ReasonCode: "discarded", Message: "discarded warning"}}
|
||||
secondWarnings := []contracts.Warning{{ReasonCode: "accepted", Message: "accepted warning"}}
|
||||
firstDiagnostics := []contracts.ProducerDiagnostic{producerDiagnostic("discarded", "discarded warning")}
|
||||
secondDiagnostics := []contracts.ProducerDiagnostic{producerDiagnostic("accepted", "accepted warning")}
|
||||
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", Candidate: attemptCandidate(t, "defective"), Warnings: firstWarnings}, nil
|
||||
return producerAttemptOutput{Value: "first", Candidate: attemptCandidate(t, "defective"), Diagnostics: firstDiagnostics}, nil
|
||||
}
|
||||
return producerAttemptOutput{Value: "second", Candidate: attemptCandidate(t, "corrected"), Warnings: secondWarnings}, nil
|
||||
return producerAttemptOutput{Value: "second", Candidate: attemptCandidate(t, "corrected"), Diagnostics: secondDiagnostics}, nil
|
||||
}, func(_ context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
if output.Value == "first" {
|
||||
report := rejectedAttemptReport("defect")
|
||||
@@ -272,8 +272,8 @@ func TestRunProducerAttemptsRejectionWinsOverValidatorFailure(t *testing.T) {
|
||||
if got := attemptKinds(terminal.Provenance); !reflect.DeepEqual(got, []producerAttemptKind{producerAttemptInitial, producerAttemptSemanticRetry}) {
|
||||
t.Fatalf("attempt kinds = %v", got)
|
||||
}
|
||||
if got := terminal.Warnings; !reflect.DeepEqual(got, secondWarnings) {
|
||||
t.Fatalf("terminal warnings = %#v, want %#v", got, secondWarnings)
|
||||
if got := terminal.Diagnostics; !reflect.DeepEqual(got, secondDiagnostics) {
|
||||
t.Fatalf("terminal diagnostics = %#v, want %#v", got, secondDiagnostics)
|
||||
}
|
||||
if len(terminal.Provenance[0].Validation.records) != 2 {
|
||||
t.Fatalf("first validation records = %#v, want rejection and failure", terminal.Provenance[0].Validation.records)
|
||||
@@ -302,9 +302,6 @@ func TestValidationSummaryIsBoundedAndCorrectedSuccessIsQuiet(t *testing.T) {
|
||||
if len(summary.ReasonCodes) != 0 || len(summary.RejectingValidators) != 0 || len(summary.IncompleteValidators) != 0 {
|
||||
t.Fatalf("corrected success summary retained prior findings: %#v", summary)
|
||||
}
|
||||
if len(terminal.Warnings) != 0 {
|
||||
t.Fatalf("corrected success warnings = %#v, want none", terminal.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarnContinueRecordsOneWarningForEachExhaustedValidator(t *testing.T) {
|
||||
@@ -324,9 +321,6 @@ func TestWarnContinueRecordsOneWarningForEachExhaustedValidator(t *testing.T) {
|
||||
if terminal.Action != producerTerminalIncompleteAccepted {
|
||||
t.Fatalf("terminal action = %q", terminal.Action)
|
||||
}
|
||||
if len(terminal.Warnings) != 0 {
|
||||
t.Fatalf("warnings = %#v, want structured diagnostics only", terminal.Warnings)
|
||||
}
|
||||
groups, groupErr := terminalDiagnosticGroups(terminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: "step", LaneID: "lane", ModuleKey: "module"}, nil)
|
||||
if groupErr != nil {
|
||||
t.Fatalf("terminalDiagnosticGroups() error = %v", groupErr)
|
||||
|
||||
@@ -30,45 +30,45 @@ type ReferenceMaterializationOptions struct {
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
func MaterializeReferences(resolved ResolvedPipeline, catalog ModuleCatalog, options ReferenceMaterializationOptions) (ResolvedPipeline, []contracts.Warning, error) {
|
||||
func MaterializeReferences(resolved ResolvedPipeline, catalog ModuleCatalog, options ReferenceMaterializationOptions) (ResolvedPipeline, []contracts.ProducerDiagnostic, error) {
|
||||
out := resolved
|
||||
out.ChunkReferences = CloneReferenceTarget(resolved.ChunkReferences)
|
||||
chunkReferenceSet, chunkWarnings, err := materializeReferenceTarget(resolved.ID, resolved.ChunkReferences, "", catalog, options)
|
||||
chunkReferenceSet, chunkDiagnostics, err := materializeReferenceTarget(resolved.ID, resolved.ChunkReferences, "", catalog, options)
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, nil, err
|
||||
}
|
||||
out.ChunkReferences.ReferenceSet = chunkReferenceSet
|
||||
warnings := append([]contracts.Warning(nil), chunkWarnings...)
|
||||
diagnostics := contracts.CloneProducerDiagnostics(chunkDiagnostics)
|
||||
if len(resolved.Steps) == 0 {
|
||||
return out, warnings, nil
|
||||
return out, diagnostics, nil
|
||||
}
|
||||
materializeLane := func(lane ResolvedArtifactLane) (ResolvedArtifactLane, []contracts.Warning, error) {
|
||||
materializeLane := func(lane ResolvedArtifactLane) (ResolvedArtifactLane, []contracts.ProducerDiagnostic, error) {
|
||||
materializedLane := lane
|
||||
var allWarnings []contracts.Warning
|
||||
var allDiagnostics []contracts.ProducerDiagnostic
|
||||
materializedLane.ExtractReferences = CloneReferenceTarget(lane.ExtractReferences)
|
||||
materializedLane.MergeReferences = CloneReferenceTarget(lane.MergeReferences)
|
||||
materializedLane.NormalizeReferences = CloneReferenceTarget(lane.NormalizeReferences)
|
||||
extractReferenceSet, laneWarnings, err := materializeReferenceTarget(resolved.ID, lane.ExtractReferences, lane.ArtifactKind, catalog, options)
|
||||
extractReferenceSet, laneDiagnostics, err := materializeReferenceTarget(resolved.ID, lane.ExtractReferences, lane.ArtifactKind, catalog, options)
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, err
|
||||
}
|
||||
materializedLane.ExtractReferences.ReferenceSet = extractReferenceSet
|
||||
allWarnings = append(allWarnings, laneWarnings...)
|
||||
allDiagnostics = append(allDiagnostics, laneDiagnostics...)
|
||||
|
||||
mergeReferenceSet, laneWarnings, err := materializeReferenceTarget(resolved.ID, lane.MergeReferences, lane.ArtifactKind, catalog, options)
|
||||
mergeReferenceSet, laneDiagnostics, err := materializeReferenceTarget(resolved.ID, lane.MergeReferences, lane.ArtifactKind, catalog, options)
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, err
|
||||
}
|
||||
materializedLane.MergeReferences.ReferenceSet = mergeReferenceSet
|
||||
allWarnings = append(allWarnings, laneWarnings...)
|
||||
allDiagnostics = append(allDiagnostics, laneDiagnostics...)
|
||||
|
||||
normalizeReferenceSet, laneWarnings, err := materializeReferenceTarget(resolved.ID, lane.NormalizeReferences, lane.ArtifactKind, catalog, options)
|
||||
normalizeReferenceSet, laneDiagnostics, err := materializeReferenceTarget(resolved.ID, lane.NormalizeReferences, lane.ArtifactKind, catalog, options)
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, err
|
||||
}
|
||||
materializedLane.NormalizeReferences.ReferenceSet = normalizeReferenceSet
|
||||
allWarnings = append(allWarnings, laneWarnings...)
|
||||
return materializedLane, allWarnings, nil
|
||||
allDiagnostics = append(allDiagnostics, laneDiagnostics...)
|
||||
return materializedLane, allDiagnostics, nil
|
||||
}
|
||||
if len(resolved.Steps) > 0 {
|
||||
out.Steps = make([]ResolvedPipelineStep, len(resolved.Steps))
|
||||
@@ -76,36 +76,16 @@ func MaterializeReferences(resolved ResolvedPipeline, catalog ModuleCatalog, opt
|
||||
out.Steps[i].ID = step.ID
|
||||
out.Steps[i].ArtifactLanes = make([]ResolvedArtifactLane, len(step.ArtifactLanes))
|
||||
for j, lane := range step.ArtifactLanes {
|
||||
materializedLane, laneWarnings, err := materializeLane(lane)
|
||||
materializedLane, laneDiagnostics, err := materializeLane(lane)
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, nil, err
|
||||
}
|
||||
out.Steps[i].ArtifactLanes[j] = materializedLane
|
||||
warnings = append(warnings, laneWarnings...)
|
||||
diagnostics = append(diagnostics, laneDiagnostics...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, warnings, nil
|
||||
}
|
||||
|
||||
// ReferenceDiagnostics converts framework-owned empty-reference findings into
|
||||
// structured process diagnostics. Legacy callers may continue to use the
|
||||
// materializer's warning return while the framework migration is in progress.
|
||||
func ReferenceDiagnostics(warnings []contracts.Warning) []contracts.ProducerDiagnostic {
|
||||
diagnostics := make([]contracts.ProducerDiagnostic, 0, len(warnings))
|
||||
for _, warning := range warnings {
|
||||
if warning.ReasonCode != "empty_reference" {
|
||||
continue
|
||||
}
|
||||
diagnostics = append(diagnostics, contracts.ProducerDiagnostic{
|
||||
Disposition: contracts.DiagnosticDispositionWarning,
|
||||
Category: contracts.DiagnosticCategoryConfiguration,
|
||||
ReasonCode: "empty_reference",
|
||||
OccurrenceCount: 1,
|
||||
Samples: []contracts.DiagnosticSample{{Scope: warning.Scope, Message: warning.Message}},
|
||||
})
|
||||
}
|
||||
return contracts.CloneProducerDiagnostics(diagnostics)
|
||||
return out, diagnostics, nil
|
||||
}
|
||||
|
||||
func materializeReferenceTarget(
|
||||
@@ -114,7 +94,7 @@ func materializeReferenceTarget(
|
||||
artifactKind contracts.ArtifactKind,
|
||||
catalog ModuleCatalog,
|
||||
options ReferenceMaterializationOptions,
|
||||
) (contracts.ReferenceSet, []contracts.Warning, error) {
|
||||
) (contracts.ReferenceSet, []contracts.ProducerDiagnostic, error) {
|
||||
if len(target.Bindings) == 0 {
|
||||
return contracts.ReferenceSet{}, nil, nil
|
||||
}
|
||||
@@ -129,7 +109,7 @@ func materializeReferenceTarget(
|
||||
}
|
||||
|
||||
set := contracts.ReferenceSet{Slots: make(map[string]contracts.ResolvedReferenceSlot, len(target.Bindings))}
|
||||
var warnings []contracts.Warning
|
||||
var diagnostics []contracts.ProducerDiagnostic
|
||||
for _, binding := range target.Bindings {
|
||||
slotName := strings.TrimSpace(binding.SlotName)
|
||||
slot, ok := slotByName[slotName]
|
||||
@@ -179,11 +159,7 @@ func materializeReferenceTarget(
|
||||
return contracts.ReferenceSet{}, nil, fmt.Errorf("%s reference slot %q path %q media type %q is not accepted", referenceTargetContext(pipelineID, target), slotName, path, mediaType)
|
||||
}
|
||||
if len(content) == 0 {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: referenceWarningScope(pipelineID, target, slotName),
|
||||
ReasonCode: "empty_reference",
|
||||
Message: fmt.Sprintf("reference slot %q for %s is bound to an empty file", slotName, referenceTargetLabel(target)),
|
||||
})
|
||||
diagnostics = append(diagnostics, contracts.ProducerDiagnostic{Disposition: contracts.DiagnosticDispositionWarning, Category: contracts.DiagnosticCategoryConfiguration, ReasonCode: "empty_reference", OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: referenceWarningScope(pipelineID, target, slotName), Message: fmt.Sprintf("reference slot %q for %s is bound to an empty file", slotName, referenceTargetLabel(target))}}})
|
||||
}
|
||||
|
||||
item := contracts.ReferenceItem{
|
||||
@@ -200,7 +176,7 @@ func materializeReferenceTarget(
|
||||
Items: []contracts.ReferenceItem{item},
|
||||
}
|
||||
}
|
||||
return set, warnings, nil
|
||||
return set, diagnostics, nil
|
||||
}
|
||||
|
||||
type referenceSizeLimitError struct {
|
||||
|
||||
@@ -445,17 +445,13 @@ func TestMaterializeReferencesWarnsForEmptyFiles(t *testing.T) {
|
||||
writeReferenceFile(t, path, nil)
|
||||
|
||||
resolved := resolvedPipelineWithReference(t, "roster", "empty.txt", contracts.ReferenceBindingSourceConfig, contracts.ReferenceSlot{Name: "roster"})
|
||||
materialized, warnings, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{{Name: "roster"}}), ReferenceMaterializationOptions{
|
||||
materialized, diagnostics, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{{Name: "roster"}}), ReferenceMaterializationOptions{
|
||||
ConfigPath: filepath.Join(configDir, "config.yml"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("MaterializeReferences() error = %v, want nil", err)
|
||||
}
|
||||
if len(warnings) != 1 || warnings[0].ReasonCode != "empty_reference" {
|
||||
t.Fatalf("warnings = %#v, want empty reference warning", warnings)
|
||||
}
|
||||
diagnostics := ReferenceDiagnostics(warnings)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || diagnostics[0].Category != contracts.DiagnosticCategoryConfiguration || diagnostics[0].ReasonCode != "empty_reference" || diagnostics[0].OccurrenceCount != 1 || !reflect.DeepEqual(diagnostics[0].Samples, []contracts.DiagnosticSample{{Scope: warnings[0].Scope, Message: warnings[0].Message}}) {
|
||||
if len(diagnostics) != 1 || diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || diagnostics[0].Category != contracts.DiagnosticCategoryConfiguration || diagnostics[0].ReasonCode != "empty_reference" || diagnostics[0].OccurrenceCount != 1 || len(diagnostics[0].Samples) != 1 {
|
||||
t.Fatalf("diagnostics = %#v, want structured empty-reference signal", diagnostics)
|
||||
}
|
||||
item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0]
|
||||
@@ -487,13 +483,13 @@ func TestMaterializeReferencesWarningScopesIncludeTargetContext(t *testing.T) {
|
||||
t.Fatalf("ResolvePipeline() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
_, warnings, err := MaterializeReferences(resolved, catalog, ReferenceMaterializationOptions{
|
||||
_, diagnostics, err := MaterializeReferences(resolved, catalog, ReferenceMaterializationOptions{
|
||||
ConfigPath: filepath.Join(configDir, "config.yml"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("MaterializeReferences() error = %v, want nil", err)
|
||||
}
|
||||
got := warningScopes(warnings)
|
||||
got := diagnosticScopes(diagnostics)
|
||||
want := []string{
|
||||
"pipeline.baseline.chunk.reference.scene_guide",
|
||||
"pipeline.baseline.lane.events.extract.reference.roster",
|
||||
@@ -625,10 +621,10 @@ func referenceCatalogForTargets(t *testing.T, chunkSlots, extractSlots, mergeSlo
|
||||
)
|
||||
}
|
||||
|
||||
func warningScopes(warnings []contracts.Warning) []string {
|
||||
scopes := make([]string, 0, len(warnings))
|
||||
for _, warning := range warnings {
|
||||
scopes = append(scopes, warning.Scope)
|
||||
func diagnosticScopes(diagnostics []contracts.ProducerDiagnostic) []string {
|
||||
scopes := make([]string, 0, len(diagnostics))
|
||||
for _, diagnostic := range diagnostics {
|
||||
scopes = append(scopes, diagnostic.Samples[0].Scope)
|
||||
}
|
||||
sort.Strings(scopes)
|
||||
return scopes
|
||||
|
||||
@@ -50,7 +50,6 @@ type RunInput struct {
|
||||
StartedAt time.Time
|
||||
LLMProfiles []artifacts.LLMProfileManifest
|
||||
Metadata map[string]any
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
ChunkCacheMode ChunkCacheMode
|
||||
ChunkPlans ChunkPlanStore
|
||||
@@ -74,7 +73,6 @@ type RunOutput struct {
|
||||
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"`
|
||||
@@ -132,7 +130,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
defer func() {
|
||||
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)
|
||||
@@ -256,14 +253,12 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
if chunkResult.validation != nil {
|
||||
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,
|
||||
"accepted": chunkResult.accepted,
|
||||
"materialized_chunks": debugSourceChunkEnvelopes(chunkResult.chunks),
|
||||
"warnings": chunkResult.warnings,
|
||||
}
|
||||
if chunkResult.plan != nil {
|
||||
chunkDebugPayload["plan"] = debugChunkPlanEnvelope(*chunkResult.plan)
|
||||
@@ -356,7 +351,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
"manifest": output.Manifest,
|
||||
"normalize_outputs": debugSerializedOutputEnvelopes(output.NormalizeOutputs),
|
||||
"rejected": debugRejectedOutputEnvelopes(output.Rejected),
|
||||
"warnings": output.Warnings,
|
||||
"options": redactSensitiveMap(input.pipeline.Output.Options),
|
||||
"metadata": redactSensitiveMap(input.Metadata),
|
||||
}
|
||||
@@ -385,7 +379,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
||||
Manifest: output.Manifest,
|
||||
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),
|
||||
@@ -455,7 +448,6 @@ func (r *Runner) runPreparedSteps(ctx context.Context, input RunInput, checkpoin
|
||||
type retryAttemptResult struct {
|
||||
accepted bool
|
||||
rejection *contracts.RejectedOutput
|
||||
warnings []contracts.Warning
|
||||
}
|
||||
|
||||
func runSimpleRetry(ctx context.Context, retries int, run func(attempt int) (retryAttemptResult, error)) (retryAttemptResult, error) {
|
||||
@@ -485,7 +477,7 @@ func runSimpleRetry(ctx context.Context, retries int, run func(attempt int) (ret
|
||||
if result.rejection != nil {
|
||||
rejection := *result.rejection
|
||||
rejection.AttemptCount = attempt
|
||||
last = retryAttemptResult{rejection: &rejection, warnings: cloneWarnings(result.warnings)}
|
||||
last = retryAttemptResult{rejection: &rejection}
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return retryAttemptResult{}, ctxErr
|
||||
@@ -708,6 +700,10 @@ func validatorChainManifests(chains []ResolvedValidatorChain) []artifacts.Valida
|
||||
}
|
||||
|
||||
func failOutput(output RunOutput) RunOutput {
|
||||
// Preserve any diagnostics already accepted by the pipeline when a later
|
||||
// operation fails. Producers and validators validate each diagnostic before
|
||||
// it is appended, so finalization here cannot introduce a new failure path.
|
||||
_ = finalizeDiagnostics(&output)
|
||||
if output.Manifest.PipelineID != "" {
|
||||
populateOutputManifest(&output)
|
||||
output.Manifest.ValidationStatus = "failed"
|
||||
@@ -982,13 +978,6 @@ func manifestMetadataWithSessionID(metadata map[string]any, sessionID string) (m
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func cloneWarnings(warnings []contracts.Warning) []contracts.Warning {
|
||||
if len(warnings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return append([]contracts.Warning(nil), warnings...)
|
||||
}
|
||||
|
||||
func cloneSourceChunkPtr(chunk *source.Chunk) (*source.Chunk, error) {
|
||||
if chunk == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -84,7 +84,8 @@ func TestRunnerHydratesRequiredNormalizedArtifact(t *testing.T) {
|
||||
loader := newAcceptedCheckpointLoader()
|
||||
producerKey := CheckpointLaneKey(producer.resolved.StepID, producer.resolved.ID)
|
||||
consumerKey := CheckpointLaneKey(consumer.resolved.StepID, consumer.resolved.ID)
|
||||
loader.accepted[producerKey] = NormalizeCheckpoint{Output: stored, Warnings: []contracts.Warning{{Scope: "normalize", ReasonCode: "stored-warning", Message: "stored normalize warning"}}}
|
||||
stored.Diagnostics = []CheckpointDiagnostic{{Diagnostic: contracts.ProducerDiagnostic{Disposition: contracts.DiagnosticDispositionWarning, Category: contracts.DiagnosticCategoryFallback, ReasonCode: "stored-warning", OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: "normalize", Message: "stored normalize warning"}}}}}
|
||||
loader.accepted[producerKey] = NormalizeCheckpoint{Output: stored}
|
||||
loader.acceptedDecision[producerKey] = NewCheckpointDecision(CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
||||
policy := CheckpointExecutionPolicy{
|
||||
RequireReusableLanes: map[string]struct{}{producerKey: {}},
|
||||
@@ -101,8 +102,8 @@ func TestRunnerHydratesRequiredNormalizedArtifact(t *testing.T) {
|
||||
if string(item.Content) != string(stored.Artifact.Content) || item.Producer.StepID != producer.resolved.StepID || item.Producer.LaneID != producer.resolved.ID {
|
||||
t.Fatalf("consumer generated reference = %#v, want exact hydrated producer bytes and identity", item)
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != "stored-warning" {
|
||||
t.Fatalf("hydrated warnings = %#v, want normalize checkpoint warnings only", output.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "stored-warning" {
|
||||
t.Fatalf("hydrated diagnostics = %#v, want normalize checkpoint diagnostics only", output.Diagnostics)
|
||||
}
|
||||
assertAcceptedNormalizeEvent(t, output.CheckpointEvents, producer.resolved.StepID, producer.resolved.ID, CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
||||
for _, event := range output.CheckpointEvents {
|
||||
@@ -225,7 +226,8 @@ func TestRunnerRetainsEarlierHydratedProducerWhenLaterRequiredProducerFails(t *t
|
||||
loader := newAcceptedCheckpointLoader()
|
||||
firstKey := CheckpointLaneKey(first.resolved.StepID, first.resolved.ID)
|
||||
secondKey := CheckpointLaneKey(second.resolved.StepID, second.resolved.ID)
|
||||
loader.accepted[firstKey] = NormalizeCheckpoint{Output: stored, Warnings: []contracts.Warning{{Scope: "normalize", ReasonCode: "retained-warning", Message: "retained warning"}}}
|
||||
stored.Diagnostics = []CheckpointDiagnostic{{Diagnostic: contracts.ProducerDiagnostic{Disposition: contracts.DiagnosticDispositionWarning, Category: contracts.DiagnosticCategoryFallback, ReasonCode: "retained-warning", OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: "normalize", Message: "retained warning"}}}}}
|
||||
loader.accepted[firstKey] = NormalizeCheckpoint{Output: stored}
|
||||
loader.acceptedDecision[firstKey] = NewCheckpointDecision(CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
||||
loader.acceptedDecision[secondKey] = NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing)
|
||||
policy := CheckpointExecutionPolicy{RequireReusableLanes: map[string]struct{}{firstKey: {}, secondKey: {}}}
|
||||
@@ -240,8 +242,8 @@ func TestRunnerRetainsEarlierHydratedProducerWhenLaterRequiredProducerFails(t *t
|
||||
if len(output.NormalizeOutputs) != 1 || output.NormalizeOutputs[0].LaneID != first.resolved.ID || string(output.NormalizeOutputs[0].Artifact.Content) != string(stored.Artifact.Content) {
|
||||
t.Fatalf("retained normalize outputs = %#v, want first producer", output.NormalizeOutputs)
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != "retained-warning" {
|
||||
t.Fatalf("retained warnings = %#v", output.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "retained-warning" {
|
||||
t.Fatalf("retained diagnostics = %#v", output.Diagnostics)
|
||||
}
|
||||
type decisionExpectation struct {
|
||||
step, lane string
|
||||
|
||||
@@ -162,13 +162,13 @@ func TestRunnerWritesAttemptScopedMergeAndNormalizeDebug(t *testing.T) {
|
||||
if err := callAttemptDebugLLM(ctx, client, "merge"); err != nil {
|
||||
return erasedTypedResult{}, err
|
||||
}
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"merged"}}, Warnings: []contracts.Warning{{Scope: "merge", ReasonCode: "observed", Message: "merge warning"}}}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"merged"}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("merge", "merge diagnostic")}}, nil
|
||||
}
|
||||
prepared.Steps[0].lanes[0].typed.normalize = func(ctx context.Context, _ any, _ contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
|
||||
if err := callAttemptDebugLLM(ctx, client, "normalize"); err != nil {
|
||||
return erasedTypedResult{}, err
|
||||
}
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"normalized"}}, Warnings: []contracts.Warning{{Scope: "normalize", ReasonCode: "observed", Message: "normalize warning"}}}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"normalized"}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("normalize", "normalize diagnostic")}}, nil
|
||||
}
|
||||
|
||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Debug: debug})
|
||||
@@ -213,7 +213,7 @@ func TestRunnerWritesAttemptScopedMergeAndNormalizeDebug(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerRecordsDistinctRetryAttemptsAndPromotesAcceptedWarningsOnly(t *testing.T) {
|
||||
func TestRunnerRecordsDistinctRetryAttemptsAndPromotesAcceptedDiagnosticsOnly(t *testing.T) {
|
||||
for _, stage := range []ModuleStage{StageMerge, StageNormalize} {
|
||||
t.Run(string(stage), func(t *testing.T) {
|
||||
prepared := preparedAttemptDebugPipeline(t)
|
||||
@@ -230,7 +230,7 @@ func TestRunnerRecordsDistinctRetryAttemptsAndPromotesAcceptedWarningsOnly(t *te
|
||||
if attempts == 1 {
|
||||
scope = "discarded"
|
||||
}
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{scope}}, Warnings: []contracts.Warning{{Scope: scope, ReasonCode: "observed", Message: scope}}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%s"]}`, scope))}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{scope}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(scope, scope)}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%s"]}`, scope))}, nil
|
||||
}
|
||||
validatorCalls := 0
|
||||
validator := preparedValidator{
|
||||
@@ -268,11 +268,11 @@ func TestRunnerRecordsDistinctRetryAttemptsAndPromotesAcceptedWarningsOnly(t *te
|
||||
if len(first.LLMCalls) != 1 || len(second.LLMCalls) != 1 || first.LLMCalls[0].CallID == second.LLMCalls[0].CallID {
|
||||
t.Fatalf("retry LLM calls = first %#v, second %#v; want distinct calls", first.LLMCalls, second.LLMCalls)
|
||||
}
|
||||
if !strings.Contains(string(debug.json[firstPath]), "discarded") || !strings.Contains(string(debug.json[firstPath]), "rejection") {
|
||||
t.Fatalf("first attempt envelope = %s, want discarded warning and rejection", debug.json[firstPath])
|
||||
if !strings.Contains(string(debug.json[firstPath]), "rejection") {
|
||||
t.Fatalf("first attempt envelope = %s, want rejection", debug.json[firstPath])
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].Scope != "accepted" {
|
||||
t.Fatalf("promoted warnings = %#v, want accepted attempt only", output.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].Samples[0].Scope != "accepted" {
|
||||
t.Fatalf("promoted diagnostics = %#v, want accepted attempt only", output.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,13 +101,13 @@ type candidateCheckpointRecorder struct {
|
||||
normalizeOutput CheckpointArtifact
|
||||
}
|
||||
|
||||
func (r *candidateCheckpointRecorder) MergeSucceeded(_ string, _ string, _ []CheckpointFingerprint, output CheckpointArtifact, _ []contracts.Warning) error {
|
||||
func (r *candidateCheckpointRecorder) MergeSucceeded(_ string, _ string, _ []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
r.mergeSucceeded++
|
||||
r.mergeOutput = cloneCheckpointArtifact(output)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *candidateCheckpointRecorder) NormalizeSucceeded(_ string, _ string, _ []CheckpointFingerprint, output CheckpointArtifact, _ []contracts.Warning) error {
|
||||
func (r *candidateCheckpointRecorder) NormalizeSucceeded(_ string, _ string, _ []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
r.normalizeSucceeded++
|
||||
r.normalizeOutput = cloneCheckpointArtifact(output)
|
||||
return nil
|
||||
|
||||
@@ -15,7 +15,6 @@ type chunkPlanExecution struct {
|
||||
accepted bool
|
||||
chunks []source.Chunk
|
||||
plan *source.ChunkPlan
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejection *contracts.RejectedOutput
|
||||
lookup ChunkPlanDecision
|
||||
@@ -29,7 +28,6 @@ type generatedChunkPlanCandidate struct {
|
||||
plan source.ChunkPlan
|
||||
chunks []source.Chunk
|
||||
record ChunkPlanRecord
|
||||
producerWarnings []contracts.Warning
|
||||
producerDiagnostics []contracts.ProducerDiagnostic
|
||||
terminal *attemptTerminalRecorder
|
||||
}
|
||||
@@ -71,7 +69,7 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
if validationErr == nil {
|
||||
report, err := r.validateChunkReport(ctx, doc, chunker.Key(), chunks, sourceInput, sessionID, input.pipeline.ChunkReferences.ReferenceSet, input.Metadata, input.Prepared.chunkValidators, 1, input.Debug)
|
||||
if err != nil {
|
||||
result.setValidation(report.Warnings(), nil, err)
|
||||
result.setValidation(nil, err)
|
||||
return result, err
|
||||
}
|
||||
rejection := report.FirstRejection()
|
||||
@@ -82,9 +80,8 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
}
|
||||
result.plan = &plan
|
||||
result.chunks = chunks
|
||||
result.warnings = append(cloneWarnings(record.Warnings), report.Warnings()...)
|
||||
result.accepted = true
|
||||
result.setValidation(report.Warnings(), nil, nil)
|
||||
result.setValidation(nil, nil)
|
||||
cachedTerminal := producerAttemptTerminal{Action: producerTerminalAccepted, Diagnostics: contracts.CloneProducerDiagnostics(record.Diagnostics), Validation: report}
|
||||
diagnostics, diagnosticErr := terminalDiagnosticGroups(cachedTerminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageChunk, ModuleKey: chunker.Key()}, nil)
|
||||
if diagnosticErr != nil {
|
||||
@@ -97,7 +94,7 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
}
|
||||
if incomplete != nil && input.pipeline.ChunkValidationPolicy.ValidatorFailure == ValidatorFailureFailRun {
|
||||
failure := validatorFailureError(*incomplete)
|
||||
result.setValidation(report.Warnings(), nil, failure)
|
||||
result.setValidation(nil, failure)
|
||||
failedTerminal := producerAttemptTerminal{Action: producerTerminalFailed, Validation: report, ValidationIncomplete: true}
|
||||
summary := validationSummary(failedTerminal, StageChunk, "", "", chunker.Key(), "", 0)
|
||||
result.validation = &summary
|
||||
@@ -145,7 +142,7 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
plan, chunks, validationErr := validateAndMaterializeChunkPlan(doc, chunkResult.Plan)
|
||||
if validationErr != nil {
|
||||
attemptErr := fmt.Errorf("validate chunk plan from chunker %q: %w", chunker.Key(), validationErr)
|
||||
payload := map[string]any{"plan": debugChunkPlanEnvelope(chunkResult.Plan), "warnings": debugWarningEnvelopes(chunkResult.Warnings)}
|
||||
payload := map[string]any{"plan": debugChunkPlanEnvelope(chunkResult.Plan)}
|
||||
return producerAttemptOutput{}, attemptTerminal.record(payload, fmt.Errorf("%w: %v", contracts.ErrInvalidStructuredOutput, attemptErr))
|
||||
}
|
||||
planDigest, digestErr := source.DigestChunkPlan(plan)
|
||||
@@ -168,19 +165,18 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
References: append([]artifacts.ReferenceProvenance(nil), referenceTargetProvenance(input.pipeline.ChunkReferences)...),
|
||||
Metadata: producerMetadata,
|
||||
},
|
||||
Warnings: cloneWarnings(chunkResult.Warnings), CreatedAt: time.Now().UTC(),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
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
|
||||
return producerAttemptOutput{Value: generatedChunkPlanCandidate{plan: plan, chunks: chunks, record: candidate, producerDiagnostics: contracts.CloneProducerDiagnostics(chunkResult.Diagnostics), terminal: &attemptTerminal}, Candidate: chunkResult.ModelCandidate, Diagnostics: contracts.CloneProducerDiagnostics(chunkResult.Diagnostics)}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(generatedChunkPlanCandidate)
|
||||
if !ok {
|
||||
return validationReport{}, fmt.Errorf("chunk attempt candidate has incompatible type")
|
||||
}
|
||||
report, validationErr := r.validateChunkReport(validationCtx, doc, chunker.Key(), candidate.chunks, sourceInput, sessionID, input.pipeline.ChunkReferences.ReferenceSet, input.Metadata, input.Prepared.chunkValidators, candidate.terminal.envelope.Attempt, input.Debug)
|
||||
attemptWarnings := append(cloneWarnings(output.Warnings), report.Warnings()...)
|
||||
payload := map[string]any{
|
||||
"plan": debugChunkPlanEnvelope(candidate.plan), "materialized_chunks": debugSourceChunkEnvelopes(candidate.chunks),
|
||||
"warnings": debugWarningEnvelopes(attemptWarnings), "rejection": debugRejectedOutputPtr(chunkRejection(report, candidate.terminal.envelope.Attempt, chunker.Key())),
|
||||
"rejection": debugRejectedOutputPtr(chunkRejection(report, candidate.terminal.envelope.Attempt, chunker.Key())),
|
||||
}
|
||||
if validationErr != nil {
|
||||
return report, candidate.terminal.record(payload, validationErr)
|
||||
@@ -208,12 +204,11 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
result.rejection.Stage = string(StageChunk)
|
||||
result.rejection.ModuleKey = chunker.Key()
|
||||
}
|
||||
result.warnings = cloneWarnings(terminal.Warnings)
|
||||
result.validation = &terminalSummary
|
||||
if result.rejection != nil {
|
||||
result.rejection.Validation = cloneValidationSummaryPtr(result.validation)
|
||||
}
|
||||
result.setValidation(terminal.Validation.Warnings(), result.rejection, nil)
|
||||
result.setValidation(result.rejection, nil)
|
||||
return result, nil
|
||||
}
|
||||
candidate, ok := terminal.Value.(generatedChunkPlanCandidate)
|
||||
@@ -234,9 +229,8 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
result.accepted = true
|
||||
result.plan = &candidate.plan
|
||||
result.chunks = candidate.chunks
|
||||
result.warnings = cloneWarnings(terminal.Warnings)
|
||||
result.validation = &terminalSummary
|
||||
result.setValidation(terminal.Validation.Warnings(), nil, nil)
|
||||
result.setValidation(nil, nil)
|
||||
if terminal.ValidationIncomplete {
|
||||
result.summary.ValidationStatus = "incomplete"
|
||||
}
|
||||
@@ -246,7 +240,6 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
||||
if cloneErr != nil {
|
||||
return result, fmt.Errorf("clone chunk plan record for publication: %w", cloneErr)
|
||||
}
|
||||
record.Warnings = cloneWarnings(candidate.producerWarnings)
|
||||
record.Diagnostics = contracts.CloneProducerDiagnostics(candidate.producerDiagnostics)
|
||||
if err := input.ChunkPlans.Save(record); err != nil {
|
||||
return result, fmt.Errorf("save chunk plan: %w", err)
|
||||
@@ -303,14 +296,12 @@ func (result *chunkPlanExecution) setCandidate(record ChunkPlanRecord, action st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (result *chunkPlanExecution) setValidation(warnings []contracts.Warning, rejection *contracts.RejectedOutput, err error) {
|
||||
func (result *chunkPlanExecution) setValidation(rejection *contracts.RejectedOutput, err error) {
|
||||
switch {
|
||||
case err != nil:
|
||||
result.summary.ValidationStatus = "error"
|
||||
case rejection != nil:
|
||||
result.summary.ValidationStatus = "rejected"
|
||||
case len(warnings) > 0:
|
||||
result.summary.ValidationStatus = "approved_with_warnings"
|
||||
default:
|
||||
result.summary.ValidationStatus = "approved"
|
||||
}
|
||||
@@ -324,7 +315,6 @@ func cloneChunkPlanRecord(record ChunkPlanRecord) (ChunkPlanRecord, error) {
|
||||
return ChunkPlanRecord{}, fmt.Errorf("clone chunk plan producer metadata: %w", err)
|
||||
}
|
||||
record.Producer.Metadata = metadata
|
||||
record.Warnings = cloneWarnings(record.Warnings)
|
||||
record.Diagnostics = contracts.CloneProducerDiagnostics(record.Diagnostics)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ func (*retryingChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil
|
||||
func (c *retryingChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||
c.calls++
|
||||
if c.calls == 1 {
|
||||
return contracts.ChunkPlanResult{Warnings: []contracts.Warning{{Scope: "discarded", ReasonCode: "retry", Message: "discarded warning"}}}, errors.New("retry generation")
|
||||
return contracts.ChunkPlanResult{Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("discarded", "discarded diagnostic")}}, errors.New("retry generation")
|
||||
}
|
||||
return contracts.ChunkPlanResult{Plan: source.CloneChunkPlan(c.plan), Warnings: []contracts.Warning{{Scope: "accepted", ReasonCode: "observed", Message: "accepted warning"}}}, nil
|
||||
return contracts.ChunkPlanResult{Plan: source.CloneChunkPlan(c.plan), Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("accepted", "accepted diagnostic")}}, nil
|
||||
}
|
||||
|
||||
type dependencyLoader struct {
|
||||
@@ -399,7 +399,7 @@ func TestRunnerRegeneratesValidationIncompleteChunkPlanHit(t *testing.T) {
|
||||
}},
|
||||
}}
|
||||
record := chunkPlanRecord(t, prepared, plan)
|
||||
record.Warnings = []contracts.Warning{{Scope: "stored", ReasonCode: "old", Message: "discarded stored warning"}}
|
||||
record.Diagnostics = []contracts.ProducerDiagnostic{producerDiagnostic("stored", "discarded stored diagnostic")}
|
||||
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||
debug := newCapturedDebugRecorder()
|
||||
|
||||
@@ -413,8 +413,8 @@ func TestRunnerRegeneratesValidationIncompleteChunkPlanHit(t *testing.T) {
|
||||
if output.ChunkPlan == nil || output.ChunkPlan.Action != "generated" || output.ChunkPlan.LookupStatus != "invalid" || output.ChunkPlan.ValidationStatus != "approved" {
|
||||
t.Fatalf("chunk plan summary = %#v", output.ChunkPlan)
|
||||
}
|
||||
if len(output.Warnings) != 0 {
|
||||
t.Fatalf("warnings = %#v, want discarded cache warnings omitted", output.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 0 {
|
||||
t.Fatalf("diagnostics = %#v, want discarded cache diagnostics omitted", output.Diagnostics)
|
||||
}
|
||||
assertAttemptEnvelopeSequence(t, debug, "chunk", 1)
|
||||
}
|
||||
@@ -427,7 +427,7 @@ func TestRunnerKeepsStoredPlanWhenCacheAndGeneratedValidationAreIncomplete(t *te
|
||||
validator := &countingChunkValidator{err: errors.New("validator unavailable")}
|
||||
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||
record := chunkPlanRecord(t, prepared, plan)
|
||||
record.Warnings = []contracts.Warning{{Scope: "stored", ReasonCode: "old", Message: "discarded stored warning"}}
|
||||
record.Diagnostics = []contracts.ProducerDiagnostic{producerDiagnostic("stored", "discarded stored diagnostic")}
|
||||
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||
|
||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto, ChunkPlans: store})
|
||||
@@ -440,8 +440,8 @@ func TestRunnerKeepsStoredPlanWhenCacheAndGeneratedValidationAreIncomplete(t *te
|
||||
if output.ChunkPlan == nil || output.ChunkPlan.Action != "generated" || output.ChunkPlan.LookupStatus != "invalid" || output.ChunkPlan.ValidationStatus != "incomplete" {
|
||||
t.Fatalf("chunk plan summary = %#v", output.ChunkPlan)
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != "validator_execution_incomplete" {
|
||||
t.Fatalf("warnings = %#v, want only generated incomplete warning", output.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
|
||||
t.Fatalf("diagnostics = %#v, want only generated incomplete diagnostic", output.Diagnostics)
|
||||
}
|
||||
if !reflect.DeepEqual(store.record, record) {
|
||||
t.Fatal("discarded incomplete candidates mutated the stored record")
|
||||
@@ -611,8 +611,8 @@ func TestRunnerRetriesBeforePublishingAcceptedPlan(t *testing.T) {
|
||||
if chunker.calls != 2 || store.saves != 1 {
|
||||
t.Fatalf("module calls = %d saves = %d, want 2 and 1", chunker.calls, store.saves)
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].Scope != "accepted" || len(store.saved.Warnings) != 1 || store.saved.Warnings[0].Scope != "accepted" {
|
||||
t.Fatalf("output warnings = %#v stored warnings = %#v", output.Warnings, store.saved.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].Samples[0].Scope != "accepted" || len(store.saved.Diagnostics) != 1 || store.saved.Diagnostics[0].Samples[0].Scope != "accepted" {
|
||||
t.Fatalf("output diagnostics = %#v stored diagnostics = %#v", output.Diagnostics, store.saved.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +624,7 @@ func TestRunnerAutoHitValidatesOnceWithoutRegenerationOrMutation(t *testing.T) {
|
||||
wantError string
|
||||
wantReject bool
|
||||
}{
|
||||
{name: "warning", result: contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: "current", ReasonCode: "observed", Message: "current warning"}}}},
|
||||
{name: "diagnostic", result: contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("current", "current diagnostic")}}},
|
||||
{name: "rejection", result: contracts.ValidationResult{Approved: false, ReasonCode: "rejected", Message: "rejected hit", CorrectionGuidance: "return an acceptable chunk plan"}, wantReject: true},
|
||||
{name: "error", validatorErr: errors.New("validator failed"), wantError: "validator failed"},
|
||||
}
|
||||
@@ -637,7 +637,7 @@ func TestRunnerAutoHitValidatesOnceWithoutRegenerationOrMutation(t *testing.T) {
|
||||
validator := &countingChunkValidator{result: tc.result, err: tc.validatorErr}
|
||||
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||
record := chunkPlanRecord(t, prepared, plan)
|
||||
record.Warnings = []contracts.Warning{{Scope: "stored", ReasonCode: "observed", Message: "stored warning"}}
|
||||
record.Diagnostics = []contracts.ProducerDiagnostic{producerDiagnostic("stored", "stored diagnostic")}
|
||||
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||
debug := newCapturedDebugRecorder()
|
||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto, ChunkPlans: store, Debug: debug})
|
||||
@@ -673,8 +673,8 @@ func TestRunnerAutoHitValidatesOnceWithoutRegenerationOrMutation(t *testing.T) {
|
||||
if tc.wantReject && (len(output.Rejected) != 1 || output.Rejected[0].ReasonCode != "rejected") {
|
||||
t.Fatalf("rejected = %#v", output.Rejected)
|
||||
}
|
||||
if tc.wantError == "" && !tc.wantReject && len(output.Warnings) != 1+len(tc.result.Warnings) {
|
||||
t.Fatalf("warnings = %#v, want stored warning once plus current warnings", output.Warnings)
|
||||
if tc.wantError == "" && !tc.wantReject && len(output.Diagnostics.Groups) != 1+len(tc.result.Diagnostics) {
|
||||
t.Fatalf("diagnostics = %#v, want stored diagnostic once plus current diagnostics", output.Diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -724,7 +724,7 @@ func TestRunnerPublishesOnlyAcceptedGeneratedPlans(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerStoresProducerProvenanceAndProducerWarnings(t *testing.T) {
|
||||
func TestRunnerStoresProducerProvenanceAndDiagnostics(t *testing.T) {
|
||||
prepared, plan := preparedTerminalDebugPipeline(t)
|
||||
prepared.resolved.Chunk.LLMProfile = "chunk-profile"
|
||||
prepared.resolved.ChunkExecutionClass = contracts.ExecutionClassLLMBacked
|
||||
@@ -734,8 +734,8 @@ func TestRunnerStoresProducerProvenanceAndProducerWarnings(t *testing.T) {
|
||||
"guide": {Items: []contracts.ReferenceItem{{SlotName: "guide", Digest: "sha256:guide", Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///guide.txt"}, Content: []byte("sensitive")}}},
|
||||
}},
|
||||
}
|
||||
prepared.chunker = manifestChunker{terminalChunker: terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan, warnings: []contracts.Warning{{Scope: "producer", ReasonCode: "observed", Message: "producer warning"}}}, metadata: map[string]any{"prompt_id": "chunk/prompt"}}
|
||||
validator := &countingChunkValidator{result: contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: "validator", ReasonCode: "observed", Message: "validator warning"}}}}
|
||||
prepared.chunker = manifestChunker{terminalChunker: terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan, diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("producer", "producer diagnostic")}}, metadata: map[string]any{"prompt_id": "chunk/prompt"}}
|
||||
validator := &countingChunkValidator{result: contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("validator", "validator diagnostic")}}}
|
||||
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||
store := &recordingChunkPlanStore{}
|
||||
if _, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheRefresh, ChunkPlans: store}); err != nil {
|
||||
@@ -748,8 +748,8 @@ func TestRunnerStoresProducerProvenanceAndProducerWarnings(t *testing.T) {
|
||||
if len(producer.References) != 1 || producer.References[0].Digest != "sha256:guide" {
|
||||
t.Fatalf("producer references = %#v", producer.References)
|
||||
}
|
||||
if len(store.saved.Warnings) != 1 || store.saved.Warnings[0].Scope != "producer" {
|
||||
t.Fatalf("stored warnings = %#v, want only producer warning", store.saved.Warnings)
|
||||
if len(store.saved.Diagnostics) != 1 || store.saved.Diagnostics[0].Samples[0].Scope != "producer" {
|
||||
t.Fatalf("stored diagnostics = %#v, want only producer diagnostic", store.saved.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,9 +201,9 @@ func TestRunnerDiscardsOutputReturnedAfterCancellation(t *testing.T) {
|
||||
if len(output.OutputFiles) != 0 {
|
||||
t.Fatalf("output files = %#v, want none", output.OutputFiles)
|
||||
}
|
||||
for _, warning := range output.Warnings {
|
||||
if warning.ReasonCode == "returned-after-cancel" {
|
||||
t.Fatalf("output warnings include encoder warning after cancellation")
|
||||
for _, group := range output.Diagnostics.Groups {
|
||||
if group.ReasonCode == "returned-after-cancel" {
|
||||
t.Fatalf("output diagnostics include encoder diagnostic after cancellation")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,7 +367,7 @@ func TestRunnerBoundsExtractJobsAndStabilizesReverseCompletion(t *testing.T) {
|
||||
return erasedTypedResult{}, ctx.Err()
|
||||
}
|
||||
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
|
||||
return erasedTypedResult{Value: typedValueForLane(lane, request.Chunk.Index), Diagnostics: []contracts.ProducerDiagnostic{{Disposition: contracts.DiagnosticDispositionObservation, Category: contracts.DiagnosticCategoryNormalization, ReasonCode: "observed", OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: scope, Message: "ordered"}}}}}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -396,14 +396,6 @@ func TestRunnerBoundsExtractJobsAndStabilizesReverseCompletion(t *testing.T) {
|
||||
if got := maximum.Load(); got != 2 {
|
||||
t.Fatalf("maximum concurrent extract jobs = %d, want 2", got)
|
||||
}
|
||||
wantScopes := []string{"lane-0/chunk-0", "lane-0/chunk-1", "lane-0/chunk-2", "lane-1/chunk-0", "lane-1/chunk-1", "lane-1/chunk-2"}
|
||||
gotScopes := make([]string, len(result.output.Warnings))
|
||||
for i := range result.output.Warnings {
|
||||
gotScopes[i] = result.output.Warnings[i].Scope
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ type laneExtractState struct {
|
||||
reuseEligible bool
|
||||
values []erasedExtractArtifact
|
||||
serialized []CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejected []contracts.RejectedOutput
|
||||
incomplete []int
|
||||
@@ -39,7 +38,6 @@ type laneExtractState struct {
|
||||
type finalizedExtractResults struct {
|
||||
accepted []erasedExtractArtifact
|
||||
serialized []CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejected []contracts.RejectedOutput
|
||||
incomplete []int
|
||||
@@ -55,8 +53,8 @@ func loadExtract(loader CheckpointLoader, stepID, laneID, moduleKey string, deps
|
||||
return loader.Extract(laneID, moduleKey, deps)
|
||||
}
|
||||
|
||||
func recordExtract(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
||||
return checkpointExtractSucceeded(recorder, stepID, laneID, moduleKey, deps, outputs, rejected, warnings)
|
||||
func recordExtract(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||
return checkpointExtractSucceeded(recorder, stepID, laneID, moduleKey, deps, outputs, rejected)
|
||||
}
|
||||
|
||||
type extractJob struct {
|
||||
@@ -69,7 +67,6 @@ type extractJobResult struct {
|
||||
chunkIndex int
|
||||
value erasedExtractArtifact
|
||||
serialized CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.DiagnosticGroup
|
||||
rejected *contracts.RejectedOutput
|
||||
validationIncomplete bool
|
||||
@@ -356,7 +353,6 @@ func hydrateRequiredLane(input RunInput, loader CheckpointLoader, doc *source.So
|
||||
return state, err
|
||||
}
|
||||
hydrated := resolution.artifacts[0]
|
||||
local.Warnings = append(local.Warnings, cloneWarnings(checkpoint.Warnings)...)
|
||||
diagnostics, diagnosticErr := promoteCheckpointDiagnostics(hydrated.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return state, fmt.Errorf("promote reused accepted diagnostics: %w", diagnosticErr)
|
||||
@@ -433,7 +429,7 @@ func prepareLaneExtract(input RunInput, loader CheckpointLoader, doc *source.Sou
|
||||
}
|
||||
state.diagnostics = append(state.diagnostics, diagnostics...)
|
||||
}
|
||||
state.warnings, state.rejected = cloneWarnings(cp.Warnings), cloneRejectedOutputs(cp.Rejected)
|
||||
state.rejected = cloneRejectedOutputs(cp.Rejected)
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
@@ -464,15 +460,14 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
return producerAttemptOutput{}, terminal.record(nil, attemptErr)
|
||||
}
|
||||
artifact := erasedExtractArtifact{LaneID: lane.ID, ExtractorKey: lane.Extract.Module, SourceID: doc.ID, ChunkID: chunk.ID, ChunkIndex: chunk.Index, ChunkRef: chunk.Ref, Value: extracted.Value}
|
||||
attemptWarnings := cloneWarnings(extracted.Warnings)
|
||||
serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, artifact.LaneID, artifact.ExtractorKey, artifact.SourceID, artifact.Value)
|
||||
if encodeErr != nil {
|
||||
attemptErr := fmt.Errorf("serialize extract candidate for lane %q chunk %q: %w", lane.ID, chunk.ID, encodeErr)
|
||||
payload := map[string]any{"warnings": debugWarningEnvelopes(attemptWarnings)}
|
||||
payload := map[string]any{}
|
||||
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, Diagnostics: contracts.CloneProducerDiagnostics(extracted.Diagnostics)}, nil
|
||||
return producerAttemptOutput{Value: extractAttemptValue{artifact: artifact, serialized: serializedCandidate, terminal: &terminal}, Candidate: extracted.ModelCandidate, Diagnostics: contracts.CloneProducerDiagnostics(extracted.Diagnostics)}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(extractAttemptValue)
|
||||
if !ok {
|
||||
@@ -481,7 +476,6 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
report, validationErr := r.validateTypedReport(validationCtx, typed.codec, typedValidationTarget{stage: StageExtract, stepID: input.stepID, laneID: lane.ID, moduleKey: lane.Extract.Module, source: doc, sourceID: doc.ID, sourceInput: chunkInputMaterial(sourceInput, chunk), sessionID: sessionID, references: operationReferenceSet(input, lane.ExtractReferences), metadata: input.Metadata, chunk: &chunk, ref: chunk.Ref, value: candidate.artifact.Value, candidate: &candidate.serialized}, state.prepared.extractValidators, candidate.terminal.envelope.Attempt, input.Debug)
|
||||
payload := map[string]any{
|
||||
"output": debugCheckpointArtifact(candidate.serialized),
|
||||
"warnings": debugWarningEnvelopes(append(cloneWarnings(output.Warnings), report.Warnings()...)),
|
||||
"rejection": debugRejectedOutputPtr(typedRejection(report, typedValidationTarget{stage: StageExtract, stepID: input.stepID, laneID: lane.ID, moduleKey: lane.Extract.Module, chunk: &chunk}, candidate.terminal.envelope.Attempt)),
|
||||
}
|
||||
if validationErr != nil {
|
||||
@@ -510,7 +504,6 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
result.validationSummary = &summary
|
||||
result.rejected.Validation = cloneValidationSummaryPtr(result.validationSummary)
|
||||
}
|
||||
result.warnings = cloneWarnings(terminalResult.Warnings)
|
||||
return result
|
||||
}
|
||||
if err == nil {
|
||||
@@ -520,7 +513,7 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
return result
|
||||
}
|
||||
stored, encodeErr := checkpointArtifact(typed.codec, candidate.artifact.LaneID, candidate.artifact.ExtractorKey, candidate.artifact.SourceID, candidate.artifact.Value)
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.serialized), "warnings": debugWarningEnvelopes(terminalResult.Warnings), "rejection": debugRejectedOutputPtr(nil)}
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.serialized), "rejection": debugRejectedOutputPtr(nil)}
|
||||
if encodeErr != nil {
|
||||
attemptErr := fmt.Errorf("serialize accepted extract output for lane %q chunk %q: %w", lane.ID, chunk.ID, encodeErr)
|
||||
result.err = candidate.terminal.record(payload, attemptErr)
|
||||
@@ -533,7 +526,6 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
||||
return result
|
||||
}
|
||||
result.value, result.serialized = candidate.artifact, stored
|
||||
result.warnings = cloneWarnings(terminalResult.Warnings)
|
||||
result.validationIncomplete = terminalResult.ValidationIncomplete
|
||||
result.validationSummary = &summary
|
||||
}
|
||||
@@ -554,13 +546,11 @@ 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)
|
||||
@@ -574,7 +564,7 @@ func finalizeLaneExtract(checkpoints CheckpointRecorder, stepID string, state *l
|
||||
state.reuseEligible = false
|
||||
}
|
||||
if !state.decision.Reused && state.reuseEligible {
|
||||
if err := recordExtract(checkpoints, stepID, lane.ID, lane.Extract.Module, state.deps, state.serialized, state.rejected, state.warnings); err != nil {
|
||||
if err := recordExtract(checkpoints, stepID, lane.ID, lane.Extract.Module, state.deps, state.serialized, state.rejected); err != nil {
|
||||
return fmt.Errorf("write extract checkpoint for lane %q: %w", lane.ID, err)
|
||||
}
|
||||
}
|
||||
@@ -587,7 +577,6 @@ func (r *Runner) continueLane(ctx context.Context, input RunInput, checkpoints C
|
||||
results := finalizedExtractResults{
|
||||
accepted: state.values,
|
||||
serialized: state.serialized,
|
||||
warnings: state.warnings,
|
||||
diagnostics: state.diagnostics,
|
||||
rejected: state.rejected,
|
||||
incomplete: state.incomplete,
|
||||
@@ -595,14 +584,13 @@ func (r *Runner) continueLane(ctx context.Context, input RunInput, checkpoints C
|
||||
decision: state.decision,
|
||||
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 {
|
||||
return local, &laneRunError{stage: StageExtract, err: err}
|
||||
}
|
||||
if err := writeDebugTimed(input.Debug, path.Join("extract", fileio.EncodePathComponent(lane.ID), "output.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, "outputs": debugCheckpointArtifacts(results.serialized), "rejected": debugRejectedOutputEnvelopes(results.rejected), "warnings": debugWarningEnvelopes(results.warnings), "validation_incomplete_chunks": append([]int(nil), results.incomplete...)}}); err != nil {
|
||||
if err := writeDebugTimed(input.Debug, path.Join("extract", fileio.EncodePathComponent(lane.ID), "output.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, "outputs": debugCheckpointArtifacts(results.serialized), "rejected": debugRejectedOutputEnvelopes(results.rejected), "validation_incomplete_chunks": append([]int(nil), results.incomplete...)}}); err != nil {
|
||||
return local, &laneRunError{stage: StageExtract, err: err}
|
||||
}
|
||||
if len(results.accepted) == 0 {
|
||||
@@ -667,7 +655,6 @@ 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)...)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -85,15 +84,6 @@ func TestRunnerCorrectsExtractCandidatesIndependentlyPerChunk(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func containsWarnings(have, want []contracts.Warning) bool {
|
||||
for index := 0; index+len(want) <= len(have); index++ {
|
||||
if reflect.DeepEqual(have[index:index+len(want)], want) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsValidationSummary(summaries []artifacts.ValidationSummary, status string) bool {
|
||||
for _, summary := range summaries {
|
||||
if summary.Status == status {
|
||||
@@ -126,8 +116,8 @@ func TestRunnerContinuesValidationIncompleteExtractWithoutCheckpoint(t *testing.
|
||||
if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
|
||||
t.Fatalf("manifest validation = %#v, want incomplete extract provenance", output.Manifest)
|
||||
}
|
||||
if got, want := output.Warnings, []contracts.Warning{{Scope: "typed/check", ReasonCode: "validator_execution_incomplete", Message: "Validator execution did not complete within its configured budget."}}; !containsWarnings(got, want) {
|
||||
t.Fatalf("warnings = %#v, want %#v", got, want)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
|
||||
t.Fatalf("diagnostics = %#v, want validation-incomplete group", output.Diagnostics)
|
||||
}
|
||||
if len(recorder.checkpoint.Outputs) != 0 || len(recorder.checkpoint.Rejected) != 0 {
|
||||
t.Fatalf("extract checkpoint = %#v, want no persisted incomplete output", recorder.checkpoint)
|
||||
|
||||
@@ -35,11 +35,10 @@ func (v *mutatingMetadataValidator) Validate(_ context.Context, request contract
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
|
||||
func (r *extractCaptureRecorder) ExtractSucceeded(_ string, _ string, _ []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
||||
func (r *extractCaptureRecorder) ExtractSucceeded(_ string, _ string, _ []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||
r.checkpoint = ExtractCheckpoint{
|
||||
Outputs: cloneCheckpointArtifacts(outputs),
|
||||
Rejected: cloneRejectedOutputs(rejected),
|
||||
Warnings: cloneWarnings(warnings),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -56,7 +55,6 @@ func (l *extractResultLoader) Extract(string, string, []CheckpointFingerprint) (
|
||||
return ExtractCheckpoint{
|
||||
Outputs: cloneCheckpointArtifacts(l.checkpoint.Outputs),
|
||||
Rejected: cloneRejectedOutputs(l.checkpoint.Rejected),
|
||||
Warnings: cloneWarnings(l.checkpoint.Warnings),
|
||||
}, l.decision
|
||||
}
|
||||
|
||||
@@ -156,8 +154,7 @@ func TestRunnerContinuesFromFreshAndReusedExtractResults(t *testing.T) {
|
||||
installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
||||
extractCalls++
|
||||
return erasedTypedResult{
|
||||
Value: typedValueForLane(0, request.Chunk.Index),
|
||||
Warnings: []contracts.Warning{{Scope: "extract", ReasonCode: "observed", Message: "accepted extract"}},
|
||||
Value: typedValueForLane(0, request.Chunk.Index),
|
||||
Diagnostics: []contracts.ProducerDiagnostic{{
|
||||
Disposition: contracts.DiagnosticDispositionObservation,
|
||||
Category: contracts.DiagnosticCategoryNormalization,
|
||||
@@ -213,15 +210,12 @@ func TestRunnerContinuesFromFreshAndReusedExtractResults(t *testing.T) {
|
||||
if !reflect.DeepEqual(reused.NormalizeOutputs, fresh.NormalizeOutputs) {
|
||||
t.Fatalf("reused normalize outputs = %#v, want fresh outputs %#v", reused.NormalizeOutputs, fresh.NormalizeOutputs)
|
||||
}
|
||||
if !reflect.DeepEqual(reused.Warnings, fresh.Warnings) {
|
||||
t.Fatalf("reused warnings = %#v, want fresh warnings %#v", reused.Warnings, fresh.Warnings)
|
||||
}
|
||||
if !reflect.DeepEqual(reused.Diagnostics, fresh.Diagnostics) {
|
||||
t.Fatalf("reused diagnostics = %#v, want fresh diagnostics %#v", reused.Diagnostics, fresh.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerPromotesOnlyAcceptedExtractRetryWarnings(t *testing.T) {
|
||||
func TestRunnerPromotesOnlyAcceptedExtractRetryDiagnostics(t *testing.T) {
|
||||
prepared := preparedAttemptDebugPipeline(t)
|
||||
prepared.Steps[0].lanes[0].resolved.Extract.Retries = 1
|
||||
attempts := 0
|
||||
@@ -233,7 +227,7 @@ func TestRunnerPromotesOnlyAcceptedExtractRetryWarnings(t *testing.T) {
|
||||
}
|
||||
return erasedTypedResult{
|
||||
Value: typedValueForLane(0, request.Chunk.Index),
|
||||
Warnings: []contracts.Warning{{Scope: scope, ReasonCode: "observed", Message: scope}},
|
||||
Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(scope, scope)},
|
||||
ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%s"]}`, scope)),
|
||||
}, nil
|
||||
})
|
||||
@@ -250,8 +244,8 @@ func TestRunnerPromotesOnlyAcceptedExtractRetryWarnings(t *testing.T) {
|
||||
if attempts != 2 {
|
||||
t.Fatalf("extract attempts = %d, want 2", attempts)
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].Scope != "accepted" {
|
||||
t.Fatalf("promoted warnings = %#v, want accepted attempt only", output.Warnings)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].Samples[0].Scope != "accepted" {
|
||||
t.Fatalf("promoted diagnostics = %#v, want accepted attempt only", output.Diagnostics)
|
||||
}
|
||||
assertAttemptEnvelopeSequence(t, debug, "extract/notes/chunk-000001", 1, 2)
|
||||
first := debug.envelope(t, "extract/notes/chunk-000001/attempt-01.json")
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
||||
validator *preparedValidator
|
||||
wantCalls int
|
||||
wantItem string
|
||||
wantWarnings []string
|
||||
wantMessages []string
|
||||
wantRejected int
|
||||
wantDebug []string
|
||||
wantCheckpoint int
|
||||
@@ -30,7 +30,7 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
||||
},
|
||||
wantCalls: 1,
|
||||
wantItem: "fallback",
|
||||
wantWarnings: []string{"ordinary", "fallback-warning"},
|
||||
wantMessages: []string{"ordinary", "fallback-warning"},
|
||||
wantDebug: []string{`"another_attempt":false`, `"fallback_accepted":true`},
|
||||
wantCheckpoint: 1,
|
||||
},
|
||||
@@ -41,11 +41,11 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
||||
if attempt == 1 {
|
||||
return retryableNormalizeResult("discarded", "discarded-ordinary", "discarded-fallback")
|
||||
}
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"accepted"}}, Warnings: []contracts.Warning{{Scope: "accepted", ReasonCode: "ordinary", Message: "accepted-warning"}}}
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"accepted"}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("accepted", "accepted-warning")}}
|
||||
},
|
||||
wantCalls: 2,
|
||||
wantItem: "accepted",
|
||||
wantWarnings: []string{"accepted-warning"},
|
||||
wantMessages: []string{"accepted-warning"},
|
||||
wantDebug: []string{`"another_attempt":true`, `"fallback_accepted":false`},
|
||||
wantCheckpoint: 1,
|
||||
},
|
||||
@@ -57,7 +57,7 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
||||
},
|
||||
wantCalls: 2,
|
||||
wantItem: "fallback-2",
|
||||
wantWarnings: []string{"ordinary-2", "fallback-warning-2"},
|
||||
wantMessages: []string{"ordinary-2", "fallback-warning-2"},
|
||||
wantDebug: []string{`"another_attempt":false`, `"fallback_accepted":true`},
|
||||
wantCheckpoint: 1,
|
||||
},
|
||||
@@ -140,12 +140,14 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
||||
if got := firstNote(normalized); got != tc.wantItem {
|
||||
t.Fatalf("normalized item = %q, want %q", got, tc.wantItem)
|
||||
}
|
||||
gotWarnings := make([]string, len(output.Warnings))
|
||||
for index, warning := range output.Warnings {
|
||||
gotWarnings[index] = warning.Message
|
||||
var gotMessages []string
|
||||
for _, group := range output.Diagnostics.Groups {
|
||||
for _, sample := range group.Samples {
|
||||
gotMessages = append(gotMessages, sample.Message)
|
||||
}
|
||||
}
|
||||
if strings.Join(gotWarnings, "|") != strings.Join(tc.wantWarnings, "|") {
|
||||
t.Fatalf("durable warnings = %#v, want %#v", gotWarnings, tc.wantWarnings)
|
||||
if strings.Join(gotMessages, "|") != strings.Join(tc.wantMessages, "|") {
|
||||
t.Fatalf("durable diagnostics = %#v, want %#v", gotMessages, tc.wantMessages)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -262,12 +264,12 @@ func TestRunnerValidatesNormalizeRetryDiagnostics(t *testing.T) {
|
||||
|
||||
func retryableNormalizeResult(item, ordinary, fallback string) erasedTypedResult {
|
||||
return erasedTypedResult{
|
||||
Value: codecNotes{Items: []string{item}},
|
||||
Warnings: []contracts.Warning{{Scope: "attempt", ReasonCode: "ordinary", Message: ordinary}},
|
||||
Value: codecNotes{Items: []string{item}},
|
||||
Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("attempt", ordinary)},
|
||||
Retry: &contracts.NormalizeRetry{
|
||||
ReasonCode: "retryable_normalization",
|
||||
Message: "safe fallback is available",
|
||||
FallbackWarnings: []contracts.Warning{{Scope: "fallback", ReasonCode: "fallback", Message: fallback}},
|
||||
ReasonCode: "retryable_normalization",
|
||||
Message: "safe fallback is available",
|
||||
FallbackDiagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("fallback", fallback)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,19 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type warningChunker struct {
|
||||
type diagnosticChunker struct {
|
||||
key string
|
||||
plan source.ChunkPlan
|
||||
calls int
|
||||
}
|
||||
|
||||
func (c *warningChunker) Key() string { return c.key }
|
||||
func (*warningChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
||||
func (c *warningChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||
func (c *diagnosticChunker) Key() string { return c.key }
|
||||
func (*diagnosticChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
||||
func (c *diagnosticChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||
c.calls++
|
||||
return contracts.ChunkPlanResult{
|
||||
Plan: source.CloneChunkPlan(c.plan),
|
||||
Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", c.calls), ReasonCode: "operation", Message: "operation warning"}},
|
||||
Plan: source.CloneChunkPlan(c.plan),
|
||||
Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("operation-%d", c.calls), "operation diagnostic")},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ func (v chunkValidationFunc) Validate(ctx context.Context, request contracts.Chu
|
||||
return v.validate(ctx, request)
|
||||
}
|
||||
|
||||
func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
|
||||
func TestRunnerPromotesOnlyTerminalRejectionDiagnostics(t *testing.T) {
|
||||
for _, target := range []ModuleStage{StageChunk, StageExtract, StageMerge, StageNormalize} {
|
||||
t.Run(string(target), func(t *testing.T) {
|
||||
prepared := preparedAttemptDebugPipeline(t)
|
||||
lane := &prepared.Steps[0].lanes[0]
|
||||
attempts := 0
|
||||
first := func() contracts.ValidationResult {
|
||||
return contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("validator-%d", attempts), ReasonCode: "validator", Message: "validator warning"}}}
|
||||
return contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("validator-%d", attempts), "validator diagnostic")}}
|
||||
}
|
||||
reject := func() contracts.ValidationResult {
|
||||
return contracts.ValidationResult{Approved: false, ReasonCode: "rejected", Message: "rejected", CorrectionGuidance: "return an acceptable candidate"}
|
||||
@@ -59,7 +59,7 @@ func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
|
||||
case StageChunk:
|
||||
prepared.resolved.ChunkValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||
chunker := prepared.chunker.(*typedTestChunker)
|
||||
prepared.chunker = &warningChunker{key: prepared.resolved.Chunk.Module, plan: source.CloneChunkPlan(chunker.plan)}
|
||||
prepared.chunker = &diagnosticChunker{key: prepared.resolved.Chunk.Module, plan: source.CloneChunkPlan(chunker.plan)}
|
||||
prepared.resolved.Chunk.Retries = 1
|
||||
prepared.chunkValidators.validators = []preparedValidator{
|
||||
{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetChunk}, chunk: chunkValidationFunc{name: "warning-approval", validate: func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
|
||||
@@ -69,34 +69,34 @@ func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
|
||||
return reject(), nil
|
||||
}}},
|
||||
}
|
||||
chunkerWithWarnings := prepared.chunker.(*warningChunker)
|
||||
chunkerWithDiagnostics := prepared.chunker.(*diagnosticChunker)
|
||||
first = func() contracts.ValidationResult {
|
||||
return contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("validator-%d", chunkerWithWarnings.calls), ReasonCode: "validator", Message: "validator warning"}}}
|
||||
return contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("validator-%d", chunkerWithDiagnostics.calls), "validator diagnostic")}}
|
||||
}
|
||||
case StageExtract:
|
||||
lane.resolved.Extract.Retries = 1
|
||||
lane.resolved.ExtractValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||
installExtractOperation(prepared, 0, func(context.Context, contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
||||
attempts++
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"extract"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%d"]}`, attempts))}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"extract"}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("operation-%d", attempts), "operation diagnostic")}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%d"]}`, attempts))}, nil
|
||||
})
|
||||
lane.extractValidators.validators = rejectionWarningTypedValidators(first, reject)
|
||||
lane.extractValidators.validators = rejectionDiagnosticTypedValidators(first, reject)
|
||||
case StageMerge:
|
||||
lane.resolved.Merge.Retries = 1
|
||||
lane.resolved.MergeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||
lane.typed.merge = func(context.Context, any, contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
|
||||
attempts++
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"merge"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%d"]}`, attempts))}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"merge"}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("operation-%d", attempts), "operation diagnostic")}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%d"]}`, attempts))}, nil
|
||||
}
|
||||
lane.mergeValidators.validators = rejectionWarningTypedValidators(first, reject)
|
||||
lane.mergeValidators.validators = rejectionDiagnosticTypedValidators(first, reject)
|
||||
case StageNormalize:
|
||||
lane.resolved.Normalize.Retries = 1
|
||||
lane.resolved.NormalizeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||
lane.typed.normalize = func(context.Context, any, contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
|
||||
attempts++
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"normalize"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%d"]}`, attempts))}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{"normalize"}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("operation-%d", attempts), "operation diagnostic")}, ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%d"]}`, attempts))}, nil
|
||||
}
|
||||
lane.normalizeValidators.validators = rejectionWarningTypedValidators(first, reject)
|
||||
lane.normalizeValidators.validators = rejectionDiagnosticTypedValidators(first, reject)
|
||||
}
|
||||
|
||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoints: recorder, Debug: debug})
|
||||
@@ -109,29 +109,26 @@ func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
|
||||
wantScopes = []string{"operation-1", "validator-1"}
|
||||
wantAttempts = 1
|
||||
}
|
||||
if got := rejectionWarningScopes(output.Warnings); !reflect.DeepEqual(got, wantScopes) {
|
||||
t.Fatalf("published warning scopes = %#v, want %#v", got, wantScopes)
|
||||
if got := rejectionDiagnosticScopes(output.Diagnostics.Groups); !reflect.DeepEqual(got, wantScopes) {
|
||||
t.Fatalf("published diagnostic scopes = %#v, want %#v", got, wantScopes)
|
||||
}
|
||||
if len(output.Rejected) != 1 || output.Rejected[0].AttemptCount != wantAttempts {
|
||||
t.Fatalf("rejections = %#v, want terminal rejection after %d attempt(s)", output.Rejected, wantAttempts)
|
||||
}
|
||||
if target == StageExtract && !reflect.DeepEqual(rejectionWarningScopes(recorder.checkpoint.Warnings), wantScopes) {
|
||||
t.Fatalf("extract checkpoint warnings = %#v, want %#v", recorder.checkpoint.Warnings, wantScopes)
|
||||
}
|
||||
attemptPath := fmt.Sprintf("%s/notes/attempt-01.json", target)
|
||||
if target == StageChunk {
|
||||
attemptPath = "chunk/attempt-01.json"
|
||||
} else if target == StageExtract {
|
||||
attemptPath = "extract/notes/chunk-000001/attempt-01.json"
|
||||
}
|
||||
if !strings.Contains(string(debug.json[attemptPath]), "operation-1") || !strings.Contains(string(debug.json[attemptPath]), "validator-1") {
|
||||
t.Fatalf("first attempt debug = %s, want discarded warnings", debug.json[attemptPath])
|
||||
if !strings.Contains(string(debug.json[attemptPath]), "rejection") {
|
||||
t.Fatalf("first attempt debug = %s, want rejection", debug.json[attemptPath])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func rejectionWarningTypedValidators(first func() contracts.ValidationResult, reject func() contracts.ValidationResult) []preparedValidator {
|
||||
func rejectionDiagnosticTypedValidators(first func() contracts.ValidationResult, reject func() contracts.ValidationResult) []preparedValidator {
|
||||
return []preparedValidator{
|
||||
{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
|
||||
return first(), nil
|
||||
@@ -142,10 +139,10 @@ func rejectionWarningTypedValidators(first func() contracts.ValidationResult, re
|
||||
}
|
||||
}
|
||||
|
||||
func rejectionWarningScopes(warnings []contracts.Warning) []string {
|
||||
scopes := make([]string, len(warnings))
|
||||
for index := range warnings {
|
||||
scopes[index] = warnings[index].Scope
|
||||
func rejectionDiagnosticScopes(groups []contracts.DiagnosticGroup) []string {
|
||||
scopes := make([]string, len(groups))
|
||||
for index := range groups {
|
||||
scopes[index] = groups[index].Samples[0].Scope
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (s *reuseLineageCheckpointSpy) ExtractRunning(laneID, _ string, _ []Checkpo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *reuseLineageCheckpointSpy) ExtractSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ []CheckpointArtifact, _ []contracts.RejectedOutput, _ []contracts.Warning) error {
|
||||
func (s *reuseLineageCheckpointSpy) ExtractSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ []CheckpointArtifact, _ []contracts.RejectedOutput) error {
|
||||
s.write("extract", "succeeded", laneID)
|
||||
return nil
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (s *reuseLineageCheckpointSpy) MergeRunning(laneID, _ string, _ []Checkpoin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *reuseLineageCheckpointSpy) MergeSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ CheckpointArtifact, _ []contracts.Warning) error {
|
||||
func (s *reuseLineageCheckpointSpy) MergeSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ CheckpointArtifact) error {
|
||||
s.write("merge", "succeeded", laneID)
|
||||
return nil
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func (s *reuseLineageCheckpointSpy) NormalizeRunning(laneID, _ string, _ []Check
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *reuseLineageCheckpointSpy) NormalizeSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ CheckpointArtifact, _ []contracts.Warning) error {
|
||||
func (s *reuseLineageCheckpointSpy) NormalizeSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ CheckpointArtifact) error {
|
||||
s.write("normalize", "succeeded", laneID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func TestRunnerContinuesNormalizeAfterValidatorFailure(t *testing.T) {
|
||||
if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
|
||||
t.Fatalf("manifest validation = %#v, want incomplete normalization provenance", output.Manifest)
|
||||
}
|
||||
if got, want := output.Warnings, []contracts.Warning{{Scope: "unavailable", ReasonCode: "validator_execution_incomplete", Message: "Validator execution did not complete within its configured budget."}}; !containsWarnings(got, want) {
|
||||
t.Fatalf("warnings = %#v, want %#v", got, want)
|
||||
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
|
||||
t.Fatalf("diagnostics = %#v, want validation-incomplete group", output.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
type terminalChunker struct {
|
||||
key string
|
||||
plan source.ChunkPlan
|
||||
warnings []contracts.Warning
|
||||
diagnostics []contracts.ProducerDiagnostic
|
||||
err error
|
||||
calls *int
|
||||
@@ -30,7 +29,7 @@ func (c terminalChunker) Plan(context.Context, contracts.ChunkRequest) (contract
|
||||
if c.calls != nil {
|
||||
(*c.calls)++
|
||||
}
|
||||
return contracts.ChunkPlanResult{Plan: source.CloneChunkPlan(c.plan), Warnings: cloneWarnings(c.warnings), Diagnostics: contracts.CloneProducerDiagnostics(c.diagnostics)}, c.err
|
||||
return contracts.ChunkPlanResult{Plan: source.CloneChunkPlan(c.plan), Diagnostics: contracts.CloneProducerDiagnostics(c.diagnostics)}, c.err
|
||||
}
|
||||
|
||||
type terminalChunkValidator struct {
|
||||
@@ -168,7 +167,7 @@ func TestRunnerRecordsChunkTerminalOutcomes(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
prepared, plan := preparedTerminalDebugPipeline(t)
|
||||
prepared.chunker = terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan, warnings: []contracts.Warning{{Scope: "chunk", ReasonCode: "observed", Message: "chunk warning"}}, err: tc.moduleError}
|
||||
prepared.chunker = terminalChunker{key: prepared.resolved.Chunk.Module, plan: plan, diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("chunk", "chunk diagnostic")}, err: tc.moduleError}
|
||||
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding("terminal/chunk-validator"), Target: ValidatorTargetChunk}, chunk: tc.validator}}
|
||||
debug := newCapturedDebugRecorder()
|
||||
|
||||
@@ -337,7 +336,7 @@ func TestRunnerRecordsExtractTerminalOutcomes(t *testing.T) {
|
||||
if tc.moduleError != nil {
|
||||
return erasedTypedResult{}, tc.moduleError
|
||||
}
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{value}}, Warnings: []contracts.Warning{{Scope: "extract", ReasonCode: "observed", Message: "extract warning"}}}, nil
|
||||
return erasedTypedResult{Value: codecNotes{Items: []string{value}}, Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("extract", "extract diagnostic")}}, nil
|
||||
})
|
||||
prepared.Steps[0].lanes[0].extractValidators.validators = []preparedValidator{{
|
||||
resolved: ResolvedValidator{Binding: Binding("terminal/extract-validator"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"},
|
||||
@@ -369,9 +368,6 @@ func TestRunnerRecordsExtractTerminalOutcomes(t *testing.T) {
|
||||
t.Fatalf("extract rejection = envelope %#v, outputs %#v", envelope, output.Rejected)
|
||||
}
|
||||
}
|
||||
if tc.moduleError == nil && !strings.Contains(string(debug.json[attemptPath]), "extract warning") {
|
||||
t.Fatalf("attempt envelope = %s, want attempt warning", debug.json[attemptPath])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ func loadNormalize(loader CheckpointLoader, stepID, laneID, moduleKey string, de
|
||||
}
|
||||
return loader.Normalize(laneID, moduleKey, deps)
|
||||
}
|
||||
func recordMerge(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
return checkpointMergeSucceeded(recorder, stepID, laneID, moduleKey, deps, output, warnings)
|
||||
func recordMerge(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
return checkpointMergeSucceeded(recorder, stepID, laneID, moduleKey, deps, output)
|
||||
}
|
||||
func recordNormalize(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
return checkpointNormalizeSucceeded(recorder, stepID, laneID, moduleKey, deps, output, warnings)
|
||||
func recordNormalize(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
return checkpointNormalizeSucceeded(recorder, stepID, laneID, moduleKey, deps, output)
|
||||
}
|
||||
func cloneCheckpointArtifact(output CheckpointArtifact) CheckpointArtifact {
|
||||
output.Artifact = contracts.CloneSerializedArtifact(output.Artifact)
|
||||
@@ -265,12 +265,9 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
}
|
||||
var merged erasedMergeArtifact
|
||||
var serializedMerge CheckpointArtifact
|
||||
var mergeWarnings []contracts.Warning
|
||||
if mergeDecision.Reused {
|
||||
merged = erasedMergeArtifact{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: mergeResolution.values[0]}
|
||||
serializedMerge = mergeResolution.artifacts[0]
|
||||
mergeWarnings = cloneWarnings(mergeCP.Warnings)
|
||||
output.Warnings = append(output.Warnings, mergeWarnings...)
|
||||
diagnostics, diagnosticErr := promoteCheckpointDiagnostics(serializedMerge.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageMerge, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return stageResult, fmt.Errorf("promote reused merge diagnostics: %w", diagnosticErr)
|
||||
@@ -296,12 +293,11 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
return producerAttemptOutput{}, terminal.record(nil, fmt.Errorf("merge lane %q with merger %q: %w", lane.ID, lane.Merge.Module, callErr))
|
||||
}
|
||||
candidate := erasedMergeArtifact{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: result.Value}
|
||||
warnings := cloneWarnings(result.Warnings)
|
||||
serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, candidate.LaneID, candidate.MergerKey, candidate.SourceID, candidate.Value)
|
||||
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{}, terminal.record(nil, 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, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics)}, nil
|
||||
return producerAttemptOutput{Value: mergeAttemptValue{artifact: candidate, candidate: serializedCandidate, terminal: &terminal}, Candidate: result.ModelCandidate, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics)}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(mergeAttemptValue)
|
||||
if !ok {
|
||||
@@ -309,7 +305,7 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
}
|
||||
target := typedValidationTarget{stage: StageMerge, stepID: input.stepID, laneID: lane.ID, moduleKey: lane.Merge.Module, source: doc, sourceID: doc.ID, sourceInput: sourceInput.Clone(), sessionID: sessionID, references: mergeReferences, metadata: input.Metadata, value: candidate.artifact.Value, candidate: &candidate.candidate}
|
||||
report, validationErr := r.validateTypedReport(validationCtx, typed.codec, target, prepared.mergeValidators, candidate.terminal.envelope.Attempt, input.Debug)
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "warnings": debugWarningEnvelopes(append(cloneWarnings(output.Warnings), report.Warnings()...)), "rejection": debugRejectedOutputPtr(typedRejection(report, target, candidate.terminal.envelope.Attempt))}
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "rejection": debugRejectedOutputPtr(typedRejection(report, target, candidate.terminal.envelope.Attempt))}
|
||||
if validationErr != nil {
|
||||
return report, candidate.terminal.record(payload, validationErr)
|
||||
}
|
||||
@@ -340,7 +336,6 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
rejected.Stage, rejected.StepID, rejected.LaneID, rejected.ModuleKey = string(StageMerge), input.stepID, lane.ID, lane.Merge.Module
|
||||
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 {
|
||||
@@ -356,7 +351,7 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
return stageResult, fmt.Errorf("merge attempt terminal has incompatible value")
|
||||
}
|
||||
stored, encodeErr := checkpointArtifact(typed.codec, candidate.artifact.LaneID, candidate.artifact.MergerKey, candidate.artifact.SourceID, candidate.artifact.Value)
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "warnings": debugWarningEnvelopes(terminalResult.Warnings), "rejection": debugRejectedOutputPtr(nil)}
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "rejection": debugRejectedOutputPtr(nil)}
|
||||
if encodeErr != nil {
|
||||
attemptErr := fmt.Errorf("serialize accepted merge output for lane %q: %w", lane.ID, encodeErr)
|
||||
if stageResult.reuseEligible {
|
||||
@@ -372,21 +367,19 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
return stageResult, debugErr
|
||||
}
|
||||
merged, serializedMerge = candidate.artifact, stored
|
||||
mergeWarnings = cloneWarnings(terminalResult.Warnings)
|
||||
output.ValidationSummaries = append(output.ValidationSummaries, terminalSummary)
|
||||
stageResult.validationIncomplete = terminalResult.ValidationIncomplete
|
||||
if stageResult.validationIncomplete {
|
||||
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 {
|
||||
if err := recordMerge(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps, serializedMerge); err != nil {
|
||||
return stageResult, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := writeDebugTimed(input.Debug, path.Join("merge", fileio.EncodePathComponent(lane.ID), "output.json"), debugTimedEnvelope{Stage: string(StageMerge), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": mergeDecision.Reused, "accepted": true, "output": debugCheckpointArtifact(serializedMerge), "warnings": debugWarningEnvelopes(mergeWarnings)}}); err != nil {
|
||||
if err := writeDebugTimed(input.Debug, path.Join("merge", fileio.EncodePathComponent(lane.ID), "output.json"), debugTimedEnvelope{Stage: string(StageMerge), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": mergeDecision.Reused, "accepted": true, "output": debugCheckpointArtifact(serializedMerge)}}); err != nil {
|
||||
return stageResult, err
|
||||
}
|
||||
stageResult.artifact = merged
|
||||
@@ -396,7 +389,6 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
||||
|
||||
type normalizeStageResult struct {
|
||||
serialized CheckpointArtifact
|
||||
warnings []contracts.Warning
|
||||
accepted bool
|
||||
reuseEligible bool
|
||||
}
|
||||
@@ -422,11 +414,8 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
return stageResult, err
|
||||
}
|
||||
var serializedNormalize CheckpointArtifact
|
||||
var normalizeWarnings []contracts.Warning
|
||||
if normalizeDecision.Reused {
|
||||
serializedNormalize = normalizeResolution.artifacts[0]
|
||||
normalizeWarnings = cloneWarnings(normalizeCP.Warnings)
|
||||
output.Warnings = append(output.Warnings, normalizeWarnings...)
|
||||
diagnostics, diagnosticErr := promoteCheckpointDiagnostics(serializedNormalize.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module}, nil)
|
||||
if diagnosticErr != nil {
|
||||
return stageResult, fmt.Errorf("promote reused normalize diagnostics: %w", diagnosticErr)
|
||||
@@ -451,28 +440,27 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
if callErr != nil {
|
||||
return producerAttemptOutput{}, terminal.record(nil, fmt.Errorf("normalize lane %q with normalizer %q: %w", lane.ID, lane.Normalize.Module, callErr))
|
||||
}
|
||||
warnings := cloneWarnings(result.Warnings)
|
||||
serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, lane.ID, lane.Normalize.Module, doc.ID, result.Value)
|
||||
if encodeErr != nil {
|
||||
return producerAttemptOutput{}, terminal.record(map[string]any{"warnings": debugWarningEnvelopes(warnings)}, fmt.Errorf("serialize normalize candidate for lane %q: %w", lane.ID, encodeErr))
|
||||
return producerAttemptOutput{}, terminal.record(nil, fmt.Errorf("serialize normalize candidate for lane %q: %w", lane.ID, encodeErr))
|
||||
}
|
||||
attemptValue := normalizeAttemptValue{value: result.Value, candidate: serializedCandidate, terminal: &terminal}
|
||||
var directive *producerRetryDirective
|
||||
if result.Retry != nil {
|
||||
if err := validateNormalizeRetry(result.Retry); err != nil {
|
||||
return producerAttemptOutput{}, terminal.record(map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(warnings)}, fmt.Errorf("normalize lane %q returned invalid retry directive: %w", lane.ID, err))
|
||||
return producerAttemptOutput{}, terminal.record(map[string]any{"output": debugCheckpointArtifact(serializedCandidate)}, fmt.Errorf("normalize lane %q returned invalid retry directive: %w", lane.ID, err))
|
||||
}
|
||||
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), FallbackDiagnostics: contracts.CloneProducerDiagnostics(result.Retry.FallbackDiagnostics)}
|
||||
directive = &producerRetryDirective{FallbackDiagnostics: contracts.CloneProducerDiagnostics(result.Retry.FallbackDiagnostics)}
|
||||
if anotherAttempt {
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(warnings), "retry": attemptValue.retry}
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "retry": attemptValue.retry}
|
||||
if debugErr := terminal.record(payload, nil); debugErr != nil {
|
||||
return producerAttemptOutput{}, debugErr
|
||||
}
|
||||
}
|
||||
}
|
||||
return producerAttemptOutput{Value: attemptValue, Candidate: result.ModelCandidate, Warnings: warnings, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), Retry: directive}, nil
|
||||
return producerAttemptOutput{Value: attemptValue, Candidate: result.ModelCandidate, Diagnostics: contracts.CloneProducerDiagnostics(result.Diagnostics), Retry: directive}, nil
|
||||
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||
candidate, ok := output.Value.(normalizeAttemptValue)
|
||||
if !ok {
|
||||
@@ -480,7 +468,7 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
}
|
||||
target := typedValidationTarget{stage: StageNormalize, stepID: input.stepID, laneID: lane.ID, moduleKey: lane.Normalize.Module, source: doc, sourceID: doc.ID, sourceInput: sourceInput.Clone(), sessionID: sessionID, references: normalizeReferences, metadata: input.Metadata, value: candidate.value, candidate: &candidate.candidate}
|
||||
report, validationErr := r.validateTypedReport(validationCtx, typed.codec, target, prepared.normalizeValidators, candidate.terminal.envelope.Attempt, input.Debug)
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "warnings": debugWarningEnvelopes(append(cloneWarnings(output.Warnings), report.Warnings()...)), "rejection": debugRejectedOutputPtr(typedRejection(report, target, candidate.terminal.envelope.Attempt))}
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "rejection": debugRejectedOutputPtr(typedRejection(report, target, candidate.terminal.envelope.Attempt))}
|
||||
if candidate.retry != nil {
|
||||
payload["retry"] = candidate.retry
|
||||
}
|
||||
@@ -514,7 +502,6 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
rejected.Stage, rejected.StepID, rejected.LaneID, rejected.ModuleKey = string(StageNormalize), input.stepID, lane.ID, lane.Normalize.Module
|
||||
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 {
|
||||
@@ -529,7 +516,7 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
return stageResult, fmt.Errorf("normalize attempt terminal has incompatible value")
|
||||
}
|
||||
stored, encodeErr := checkpointArtifact(typed.codec, lane.ID, lane.Normalize.Module, doc.ID, candidate.value)
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "warnings": debugWarningEnvelopes(terminalResult.Warnings), "rejection": debugRejectedOutputPtr(nil)}
|
||||
payload := map[string]any{"output": debugCheckpointArtifact(candidate.candidate), "rejection": debugRejectedOutputPtr(nil)}
|
||||
if candidate.retry != nil {
|
||||
payload["retry"] = candidate.retry
|
||||
}
|
||||
@@ -548,24 +535,21 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
||||
return stageResult, debugErr
|
||||
}
|
||||
serializedNormalize = stored
|
||||
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
|
||||
}
|
||||
if stageResult.reuseEligible {
|
||||
if err := recordNormalize(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps, serializedNormalize, normalizeWarnings); err != nil {
|
||||
if err := recordNormalize(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps, serializedNormalize); err != nil {
|
||||
return stageResult, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := writeDebugTimed(input.Debug, path.Join("normalize", fileio.EncodePathComponent(lane.ID), "output.json"), debugTimedEnvelope{Stage: string(StageNormalize), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": normalizeDecision.Reused, "accepted": true, "output": debugCheckpointArtifact(serializedNormalize), "warnings": debugWarningEnvelopes(normalizeWarnings)}}); err != nil {
|
||||
if err := writeDebugTimed(input.Debug, path.Join("normalize", fileio.EncodePathComponent(lane.ID), "output.json"), debugTimedEnvelope{Stage: string(StageNormalize), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": normalizeDecision.Reused, "accepted": true, "output": debugCheckpointArtifact(serializedNormalize)}}); err != nil {
|
||||
return stageResult, err
|
||||
}
|
||||
stageResult.serialized = serializedNormalize
|
||||
stageResult.warnings = normalizeWarnings
|
||||
stageResult.accepted = true
|
||||
return stageResult, nil
|
||||
}
|
||||
|
||||
@@ -134,8 +134,8 @@ func (r *lockedCheckpointRecorder) SourceFailed(key string, err error) error {
|
||||
func (r *lockedCheckpointRecorder) ExtractRunning(lane, key string, deps []CheckpointFingerprint) error {
|
||||
return r.call(func() error { return r.inner.ExtractRunning(lane, key, deps) })
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) ExtractSucceeded(lane, key string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
||||
return r.call(func() error { return r.inner.ExtractSucceeded(lane, key, deps, outputs, rejected, warnings) })
|
||||
func (r *lockedCheckpointRecorder) ExtractSucceeded(lane, key string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||
return r.call(func() error { return r.inner.ExtractSucceeded(lane, key, deps, outputs, rejected) })
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) ExtractFailed(lane, key string, deps []CheckpointFingerprint, err error) error {
|
||||
return r.call(func() error { return r.inner.ExtractFailed(lane, key, deps, err) })
|
||||
@@ -143,8 +143,8 @@ func (r *lockedCheckpointRecorder) ExtractFailed(lane, key string, deps []Checkp
|
||||
func (r *lockedCheckpointRecorder) MergeRunning(lane, key string, deps []CheckpointFingerprint) error {
|
||||
return r.call(func() error { return r.inner.MergeRunning(lane, key, deps) })
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) MergeSucceeded(lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
return r.call(func() error { return r.inner.MergeSucceeded(lane, key, deps, output, warnings) })
|
||||
func (r *lockedCheckpointRecorder) MergeSucceeded(lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
return r.call(func() error { return r.inner.MergeSucceeded(lane, key, deps, output) })
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) MergeRejected(lane, key string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||
return r.call(func() error { return r.inner.MergeRejected(lane, key, deps, rejected) })
|
||||
@@ -155,8 +155,8 @@ func (r *lockedCheckpointRecorder) MergeFailed(lane, key string, deps []Checkpoi
|
||||
func (r *lockedCheckpointRecorder) NormalizeRunning(lane, key string, deps []CheckpointFingerprint) error {
|
||||
return r.call(func() error { return r.inner.NormalizeRunning(lane, key, deps) })
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) NormalizeSucceeded(lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
return r.call(func() error { return r.inner.NormalizeSucceeded(lane, key, deps, output, warnings) })
|
||||
func (r *lockedCheckpointRecorder) NormalizeSucceeded(lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
return r.call(func() error { return r.inner.NormalizeSucceeded(lane, key, deps, output) })
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) NormalizeRejected(lane, key string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||
return r.call(func() error { return r.inner.NormalizeRejected(lane, key, deps, rejected) })
|
||||
@@ -173,12 +173,12 @@ func (r *lockedCheckpointRecorder) ExtractRunningForStep(step, lane, key string,
|
||||
return r.inner.ExtractRunning(lane, key, deps)
|
||||
})
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) ExtractSucceededForStep(step, lane, key string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
||||
func (r *lockedCheckpointRecorder) ExtractSucceededForStep(step, lane, key string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||
return r.call(func() error {
|
||||
if v, ok := r.inner.(StepCheckpointRecorder); ok {
|
||||
return v.ExtractSucceededForStep(step, lane, key, deps, outputs, rejected, warnings)
|
||||
return v.ExtractSucceededForStep(step, lane, key, deps, outputs, rejected)
|
||||
}
|
||||
return r.inner.ExtractSucceeded(lane, key, deps, outputs, rejected, warnings)
|
||||
return r.inner.ExtractSucceeded(lane, key, deps, outputs, rejected)
|
||||
})
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) ExtractFailedForStep(step, lane, key string, deps []CheckpointFingerprint, err error) error {
|
||||
@@ -197,12 +197,12 @@ func (r *lockedCheckpointRecorder) MergeRunningForStep(step, lane, key string, d
|
||||
return r.inner.MergeRunning(lane, key, deps)
|
||||
})
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) MergeSucceededForStep(step, lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
func (r *lockedCheckpointRecorder) MergeSucceededForStep(step, lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
return r.call(func() error {
|
||||
if v, ok := r.inner.(StepCheckpointRecorder); ok {
|
||||
return v.MergeSucceededForStep(step, lane, key, deps, output, warnings)
|
||||
return v.MergeSucceededForStep(step, lane, key, deps, output)
|
||||
}
|
||||
return r.inner.MergeSucceeded(lane, key, deps, output, warnings)
|
||||
return r.inner.MergeSucceeded(lane, key, deps, output)
|
||||
})
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) MergeRejectedForStep(step, lane, key string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||
@@ -229,12 +229,12 @@ func (r *lockedCheckpointRecorder) NormalizeRunningForStep(step, lane, key strin
|
||||
return r.inner.NormalizeRunning(lane, key, deps)
|
||||
})
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) NormalizeSucceededForStep(step, lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
||||
func (r *lockedCheckpointRecorder) NormalizeSucceededForStep(step, lane, key string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||
return r.call(func() error {
|
||||
if v, ok := r.inner.(StepCheckpointRecorder); ok {
|
||||
return v.NormalizeSucceededForStep(step, lane, key, deps, output, warnings)
|
||||
return v.NormalizeSucceededForStep(step, lane, key, deps, output)
|
||||
}
|
||||
return r.inner.NormalizeSucceeded(lane, key, deps, output, warnings)
|
||||
return r.inner.NormalizeSucceeded(lane, key, deps, output)
|
||||
})
|
||||
}
|
||||
func (r *lockedCheckpointRecorder) NormalizeRejectedForStep(step, lane, key string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||
|
||||
@@ -23,7 +23,6 @@ type erasedMergeArtifact struct {
|
||||
|
||||
type erasedTypedResult struct {
|
||||
Value any
|
||||
Warnings []contracts.Warning
|
||||
Diagnostics []contracts.ProducerDiagnostic
|
||||
Retry *contracts.NormalizeRetry
|
||||
ModelCandidate *contracts.ModelCandidate
|
||||
|
||||
@@ -29,14 +29,12 @@ type validationRecord struct {
|
||||
reasonCode string
|
||||
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
|
||||
}
|
||||
@@ -54,16 +52,6 @@ func (report validationReport) Records() []validationRecord {
|
||||
return records
|
||||
}
|
||||
|
||||
func (report validationReport) Warnings() []contracts.Warning {
|
||||
var warnings []contracts.Warning
|
||||
for _, record := range report.records {
|
||||
if record.outcome == validationApproved || record.outcome == validationRejected {
|
||||
warnings = append(warnings, cloneWarnings(record.warnings)...)
|
||||
}
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (report validationReport) Diagnostics() []validationDiagnosticRecord {
|
||||
var diagnostics []validationDiagnosticRecord
|
||||
for _, record := range report.records {
|
||||
@@ -207,14 +195,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), diagnostics: cloneProducerDiagnostics(invocation.result.Diagnostics), diagnosticPath: invocation.result.DiagnosticArtifactPath})
|
||||
report.records = append(report.records, validationRecord{validatorName: binding.Module, outcome: validationApproved, attemptCount: attempt, 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), diagnostics: cloneProducerDiagnostics(invocation.result.Diagnostics), 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, diagnostics: cloneProducerDiagnostics(invocation.result.Diagnostics), correctionGuidance: invocation.result.CorrectionGuidance})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,24 @@ import (
|
||||
|
||||
func TestExecuteValidationChainSettlesEveryValidatorInOrder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chain preparedValidatorChain
|
||||
invoke validationInvoker
|
||||
want []validationOutcome
|
||||
wantCalls []string
|
||||
wantGuidance []string
|
||||
wantWarnings []string
|
||||
name string
|
||||
chain preparedValidatorChain
|
||||
invoke validationInvoker
|
||||
want []validationOutcome
|
||||
wantCalls []string
|
||||
wantGuidance []string
|
||||
wantDiagnostics []string
|
||||
}{
|
||||
{
|
||||
name: "all approved",
|
||||
chain: validationChain(validationSpec("shape", contracts.ExecutionClassDeterministic, 0), validationSpec("refs", contracts.ExecutionClassDeterministic, 0)),
|
||||
invoke: validationSequence(map[string][]validationStep{
|
||||
"shape": {{result: contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{ReasonCode: "shape"}}}}},
|
||||
"refs": {{result: contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{ReasonCode: "refs"}}}}},
|
||||
"shape": {{result: contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{validationDiagnostic("shape")}}}},
|
||||
"refs": {{result: contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{validationDiagnostic("refs")}}}},
|
||||
}),
|
||||
want: []validationOutcome{validationApproved, validationApproved},
|
||||
wantCalls: []string{"shape:1", "refs:1"},
|
||||
wantWarnings: []string{"shape", "refs"},
|
||||
want: []validationOutcome{validationApproved, validationApproved},
|
||||
wantCalls: []string{"shape:1", "refs:1"},
|
||||
wantDiagnostics: []string{"shape", "refs"},
|
||||
},
|
||||
{
|
||||
name: "multiple rejections deduplicate guidance",
|
||||
@@ -117,13 +117,13 @@ func TestExecuteValidationChainSettlesEveryValidatorInOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
warnings := report.Warnings()
|
||||
var warningCodes []string
|
||||
for _, warning := range warnings {
|
||||
warningCodes = append(warningCodes, warning.ReasonCode)
|
||||
diagnostics := report.Diagnostics()
|
||||
var diagnosticCodes []string
|
||||
for _, record := range diagnostics {
|
||||
diagnosticCodes = append(diagnosticCodes, record.diagnostic.ReasonCode)
|
||||
}
|
||||
if !reflect.DeepEqual(warningCodes, test.wantWarnings) {
|
||||
t.Fatalf("Warnings() = %#v, want codes %#v", warnings, test.wantWarnings)
|
||||
if !reflect.DeepEqual(diagnosticCodes, test.wantDiagnostics) {
|
||||
t.Fatalf("Diagnostics() = %#v, want codes %#v", diagnostics, test.wantDiagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -158,14 +158,24 @@ func TestValidationReportRejectsOversizedGuidanceAndOwnsRecords(t *testing.T) {
|
||||
unique[0] = byte('a' + index)
|
||||
report.records[index] = validationRecord{validatorName: "validator", outcome: validationRejected, correctionGuidance: string(unique)}
|
||||
}
|
||||
report.records[0].warnings = []contracts.Warning{{ReasonCode: "warning"}}
|
||||
report.records[0].diagnostics = []contracts.ProducerDiagnostic{validationDiagnostic("diagnostic")}
|
||||
if _, err := report.CorrectionRequest(); err == nil {
|
||||
t.Fatal("CorrectionRequest() error = nil, want aggregate overflow error")
|
||||
}
|
||||
records := report.Records()
|
||||
records[0].warnings[0].ReasonCode = "changed"
|
||||
if report.Records()[0].warnings[0].ReasonCode != "warning" {
|
||||
t.Fatal("Records() exposed mutable warning storage")
|
||||
records[0].diagnostics[0].ReasonCode = "changed"
|
||||
if report.Records()[0].diagnostics[0].ReasonCode != "diagnostic" {
|
||||
t.Fatal("Records() exposed mutable diagnostic storage")
|
||||
}
|
||||
}
|
||||
|
||||
func validationDiagnostic(reasonCode string) contracts.ProducerDiagnostic {
|
||||
return contracts.ProducerDiagnostic{
|
||||
Disposition: contracts.DiagnosticDispositionObservation,
|
||||
Category: contracts.DiagnosticCategoryNormalization,
|
||||
ReasonCode: reasonCode,
|
||||
OccurrenceCount: 1,
|
||||
Samples: []contracts.DiagnosticSample{{Scope: "validator", Message: reasonCode}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user