Add durable validation provenance
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
@@ -123,6 +124,7 @@ func (terminal producerAttemptTerminal) clone() producerAttemptTerminal {
|
||||
terminal.Warnings = cloneWarnings(terminal.Warnings)
|
||||
if terminal.Rejection != nil {
|
||||
rejection := *terminal.Rejection
|
||||
rejection.Validation = cloneValidationSummaryPtr(rejection.Validation)
|
||||
terminal.Rejection = &rejection
|
||||
}
|
||||
terminal.Validation = cloneValidationReport(terminal.Validation)
|
||||
@@ -246,7 +248,9 @@ 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 {
|
||||
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, Warnings: terminalWarnings(output, report), Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
warnings := terminalWarnings(output, report)
|
||||
warnings = append(warnings, incompleteValidationWarnings(report)...)
|
||||
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, Warnings: warnings, Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||
}
|
||||
return failedProducerAttempt(provenance), validatorFailureError(*incomplete)
|
||||
}
|
||||
@@ -305,6 +309,78 @@ func terminalWarnings(output producerAttemptOutput, report validationReport) []c
|
||||
return warnings
|
||||
}
|
||||
|
||||
// incompleteValidationWarnings reports only validators that exhausted their
|
||||
// execution budget. It never reports rejected candidates, and it uses fixed
|
||||
// text so provider errors and correction content cannot cross this boundary.
|
||||
func incompleteValidationWarnings(report validationReport) []contracts.Warning {
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
for _, record := range report.records {
|
||||
if record.outcome != validationFailed {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: record.validatorName,
|
||||
ReasonCode: "validator_execution_incomplete",
|
||||
Message: "Validator execution did not complete within its configured budget.",
|
||||
})
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// validationSummary projects a terminal state-machine result into the durable
|
||||
// bounded representation used by manifests, rejections, and receipts.
|
||||
func validationSummary(terminal producerAttemptTerminal, stage ModuleStage, stepID, laneID, moduleKey, chunkID string, chunkIndex int) artifacts.ValidationSummary {
|
||||
summary := artifacts.ValidationSummary{
|
||||
Stage: string(stage),
|
||||
StepID: stepID,
|
||||
LaneID: laneID,
|
||||
ModuleKey: moduleKey,
|
||||
ChunkID: chunkID,
|
||||
ChunkIndex: chunkIndex,
|
||||
ProducerAttemptCount: len(terminal.Provenance),
|
||||
TerminalAction: string(terminal.Action),
|
||||
}
|
||||
switch {
|
||||
case terminal.Action == producerTerminalRejected:
|
||||
summary.Status = "rejected"
|
||||
case terminal.Action == producerTerminalFailed:
|
||||
summary.Status = "incomplete"
|
||||
case terminal.ValidationIncomplete:
|
||||
summary.Status = "incomplete"
|
||||
default:
|
||||
summary.Status = "complete"
|
||||
}
|
||||
seenValidators := make(map[string]struct{})
|
||||
seenReasons := make(map[string]struct{})
|
||||
seenIncomplete := make(map[string]struct{})
|
||||
for _, record := range terminal.Validation.records {
|
||||
if record.reasonCode != "" {
|
||||
if _, exists := seenReasons[record.reasonCode]; !exists {
|
||||
seenReasons[record.reasonCode] = struct{}{}
|
||||
summary.ReasonCodes = append(summary.ReasonCodes, record.reasonCode)
|
||||
}
|
||||
}
|
||||
if record.outcome == validationRejected {
|
||||
if _, exists := seenValidators[record.validatorName]; !exists {
|
||||
seenValidators[record.validatorName] = struct{}{}
|
||||
summary.RejectingValidators = append(summary.RejectingValidators, record.validatorName)
|
||||
}
|
||||
}
|
||||
if record.outcome == validationFailed || record.outcome == validationSkipped {
|
||||
if _, exists := seenIncomplete[record.validatorName]; !exists {
|
||||
seenIncomplete[record.validatorName] = struct{}{}
|
||||
summary.IncompleteValidators = append(summary.IncompleteValidators, record.validatorName)
|
||||
}
|
||||
}
|
||||
}
|
||||
if terminal.Rejection != nil && terminal.Rejection.ReasonCode != "" {
|
||||
if _, exists := seenReasons[terminal.Rejection.ReasonCode]; !exists {
|
||||
summary.ReasonCodes = append(summary.ReasonCodes, terminal.Rejection.ReasonCode)
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func failedProducerAttempt(provenance []producerAttemptProvenance) producerAttemptTerminal {
|
||||
return producerAttemptTerminal{Action: producerTerminalFailed, Provenance: cloneProducerAttemptProvenance(provenance)}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user