Classify framework process diagnostics
This commit is contained in:
@@ -283,7 +283,7 @@ origins; do not assert incidental diagnostic prose.
|
|||||||
This stage is appropriately sized for one high-reasoning
|
This stage is appropriately sized for one high-reasoning
|
||||||
`gpt-5.6-terra` prompt. Do not combine it with D&D migration.
|
`gpt-5.6-terra` prompt. Do not combine it with D&D migration.
|
||||||
|
|
||||||
## Stage 3 — Migrate Framework Process Signals And Close The Output Boundary
|
## Stage 3 ✅ — Migrate Framework Process Signals And Close The Output Boundary
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
StartedAt: startedAt,
|
StartedAt: startedAt,
|
||||||
LLMProfiles: llmProfiles,
|
LLMProfiles: llmProfiles,
|
||||||
Metadata: runMetadata(effective.Config.Output.Directory, debugPath),
|
Metadata: runMetadata(effective.Config.Output.Directory, debugPath),
|
||||||
Warnings: referenceWarnings,
|
Diagnostics: pipeline.ReferenceDiagnostics(referenceWarnings),
|
||||||
ChunkCacheMode: effective.Config.Cache.ChunkPlans.Mode,
|
ChunkCacheMode: effective.Config.Cache.ChunkPlans.Mode,
|
||||||
ChunkPlans: chunkPlans,
|
ChunkPlans: chunkPlans,
|
||||||
Checkpoints: checkpointRecorder,
|
Checkpoints: checkpointRecorder,
|
||||||
|
|||||||
@@ -319,8 +319,7 @@ type OutputFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutputResult struct {
|
type OutputResult struct {
|
||||||
Files []OutputFile `json:"files,omitempty"`
|
Files []OutputFile `json:"files,omitempty"`
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutputEncoder interface {
|
type OutputEncoder interface {
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ func terminalDiagnosticGroups(terminal producerAttemptTerminal, origin contracts
|
|||||||
}
|
}
|
||||||
groups = append(groups, promoted...)
|
groups = append(groups, promoted...)
|
||||||
}
|
}
|
||||||
|
if terminal.Action == producerTerminalIncompleteAccepted {
|
||||||
|
for _, record := range incompleteValidationDiagnostics(terminal.Validation) {
|
||||||
|
validatorOrigin := origin
|
||||||
|
validatorOrigin.ValidatorKey = record.validatorName
|
||||||
|
promoted, err := promoteProducerDiagnostics([]contracts.ProducerDiagnostic{record.diagnostic}, validatorOrigin, chunk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("validator %q incomplete diagnostic: %w", record.validatorName, err)
|
||||||
|
}
|
||||||
|
groups = append(groups, promoted...)
|
||||||
|
}
|
||||||
|
}
|
||||||
return groups, nil
|
return groups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
|||||||
provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptIncompleteAccepted, Validation: report})
|
provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptIncompleteAccepted, Validation: report})
|
||||||
if config.Policy.ValidatorFailure == ValidatorFailureWarnContinue {
|
if config.Policy.ValidatorFailure == ValidatorFailureWarnContinue {
|
||||||
warnings := terminalWarnings(output, report)
|
warnings := terminalWarnings(output, report)
|
||||||
warnings = append(warnings, incompleteValidationWarnings(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, Warnings: warnings, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
|
||||||
}
|
}
|
||||||
return failedProducerAttempt(provenance), validatorFailureError(*incomplete)
|
return failedProducerAttempt(provenance), validatorFailureError(*incomplete)
|
||||||
@@ -324,22 +323,27 @@ func cloneProducerDiagnostics(diagnostics []contracts.ProducerDiagnostic) []cont
|
|||||||
return contracts.CloneProducerDiagnostics(diagnostics)
|
return contracts.CloneProducerDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
// incompleteValidationWarnings reports only validators that exhausted their
|
// incompleteValidationDiagnostics reports every applicable validator that
|
||||||
// execution budget. It never reports rejected candidates, and it uses fixed
|
// could not complete under warn_continue. It uses fixed text so provider
|
||||||
// text so provider errors and correction content cannot cross this boundary.
|
// errors and arbitrary validator prose cannot cross this boundary.
|
||||||
func incompleteValidationWarnings(report validationReport) []contracts.Warning {
|
func incompleteValidationDiagnostics(report validationReport) []validationDiagnosticRecord {
|
||||||
warnings := make([]contracts.Warning, 0)
|
diagnostics := make([]validationDiagnosticRecord, 0)
|
||||||
for _, record := range report.records {
|
for _, record := range report.records {
|
||||||
if record.outcome != validationFailed {
|
if record.outcome != validationFailed && record.outcome != validationSkipped {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
diagnostics = append(diagnostics, validationDiagnosticRecord{validatorName: record.validatorName, diagnostic: contracts.ProducerDiagnostic{
|
||||||
Scope: record.validatorName,
|
Disposition: contracts.DiagnosticDispositionWarning,
|
||||||
ReasonCode: "validator_execution_incomplete",
|
Category: contracts.DiagnosticCategoryValidationIncomplete,
|
||||||
Message: "Validator execution did not complete within its configured budget.",
|
ReasonCode: "validator_execution_incomplete",
|
||||||
})
|
OccurrenceCount: 1,
|
||||||
|
Samples: []contracts.DiagnosticSample{{
|
||||||
|
Scope: record.validatorName,
|
||||||
|
Message: "Validator execution did not complete within its configured budget.",
|
||||||
|
}},
|
||||||
|
}})
|
||||||
}
|
}
|
||||||
return warnings
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
// validationSummary projects a terminal state-machine result into the durable
|
// validationSummary projects a terminal state-machine result into the durable
|
||||||
|
|||||||
@@ -324,11 +324,20 @@ func TestWarnContinueRecordsOneWarningForEachExhaustedValidator(t *testing.T) {
|
|||||||
if terminal.Action != producerTerminalIncompleteAccepted {
|
if terminal.Action != producerTerminalIncompleteAccepted {
|
||||||
t.Fatalf("terminal action = %q", terminal.Action)
|
t.Fatalf("terminal action = %q", terminal.Action)
|
||||||
}
|
}
|
||||||
if got, want := terminal.Warnings, []contracts.Warning{
|
if len(terminal.Warnings) != 0 {
|
||||||
{Scope: "first", ReasonCode: "validator_execution_incomplete", Message: "Validator execution did not complete within its configured budget."},
|
t.Fatalf("warnings = %#v, want structured diagnostics only", terminal.Warnings)
|
||||||
{Scope: "third", ReasonCode: "validator_execution_incomplete", Message: "Validator execution did not complete within its configured budget."},
|
}
|
||||||
}; !reflect.DeepEqual(got, want) {
|
groups, groupErr := terminalDiagnosticGroups(terminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: "step", LaneID: "lane", ModuleKey: "module"}, nil)
|
||||||
t.Fatalf("warnings = %#v, want %#v", got, want)
|
if groupErr != nil {
|
||||||
|
t.Fatalf("terminalDiagnosticGroups() error = %v", groupErr)
|
||||||
|
}
|
||||||
|
if len(groups) != 3 {
|
||||||
|
t.Fatalf("diagnostic groups = %#v, want every failed and skipped validator", groups)
|
||||||
|
}
|
||||||
|
for index, validator := range []string{"first", "second", "third"} {
|
||||||
|
if group := groups[index]; group.Origin.ValidatorKey != validator || group.Disposition != contracts.DiagnosticDispositionWarning || group.Category != contracts.DiagnosticCategoryValidationIncomplete || group.ReasonCode != "validator_execution_incomplete" {
|
||||||
|
t.Fatalf("diagnostic group %d = %#v, want incomplete warning for %q", index, group, validator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
summary := validationSummary(terminal, StageNormalize, "step", "lane", "module", "", 0)
|
summary := validationSummary(terminal, StageNormalize, "step", "lane", "module", "", 0)
|
||||||
if summary.Status != "incomplete" || !reflect.DeepEqual(summary.IncompleteValidators, []string{"first", "second", "third"}) || !reflect.DeepEqual(summary.ReasonCodes, []string{"missing_prerequisite"}) {
|
if summary.Status != "incomplete" || !reflect.DeepEqual(summary.IncompleteValidators, []string{"first", "second", "third"}) || !reflect.DeepEqual(summary.ReasonCodes, []string{"missing_prerequisite"}) {
|
||||||
|
|||||||
@@ -88,6 +88,26 @@ func MaterializeReferences(resolved ResolvedPipeline, catalog ModuleCatalog, opt
|
|||||||
return out, warnings, nil
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
func materializeReferenceTarget(
|
func materializeReferenceTarget(
|
||||||
pipelineID string,
|
pipelineID string,
|
||||||
target ResolvedReferenceTarget,
|
target ResolvedReferenceTarget,
|
||||||
|
|||||||
@@ -454,6 +454,10 @@ func TestMaterializeReferencesWarnsForEmptyFiles(t *testing.T) {
|
|||||||
if len(warnings) != 1 || warnings[0].ReasonCode != "empty_reference" {
|
if len(warnings) != 1 || warnings[0].ReasonCode != "empty_reference" {
|
||||||
t.Fatalf("warnings = %#v, want empty reference warning", warnings)
|
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}}) {
|
||||||
|
t.Fatalf("diagnostics = %#v, want structured empty-reference signal", diagnostics)
|
||||||
|
}
|
||||||
item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0]
|
item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0]
|
||||||
if item.SizeBytes != 0 || item.Digest != referenceDigest(nil) {
|
if item.SizeBytes != 0 || item.Digest != referenceDigest(nil) {
|
||||||
t.Fatalf("empty item = %#v, want zero size and empty digest", item)
|
t.Fatalf("empty item = %#v, want zero size and empty digest", item)
|
||||||
|
|||||||
@@ -408,8 +408,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
ModuleKey: encoder.Key(),
|
ModuleKey: encoder.Key(),
|
||||||
StartedAt: outputStarted,
|
StartedAt: outputStarted,
|
||||||
Payload: map[string]any{
|
Payload: map[string]any{
|
||||||
"files": debugOutputFiles(files),
|
"files": debugOutputFiles(files),
|
||||||
"warnings": encoded.Warnings,
|
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return failOutput(output), fmt.Errorf("write output debug artifact: %w", err)
|
return failOutput(output), fmt.Errorf("write output debug artifact: %w", err)
|
||||||
@@ -417,7 +416,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return failOutput(output), err
|
return failOutput(output), err
|
||||||
}
|
}
|
||||||
output.Warnings = append(output.Warnings, encoded.Warnings...)
|
|
||||||
output.OutputFiles = files
|
output.OutputFiles = files
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
|
|||||||
@@ -146,8 +146,7 @@ func (e *cancelingOutputEncoder) Encode(context.Context, contracts.OutputRequest
|
|||||||
e.calls.Add(1)
|
e.calls.Add(1)
|
||||||
e.cancel()
|
e.cancel()
|
||||||
return contracts.OutputResult{
|
return contracts.OutputResult{
|
||||||
Files: []contracts.OutputFile{{Name: "result.txt", ContentType: "text/plain", Bytes: []byte("result")}},
|
Files: []contracts.OutputFile{{Name: "result.txt", ContentType: "text/plain", Bytes: []byte("result")}},
|
||||||
Warnings: []contracts.Warning{{ReasonCode: "returned-after-cancel"}},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user