Finalize structured diagnostic aggregation
This commit is contained in:
@@ -481,7 +481,7 @@ observations can be present while the warning group count remains zero.
|
|||||||
This stage is appropriately sized for one high-reasoning
|
This stage is appropriately sized for one high-reasoning
|
||||||
`gpt-5.6-terra` prompt.
|
`gpt-5.6-terra` prompt.
|
||||||
|
|
||||||
## Stage 8 — Remove The Legacy Warning Path And Finalize Aggregation
|
## Stage 8 ✅ — Remove The Legacy Warning Path And Finalize Aggregation
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -77,22 +77,22 @@ func TestAssembledSpellPipelineNormalizesMergedCasts(t *testing.T) {
|
|||||||
spellnormalize.ReasonCodeDuplicateSpellCastCollapsed,
|
spellnormalize.ReasonCodeDuplicateSpellCastCollapsed,
|
||||||
"spell_not_near_source",
|
"spell_not_near_source",
|
||||||
}
|
}
|
||||||
gotWarningReasons := make([]string, len(output.Warnings))
|
gotWarningReasons := make([]string, len(output.Diagnostics.Groups))
|
||||||
for index, warning := range output.Warnings {
|
for index, group := range output.Diagnostics.Groups {
|
||||||
gotWarningReasons[index] = warning.ReasonCode
|
gotWarningReasons[index] = group.ReasonCode
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(gotWarningReasons, wantWarningReasons) {
|
if !reflect.DeepEqual(gotWarningReasons, wantWarningReasons) {
|
||||||
t.Fatalf("warnings = %#v, want deterministic normalize and validation warnings", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want deterministic normalize and validation diagnostics", output.Diagnostics)
|
||||||
}
|
}
|
||||||
if output.Warnings[2].Scope != "spell_casts[0]" || !strings.Contains(output.Warnings[2].Message, "retained input index 0") || !strings.Contains(output.Warnings[2].Message, "removed input indices [1]") {
|
if output.Diagnostics.Groups[2].Samples[0].Scope != "spell_casts[0]" || !strings.Contains(output.Diagnostics.Groups[2].Samples[0].Message, "retained input index 0") || !strings.Contains(output.Diagnostics.Groups[2].Samples[0].Message, "removed input indices [1]") {
|
||||||
t.Fatalf("duplicate warning = %#v, want retained and removed merged indices", output.Warnings[2])
|
t.Fatalf("duplicate diagnostic = %#v, want retained and removed merged indices", output.Diagnostics.Groups[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
warningsFile := decodeAssembledOutput[struct {
|
warningsFile := decodeAssembledOutput[struct {
|
||||||
Warnings []contracts.Warning `json:"warnings"`
|
Groups []contracts.DiagnosticGroup `json:"groups"`
|
||||||
}](t, output.OutputFiles, "warnings.json")
|
}](t, output.OutputFiles, "warnings.json")
|
||||||
if !reflect.DeepEqual(warningsFile.Warnings, output.Warnings) {
|
if len(warningsFile.Groups) != 0 {
|
||||||
t.Fatalf("warnings file = %#v, run warnings = %#v, want manifest output path to preserve warnings", warningsFile.Warnings, output.Warnings)
|
t.Fatalf("warnings file = %#v, want no process warnings for advisory-only diagnostics", warningsFile.Groups)
|
||||||
}
|
}
|
||||||
manifest := decodeAssembledOutput[artifacts.RunManifest](t, output.OutputFiles, "manifest.json")
|
manifest := decodeAssembledOutput[artifacts.RunManifest](t, output.OutputFiles, "manifest.json")
|
||||||
if len(manifest.ArtifactLanes) != 1 || manifest.ArtifactLanes[0].Normalizer != spellnormalize.Key {
|
if len(manifest.ArtifactLanes) != 1 || manifest.ArtifactLanes[0].Normalizer != spellnormalize.Key {
|
||||||
@@ -155,9 +155,9 @@ func TestAssembledSpellPipelineHonorsNormalizeValidatorOverride(t *testing.T) {
|
|||||||
if err != nil || output.Manifest.ValidationStatus != "approved" || len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 1 {
|
if err != nil || output.Manifest.ValidationStatus != "approved" || len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 1 {
|
||||||
t.Fatalf("Run() error = %v output = %#v, want approved override run", err, output)
|
t.Fatalf("Run() error = %v output = %#v, want approved override run", err, output)
|
||||||
}
|
}
|
||||||
for _, warning := range output.Warnings {
|
for _, group := range output.Diagnostics.Groups {
|
||||||
if warning.ReasonCode == "spell_not_near_source" {
|
if group.ReasonCode == "spell_not_near_source" {
|
||||||
t.Fatalf("warnings = %#v, want explicit validator override to replace default relatedness chain", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want explicit validator override to replace default relatedness chain", output.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,8 +190,8 @@ func TestAssembledSpellPipelinePromotesTerminalUnknownSpellWarning(t *testing.T)
|
|||||||
if !reflect.DeepEqual(rejectedFile.Rejected, output.Rejected) {
|
if !reflect.DeepEqual(rejectedFile.Rejected, output.Rejected) {
|
||||||
t.Fatalf("rejected file = %#v, run rejections = %#v, want durable rejection diagnostic", rejectedFile.Rejected, output.Rejected)
|
t.Fatalf("rejected file = %#v, run rejections = %#v, want durable rejection diagnostic", rejectedFile.Rejected, output.Rejected)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 2 || output.Warnings[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Warnings[0].Scope != "spell_casts[0]" || output.Warnings[1].ReasonCode != "spell_not_near_source" {
|
if len(output.Diagnostics.Groups) != 2 || output.Diagnostics.Groups[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Diagnostics.Groups[0].Samples[0].Scope != "spell_casts[0]" || output.Diagnostics.Groups[1].ReasonCode != "spell_not_near_source" {
|
||||||
t.Fatalf("warnings = %#v, want complete terminal normalize validation warnings", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want complete terminal normalize validation diagnostics", output.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,14 +219,14 @@ func TestAssembledSpellPipelinePromotesUnknownSpellWarningWhenOverrideAccepts(t
|
|||||||
if len(normalized.SpellCasts) != 1 || normalized.SpellCasts[0].Spell != "Mysterious Burst" {
|
if len(normalized.SpellCasts) != 1 || normalized.SpellCasts[0].Spell != "Mysterious Burst" {
|
||||||
t.Fatalf("normalized casts = %#v, want unresolved name preserved", normalized.SpellCasts)
|
t.Fatalf("normalized casts = %#v, want unresolved name preserved", normalized.SpellCasts)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Warnings[0].Scope != "spell_casts[0]" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Diagnostics.Groups[0].Samples[0].Scope != "spell_casts[0]" {
|
||||||
t.Fatalf("warnings = %#v, want promoted scoped unresolved-name warning", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want promoted scoped unresolved-name diagnostic", output.Diagnostics)
|
||||||
}
|
}
|
||||||
warningsFile := decodeAssembledOutput[struct {
|
warningsFile := decodeAssembledOutput[struct {
|
||||||
Warnings []contracts.Warning `json:"warnings"`
|
Groups []contracts.DiagnosticGroup `json:"groups"`
|
||||||
}](t, output.OutputFiles, "warnings.json")
|
}](t, output.OutputFiles, "warnings.json")
|
||||||
if !reflect.DeepEqual(warningsFile.Warnings, output.Warnings) {
|
if len(warningsFile.Groups) != 0 {
|
||||||
t.Fatalf("warnings file = %#v, run warnings = %#v, want durable unresolved-name warning", warningsFile.Warnings, output.Warnings)
|
t.Fatalf("warnings file = %#v, want no process warnings for an advisory diagnostic", warningsFile.Groups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ func TestProductionSceneDescriptionWorkflow(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(durable, want) {
|
if !reflect.DeepEqual(durable, want) {
|
||||||
t.Fatalf("durable output payload = %#v, want %#v", durable, want)
|
t.Fatalf("durable output payload = %#v, want %#v", durable, want)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 0 {
|
if len(output.Diagnostics.Groups) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want grounded descriptions without warnings", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want grounded descriptions without diagnostics", output.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,10 +239,10 @@ func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) {
|
|||||||
t.Fatalf("rejected = %#v, want empty rejection list", rejected.Rejected)
|
t.Fatalf("rejected = %#v, want empty rejection list", rejected.Rejected)
|
||||||
}
|
}
|
||||||
warnings := readProductionJSON[struct {
|
warnings := readProductionJSON[struct {
|
||||||
Warnings []json.RawMessage `json:"warnings"`
|
Groups []json.RawMessage `json:"groups"`
|
||||||
}](t, filepath.Join(runRoot, "warnings.json"))
|
}](t, filepath.Join(runRoot, "warnings.json"))
|
||||||
if len(warnings.Warnings) != 0 {
|
if len(warnings.Groups) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want empty warning list", warnings.Warnings)
|
t.Fatalf("warnings = %#v, want empty warning list", warnings.Groups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -852,10 +852,10 @@ func TestProductionSceneRunRecordsAnnotationFreeChunkPlanAndProvenance(t *testin
|
|||||||
t.Fatalf("chunk map range annotations = %#v, want none", chunkMap.Chunks[0].Annotations)
|
t.Fatalf("chunk map range annotations = %#v, want none", chunkMap.Chunks[0].Annotations)
|
||||||
}
|
}
|
||||||
warnings := readProductionJSON[struct {
|
warnings := readProductionJSON[struct {
|
||||||
Warnings []contracts.Warning `json:"warnings"`
|
Groups []contracts.DiagnosticGroup `json:"groups"`
|
||||||
}](t, filepath.Join(outputRoot, productionRunID, "warnings.json"))
|
}](t, filepath.Join(outputRoot, productionRunID, "warnings.json"))
|
||||||
if len(warnings.Warnings) != 0 {
|
if len(warnings.Groups) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want none", warnings.Warnings)
|
t.Fatalf("warnings = %#v, want none", warnings.Groups)
|
||||||
}
|
}
|
||||||
if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 || len(fake.requestsFor(itemoccurrenceextract.PromptID)) != 1 {
|
if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 || len(fake.requestsFor(itemoccurrenceextract.PromptID)) != 1 {
|
||||||
t.Fatalf("fake prompt requests = %#v, want one scene, spell, and item-occurrence request", fake.requestPrompts())
|
t.Fatalf("fake prompt requests = %#v, want one scene, spell, and item-occurrence request", fake.requestPrompts())
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, fmt.Errorf("resolve working directory: %w", err))
|
return failPipelineCommand(stderr, commandState, terminalWriter, fmt.Errorf("resolve working directory: %w", err))
|
||||||
}
|
}
|
||||||
materialized, referenceWarnings, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, catalog, pipeline.ReferenceMaterializationOptions{
|
materialized, referenceDiagnostics, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, catalog, pipeline.ReferenceMaterializationOptions{
|
||||||
ConfigPath: loadedConfigPath,
|
ConfigPath: loadedConfigPath,
|
||||||
WorkingDir: workingDir,
|
WorkingDir: workingDir,
|
||||||
})
|
})
|
||||||
@@ -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),
|
||||||
Diagnostics: pipeline.ReferenceDiagnostics(referenceWarnings),
|
Diagnostics: referenceDiagnostics,
|
||||||
ChunkCacheMode: effective.Config.Cache.ChunkPlans.Mode,
|
ChunkCacheMode: effective.Config.Cache.ChunkPlans.Mode,
|
||||||
ChunkPlans: chunkPlans,
|
ChunkPlans: chunkPlans,
|
||||||
Checkpoints: checkpointRecorder,
|
Checkpoints: checkpointRecorder,
|
||||||
@@ -513,8 +513,8 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
fmt.Fprintf(stdout, "debug=%s\n", debugPath)
|
fmt.Fprintf(stdout, "debug=%s\n", debugPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(output.Warnings) > 0 {
|
if warningCount := warningGroupCount(output.Diagnostics); warningCount > 0 {
|
||||||
fmt.Fprintf(stderr, "notarius: run completed with %d warning(s)\n", len(output.Warnings))
|
fmt.Fprintf(stderr, "notarius: run completed with %d warning(s)\n", warningCount)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -538,7 +538,7 @@ func writePartialSummary(summary *debugbundle.SummaryWriter, output pipeline.Run
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := summary.WriteWarnings(output.Warnings); err != nil {
|
if err := summary.WriteWarnings(output.Diagnostics); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return summary.WriteCheckpointEvents(output.CheckpointEvents)
|
return summary.WriteCheckpointEvents(output.CheckpointEvents)
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ func TestRunWarningsRemainSuccessfulAndReachDurableSurfaces(t *testing.T) {
|
|||||||
roots := newStateTestRoots(t)
|
roots := newStateTestRoots(t)
|
||||||
harness := newStateTestHarness()
|
harness := newStateTestHarness()
|
||||||
harness.includeWarnings = true
|
harness.includeWarnings = true
|
||||||
harness.chunkWarnings = []contracts.Warning{{Scope: "chunk", ReasonCode: "contract-warning", Message: "warning retained"}}
|
harness.chunkDiagnostics = []contracts.ProducerDiagnostic{stateTestDiagnostic("chunk", "contract-warning", "warning retained")}
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
code := RunWithOptions([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass", "--debug"}, &stdout, &stderr, harness.options())
|
code := RunWithOptions([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass", "--debug"}, &stdout, &stderr, harness.options())
|
||||||
if code != 0 || !strings.Contains(stdout.String(), "outputs=1") || !strings.Contains(stderr.String(), "1 warning(s)") {
|
if code != 0 || !strings.Contains(stdout.String(), "outputs=1") || !strings.Contains(stderr.String(), "1 warning(s)") {
|
||||||
@@ -602,10 +602,10 @@ func TestRunWarningsRemainSuccessfulAndReachDurableSurfaces(t *testing.T) {
|
|||||||
t.Fatalf("durable output = %q, %v", output, err)
|
t.Fatalf("durable output = %q, %v", output, err)
|
||||||
}
|
}
|
||||||
bundle := onlyChildDir(t, roots.debug)
|
bundle := onlyChildDir(t, roots.debug)
|
||||||
var warnings []contracts.Warning
|
var diagnostics contracts.DiagnosticCollection
|
||||||
readStateTestSummaryJSON(t, bundle, "warnings.json", &warnings)
|
readStateTestSummaryJSON(t, bundle, "warnings.json", &diagnostics)
|
||||||
if len(warnings) != 1 || warnings[0].ReasonCode != "contract-warning" {
|
if len(diagnostics.Groups) != 1 || diagnostics.Groups[0].ReasonCode != "contract-warning" {
|
||||||
t.Fatalf("debug warnings = %#v", warnings)
|
t.Fatalf("debug diagnostics = %#v", diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ func newRunResult(resolved pipeline.ResolvedPipeline, output pipeline.RunOutput,
|
|||||||
OutputDirectory: absOutputDirectory,
|
OutputDirectory: absOutputDirectory,
|
||||||
NormalizedOutputCount: len(output.NormalizeOutputs),
|
NormalizedOutputCount: len(output.NormalizeOutputs),
|
||||||
RejectedOutputCount: len(output.Rejected),
|
RejectedOutputCount: len(output.Rejected),
|
||||||
WarningCount: len(output.Warnings),
|
WarningCount: warningGroupCount(output.Diagnostics),
|
||||||
ValidationStatus: output.Manifest.ValidationStatus,
|
ValidationStatus: output.Manifest.ValidationStatus,
|
||||||
ValidationSummaries: cloneValidationSummaries(output.Manifest.ValidationSummaries),
|
ValidationSummaries: cloneValidationSummaries(output.Manifest.ValidationSummaries),
|
||||||
}
|
}
|
||||||
@@ -88,6 +89,16 @@ func newRunResult(resolved pipeline.ResolvedPipeline, output pipeline.RunOutput,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func warningGroupCount(diagnostics contracts.DiagnosticCollection) int {
|
||||||
|
count := 0
|
||||||
|
for _, group := range diagnostics.Groups {
|
||||||
|
if group.Disposition == contracts.DiagnosticDispositionWarning {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
func cloneValidationSummaries(summaries []artifacts.ValidationSummary) []artifacts.ValidationSummary {
|
func cloneValidationSummaries(summaries []artifacts.ValidationSummary) []artifacts.ValidationSummary {
|
||||||
if len(summaries) == 0 {
|
if len(summaries) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func TestMaintainedMinimalInvocationEmitsRunResult(t *testing.T) {
|
|||||||
func TestRunResultReportsWarningsAndDebugBundle(t *testing.T) {
|
func TestRunResultReportsWarningsAndDebugBundle(t *testing.T) {
|
||||||
roots := newStateTestRoots(t)
|
roots := newStateTestRoots(t)
|
||||||
harness := newStateTestHarness()
|
harness := newStateTestHarness()
|
||||||
harness.chunkWarnings = []contracts.Warning{{Scope: "chunk", ReasonCode: "contract-warning", Message: "warning retained"}}
|
harness.chunkDiagnostics = []contracts.ProducerDiagnostic{stateTestDiagnostic("chunk", "contract-warning", "warning retained")}
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
code := RunWithOptions([]string{
|
code := RunWithOptions([]string{
|
||||||
"run", "sample", "--config", roots.config, "--input", roots.input,
|
"run", "sample", "--config", roots.config, "--input", roots.input,
|
||||||
|
|||||||
@@ -182,8 +182,10 @@ func testRunOutput() pipeline.RunOutput {
|
|||||||
Manifest: artifacts.RunManifest{RunID: "run-123", PipelineID: "sample", ValidationStatus: "rejected"},
|
Manifest: artifacts.RunManifest{RunID: "run-123", PipelineID: "sample", ValidationStatus: "rejected"},
|
||||||
NormalizeOutputs: []contracts.SerializedOutput{{}, {}},
|
NormalizeOutputs: []contracts.SerializedOutput{{}, {}},
|
||||||
Rejected: []contracts.RejectedOutput{{}},
|
Rejected: []contracts.RejectedOutput{{}},
|
||||||
Warnings: []contracts.Warning{{}},
|
Diagnostics: contracts.DiagnosticCollection{Groups: []contracts.DiagnosticGroup{{
|
||||||
OutputFiles: []contracts.OutputFile{{Name: "index.json"}},
|
Disposition: contracts.DiagnosticDispositionWarning,
|
||||||
|
}}},
|
||||||
|
OutputFiles: []contracts.OutputFile{{Name: "index.json"}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (s *pipelineCommandState) observeOutput(output pipeline.RunOutput) {
|
|||||||
}
|
}
|
||||||
s.report.OutputCount = len(output.NormalizeOutputs)
|
s.report.OutputCount = len(output.NormalizeOutputs)
|
||||||
s.report.RejectedCount = len(output.Rejected)
|
s.report.RejectedCount = len(output.Rejected)
|
||||||
s.report.WarningCount = len(output.Warnings)
|
s.report.WarningCount = warningGroupCount(output.Diagnostics)
|
||||||
s.report.ValidationStatus = output.Manifest.ValidationStatus
|
s.report.ValidationStatus = output.Manifest.ValidationStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t
|
|||||||
Kind: dnd.SpellListKind, Schema: normalizeSchema, MediaType: "application/json", Content: []byte(`{"spell_casts":[]}`),
|
Kind: dnd.SpellListKind, Schema: normalizeSchema, MediaType: "application/json", Content: []byte(`{"spell_casts":[]}`),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := recorder.NormalizeSucceeded("spells", spellnormalize.Key, normalizeDependencies, normalizeArtifact, nil); err != nil {
|
if err := recorder.NormalizeSucceeded("spells", spellnormalize.Key, normalizeDependencies, normalizeArtifact); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
|
|||||||
if rejection.ReasonCode != "unknown_spell" || rejection.AttemptCount != retries+1 {
|
if rejection.ReasonCode != "unknown_spell" || rejection.AttemptCount != retries+1 {
|
||||||
t.Fatalf("rejection = %#v, want exhausted unknown-spell rejection", rejection)
|
t.Fatalf("rejection = %#v, want exhausted unknown-spell rejection", rejection)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != "spell_not_near_source" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "spell_not_near_source" {
|
||||||
t.Fatalf("warnings = %#v, want complete terminal validation warnings", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want complete terminal validation diagnostic", output.Diagnostics)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -108,8 +108,8 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
|
|||||||
if len(value.SpellCasts) != 1 || value.SpellCasts[0].Spell != tt.wantSpell {
|
if len(value.SpellCasts) != 1 || value.SpellCasts[0].Spell != tt.wantSpell {
|
||||||
t.Fatalf("normalized spell list = %#v, want accepted overlay spell", value)
|
t.Fatalf("normalized spell list = %#v, want accepted overlay spell", value)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != tt.wantWarningCode {
|
if len(output.Diagnostics.Groups) != 2 || output.Diagnostics.Groups[0].ReasonCode != tt.wantWarningCode {
|
||||||
t.Fatalf("warnings = %#v, want the terminal extract compatibility projection", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want terminal extract and normalize diagnostics", output.Diagnostics)
|
||||||
}
|
}
|
||||||
if len(output.Diagnostics.Groups) != 2 {
|
if len(output.Diagnostics.Groups) != 2 {
|
||||||
t.Fatalf("diagnostics = %#v, want extract and normalize data-quality advisories", output.Diagnostics)
|
t.Fatalf("diagnostics = %#v, want extract and normalize data-quality advisories", output.Diagnostics)
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ func TestRunWritesTerminalArtifactsForResolutionPipelineAndOutputFailures(t *tes
|
|||||||
func TestRunRetainsPartialPipelineOutcomeInFailureSummary(t *testing.T) {
|
func TestRunRetainsPartialPipelineOutcomeInFailureSummary(t *testing.T) {
|
||||||
roots := newStateTestRoots(t)
|
roots := newStateTestRoots(t)
|
||||||
harness := newStateTestHarness()
|
harness := newStateTestHarness()
|
||||||
harness.chunkWarnings = []contracts.Warning{{Scope: "chunk", ReasonCode: "partial-warning", Message: "warning retained before failure"}}
|
harness.chunkDiagnostics = []contracts.ProducerDiagnostic{stateTestDiagnostic("chunk", "partial-warning", "warning retained before failure")}
|
||||||
harness.extractErr = errors.New("synthetic partial pipeline failure")
|
harness.extractErr = errors.New("synthetic partial pipeline failure")
|
||||||
|
|
||||||
result := runStateTest(t, roots, harness.options(), true, true, "bypass")
|
result := runStateTest(t, roots, harness.options(), true, true, "bypass")
|
||||||
@@ -463,10 +463,10 @@ func TestRunRetainsPartialPipelineOutcomeInFailureSummary(t *testing.T) {
|
|||||||
if manifest.RunID != report.RunID || manifest.PipelineID != "sample" || manifest.ValidationStatus != "failed" {
|
if manifest.RunID != report.RunID || manifest.PipelineID != "sample" || manifest.ValidationStatus != "failed" {
|
||||||
t.Fatalf("partial manifest = %#v", manifest)
|
t.Fatalf("partial manifest = %#v", manifest)
|
||||||
}
|
}
|
||||||
var warnings []contracts.Warning
|
var diagnostics contracts.DiagnosticCollection
|
||||||
readStateTestSummaryJSON(t, bundlePath, "warnings.json", &warnings)
|
readStateTestSummaryJSON(t, bundlePath, "warnings.json", &diagnostics)
|
||||||
if len(warnings) != 1 || warnings[0].ReasonCode != "partial-warning" {
|
if len(diagnostics.Groups) != 1 || diagnostics.Groups[0].ReasonCode != "partial-warning" {
|
||||||
t.Fatalf("partial warnings = %#v", warnings)
|
t.Fatalf("partial diagnostics = %#v", diagnostics)
|
||||||
}
|
}
|
||||||
var events []pipeline.CheckpointEvent
|
var events []pipeline.CheckpointEvent
|
||||||
readStateTestSummaryJSON(t, bundlePath, "checkpoint-events.json", &events)
|
readStateTestSummaryJSON(t, bundlePath, "checkpoint-events.json", &events)
|
||||||
@@ -863,10 +863,10 @@ type stateTestHarness struct {
|
|||||||
chunkCalls, extractCalls int
|
chunkCalls, extractCalls int
|
||||||
runIDCalls uint64
|
runIDCalls uint64
|
||||||
extractErr error
|
extractErr error
|
||||||
chunkWarnings []contracts.Warning
|
chunkDiagnostics []contracts.ProducerDiagnostic
|
||||||
moduleProfiles []string
|
moduleProfiles []string
|
||||||
sessionIDs []string
|
sessionIDs []string
|
||||||
outputWarnings []contracts.Warning
|
outputDiagnostics contracts.DiagnosticCollection
|
||||||
includeWarnings bool
|
includeWarnings bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,7 +892,7 @@ func (h *stateTestHarness) options() Options {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := registries.Outputs.RegisterWithSpec(pipeline.ModuleSpec{Key: "test/output", Stage: pipeline.StageOutput, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"normalized"}, Provides: []string{"output"}}, func() (contracts.OutputEncoder, error) {
|
if err := registries.Outputs.RegisterWithSpec(pipeline.ModuleSpec{Key: "test/output", Stage: pipeline.StageOutput, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"normalized"}, Provides: []string{"output"}}, func() (contracts.OutputEncoder, error) {
|
||||||
return stateTestOutput{harness: h, includeWarnings: h.includeWarnings}, nil
|
return stateTestOutput{harness: h, includeDiagnostics: h.includeWarnings}, nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -931,7 +931,7 @@ func (c stateTestChunker) Plan(_ context.Context, req contracts.ChunkRequest) (c
|
|||||||
c.harness.mu.Lock()
|
c.harness.mu.Lock()
|
||||||
c.harness.chunkCalls++
|
c.harness.chunkCalls++
|
||||||
c.harness.mu.Unlock()
|
c.harness.mu.Unlock()
|
||||||
return contracts.ChunkPlanResult{Plan: source.ChunkPlan{SourceDigest: req.Source.Digest, Ranges: []source.ChunkRange{{StartUnitID: 1, EndUnitID: 1}}}, Warnings: append([]contracts.Warning(nil), c.harness.chunkWarnings...)}, nil
|
return contracts.ChunkPlanResult{Plan: source.ChunkPlan{SourceDigest: req.Source.Digest, Ranges: []source.ChunkRange{{StartUnitID: 1, EndUnitID: 1}}}, Diagnostics: contracts.CloneProducerDiagnostics(c.harness.chunkDiagnostics)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateTestArtifactKind contracts.ArtifactKind = "test/artifact"
|
const stateTestArtifactKind contracts.ArtifactKind = "test/artifact"
|
||||||
@@ -999,22 +999,26 @@ func (n stateTestNormalizer) Normalize(_ context.Context, req contracts.TypedNor
|
|||||||
}
|
}
|
||||||
|
|
||||||
type stateTestOutput struct {
|
type stateTestOutput struct {
|
||||||
harness *stateTestHarness
|
harness *stateTestHarness
|
||||||
includeWarnings bool
|
includeDiagnostics bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o stateTestOutput) Key() string { return "test/output" }
|
func (o stateTestOutput) Key() string { return "test/output" }
|
||||||
func (o stateTestOutput) Encode(_ context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
|
func (o stateTestOutput) Encode(_ context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
|
||||||
o.harness.mu.Lock()
|
o.harness.mu.Lock()
|
||||||
o.harness.outputWarnings = append([]contracts.Warning(nil), req.Warnings...)
|
o.harness.outputDiagnostics = contracts.CloneDiagnosticCollection(req.Diagnostics)
|
||||||
o.harness.mu.Unlock()
|
o.harness.mu.Unlock()
|
||||||
data := []byte("{\"ok\":true}\n")
|
data := []byte("{\"ok\":true}\n")
|
||||||
if o.includeWarnings && len(req.Warnings) > 0 {
|
if o.includeDiagnostics && len(req.Diagnostics.Groups) > 0 {
|
||||||
data = []byte(fmt.Sprintf("{\"ok\":true,\"warnings\":%q}\n", req.Warnings[0].ReasonCode))
|
data = []byte(fmt.Sprintf("{\"ok\":true,\"diagnostics\":%q}\n", req.Diagnostics.Groups[0].ReasonCode))
|
||||||
}
|
}
|
||||||
return contracts.OutputResult{Files: []contracts.OutputFile{{Name: "result.json", Bytes: data}}}, nil
|
return contracts.OutputResult{Files: []contracts.OutputFile{{Name: "result.json", Bytes: data}}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stateTestDiagnostic(scope, reasonCode, message string) contracts.ProducerDiagnostic {
|
||||||
|
return contracts.ProducerDiagnostic{Disposition: contracts.DiagnosticDispositionWarning, Category: contracts.DiagnosticCategoryDegradation, ReasonCode: reasonCode, OccurrenceCount: 1, Samples: []contracts.DiagnosticSample{{Scope: scope, Message: message}}}
|
||||||
|
}
|
||||||
|
|
||||||
type failingDebugRecorder struct{}
|
type failingDebugRecorder struct{}
|
||||||
|
|
||||||
func (failingDebugRecorder) Enabled() bool { return true }
|
func (failingDebugRecorder) Enabled() bool { return true }
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
|
|||||||
if err := summary.WriteRunReport(RunReport{RunID: bundle.RunID(), PipelineID: "test"}); err != nil {
|
if err := summary.WriteRunReport(RunReport{RunID: bundle.RunID(), PipelineID: "test"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := summary.WriteWarnings([]contracts.Warning{{ReasonCode: "test"}}); err != nil {
|
if err := summary.WriteWarnings(contracts.DiagnosticCollection{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := summary.WriteError("failed"); err != nil {
|
if err := summary.WriteError("failed"); err != nil {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func (w *SummaryWriter) WriteChunkPlan(v artifacts.ChunkPlanSummary) error {
|
|||||||
return w.writeJSON(ArtifactChunkPlan, v)
|
return w.writeJSON(ArtifactChunkPlan, v)
|
||||||
}
|
}
|
||||||
func (w *SummaryWriter) WriteRunReport(v RunReport) error { return w.writeJSON(ArtifactRunReport, v) }
|
func (w *SummaryWriter) WriteRunReport(v RunReport) error { return w.writeJSON(ArtifactRunReport, v) }
|
||||||
func (w *SummaryWriter) WriteWarnings(v []contracts.Warning) error {
|
func (w *SummaryWriter) WriteWarnings(v contracts.DiagnosticCollection) error {
|
||||||
return w.writeJSON(ArtifactWarnings, v)
|
return w.writeJSON(ArtifactWarnings, v)
|
||||||
}
|
}
|
||||||
func (w *SummaryWriter) WriteError(message string) error {
|
func (w *SummaryWriter) WriteError(message string) error {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ type filesystemCheckpointFixture struct {
|
|||||||
merge pipeline.CheckpointArtifact
|
merge pipeline.CheckpointArtifact
|
||||||
normalize pipeline.CheckpointArtifact
|
normalize pipeline.CheckpointArtifact
|
||||||
dependencies []pipeline.CheckpointFingerprint
|
dependencies []pipeline.CheckpointFingerprint
|
||||||
warnings []contracts.Warning
|
|
||||||
rejected []contracts.RejectedOutput
|
rejected []contracts.RejectedOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +38,6 @@ func TestFilesystemCheckpointRoundTripsAllStages(t *testing.T) {
|
|||||||
fixture.extract.Diagnostics[0].Diagnostic.Samples[0].Message = "caller mutation"
|
fixture.extract.Diagnostics[0].Diagnostic.Samples[0].Message = "caller mutation"
|
||||||
fixture.merge.Artifact.Content[0] = 'x'
|
fixture.merge.Artifact.Content[0] = 'x'
|
||||||
fixture.normalize.Artifact.Content[0] = 'x'
|
fixture.normalize.Artifact.Content[0] = 'x'
|
||||||
fixture.warnings[0].Message = "caller mutation"
|
|
||||||
fixture.rejected[0].Message = "caller mutation"
|
fixture.rejected[0].Message = "caller mutation"
|
||||||
|
|
||||||
t.Run("source", func(t *testing.T) {
|
t.Run("source", func(t *testing.T) {
|
||||||
@@ -61,11 +59,11 @@ func TestFilesystemCheckpointRoundTripsAllStages(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("extract", func(t *testing.T) {
|
t.Run("extract", func(t *testing.T) {
|
||||||
got, decision := fixture.loader.Extract("lane-a", "extract-module", fixture.dependencies)
|
got, decision := fixture.loader.Extract("lane-a", "extract-module", fixture.dependencies)
|
||||||
if !decision.Reused || len(got.Outputs) != 1 || len(got.Rejected) != 1 || len(got.Warnings) != 1 || len(got.Outputs[0].Diagnostics) != 1 {
|
if !decision.Reused || len(got.Outputs) != 1 || len(got.Rejected) != 1 || len(got.Outputs[0].Diagnostics) != 1 {
|
||||||
t.Fatalf("extract result=%#v decision=%#v", got, decision)
|
t.Fatalf("extract result=%#v decision=%#v", got, decision)
|
||||||
}
|
}
|
||||||
output := got.Outputs[0]
|
output := got.Outputs[0]
|
||||||
if !bytes.Equal(output.Artifact.Content, []byte(`{"spell":"fire"}`)) || output.Artifact.Kind != "spell" || output.Artifact.Schema.ID != "spell-schema" || output.Artifact.Schema.Version != "1" || output.Artifact.MediaType != "application/json" || output.Artifact.Metadata["chunk"] != "chunk-a" || output.ChunkRef.StartUnitID != 1 || output.Diagnostics[0].Diagnostic.ReasonCode != "normalized_record" || got.Warnings[0].ReasonCode != "partial" || got.Rejected[0].ReasonCode != "invalid_source" {
|
if !bytes.Equal(output.Artifact.Content, []byte(`{"spell":"fire"}`)) || output.Artifact.Kind != "spell" || output.Artifact.Schema.ID != "spell-schema" || output.Artifact.Schema.Version != "1" || output.Artifact.MediaType != "application/json" || output.Artifact.Metadata["chunk"] != "chunk-a" || output.ChunkRef.StartUnitID != 1 || output.Diagnostics[0].Diagnostic.ReasonCode != "normalized_record" || got.Rejected[0].ReasonCode != "invalid_source" {
|
||||||
t.Fatalf("extract values were not restored: %#v", got)
|
t.Fatalf("extract values were not restored: %#v", got)
|
||||||
}
|
}
|
||||||
manifest := readManifest[ExtractLaneManifest](t, filepath.Join(fixture.root, mustRelativePath(t, fixture.identity), "extract", "lane-a", "manifest.json"))
|
manifest := readManifest[ExtractLaneManifest](t, filepath.Join(fixture.root, mustRelativePath(t, fixture.identity), "extract", "lane-a", "manifest.json"))
|
||||||
@@ -74,37 +72,36 @@ func TestFilesystemCheckpointRoundTripsAllStages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
got.Outputs[0].Artifact.Content[0] = 'y'
|
got.Outputs[0].Artifact.Content[0] = 'y'
|
||||||
got.Warnings[0].Message = "loaded mutation"
|
|
||||||
reloaded, decision := fixture.loader.Extract("lane-a", "extract-module", fixture.dependencies)
|
reloaded, decision := fixture.loader.Extract("lane-a", "extract-module", fixture.dependencies)
|
||||||
if !decision.Reused || !bytes.Equal(reloaded.Outputs[0].Artifact.Content, []byte(`{"spell":"fire"}`)) || reloaded.Warnings[0].Message != "partial output" {
|
if !decision.Reused || !bytes.Equal(reloaded.Outputs[0].Artifact.Content, []byte(`{"spell":"fire"}`)) {
|
||||||
t.Fatalf("extract reload changed after loaded mutation: %#v decision=%#v", reloaded, decision)
|
t.Fatalf("extract reload changed after loaded mutation: %#v decision=%#v", reloaded, decision)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
name string
|
name string
|
||||||
load func() (pipeline.CheckpointArtifact, []contracts.Warning, pipeline.CheckpointDecision)
|
load func() (pipeline.CheckpointArtifact, pipeline.CheckpointDecision)
|
||||||
want []byte
|
want []byte
|
||||||
}{
|
}{
|
||||||
{name: "merge", load: func() (pipeline.CheckpointArtifact, []contracts.Warning, pipeline.CheckpointDecision) {
|
{name: "merge", load: func() (pipeline.CheckpointArtifact, pipeline.CheckpointDecision) {
|
||||||
got, decision := fixture.loader.Merge("lane-a", "merge-module", fixture.dependencies)
|
got, decision := fixture.loader.Merge("lane-a", "merge-module", fixture.dependencies)
|
||||||
return got.Output, got.Warnings, decision
|
return got.Output, decision
|
||||||
}, want: []byte(`{"spells":["fire"]}`)},
|
}, want: []byte(`{"spells":["fire"]}`)},
|
||||||
{name: "normalize", load: func() (pipeline.CheckpointArtifact, []contracts.Warning, pipeline.CheckpointDecision) {
|
{name: "normalize", load: func() (pipeline.CheckpointArtifact, pipeline.CheckpointDecision) {
|
||||||
got, decision := fixture.loader.Normalize("lane-a", "normalize-module", fixture.dependencies)
|
got, decision := fixture.loader.Normalize("lane-a", "normalize-module", fixture.dependencies)
|
||||||
return got.Output, got.Warnings, decision
|
return got.Output, decision
|
||||||
}, want: []byte(`{"spells":["fire"],"normalized":true}`)},
|
}, want: []byte(`{"spells":["fire"],"normalized":true}`)},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, warnings, decision := tt.load()
|
got, decision := tt.load()
|
||||||
if !decision.Reused || !bytes.Equal(got.Artifact.Content, tt.want) || got.Artifact.Kind != "spell" || got.Artifact.Schema.ID != "spell-schema" || got.Artifact.Schema.Version != "1" || got.Artifact.Metadata["lane"] != "lane-a" || len(got.Diagnostics) != 1 || got.Diagnostics[0].Diagnostic.ReasonCode != "normalized_record" || len(warnings) != 1 || warnings[0].ReasonCode != "review" {
|
if !decision.Reused || !bytes.Equal(got.Artifact.Content, tt.want) || got.Artifact.Kind != "spell" || got.Artifact.Schema.ID != "spell-schema" || got.Artifact.Schema.Version != "1" || got.Artifact.Metadata["lane"] != "lane-a" || len(got.Diagnostics) != 1 || got.Diagnostics[0].Diagnostic.ReasonCode != "normalized_record" {
|
||||||
t.Fatalf("%s result=%#v warnings=%#v decision=%#v", tt.name, got, warnings, decision)
|
t.Fatalf("%s result=%#v decision=%#v", tt.name, got, decision)
|
||||||
}
|
}
|
||||||
|
|
||||||
got.Artifact.Content[0] = 'z'
|
got.Artifact.Content[0] = 'z'
|
||||||
reloaded, warnings, decision := tt.load()
|
reloaded, decision := tt.load()
|
||||||
if !decision.Reused || !bytes.Equal(reloaded.Artifact.Content, tt.want) || warnings[0].Message != "review manually" {
|
if !decision.Reused || !bytes.Equal(reloaded.Artifact.Content, tt.want) {
|
||||||
t.Fatalf("%s reload changed after loaded mutation: %#v warnings=%#v decision=%#v", tt.name, reloaded, warnings, decision)
|
t.Fatalf("%s reload changed after loaded mutation: %#v decision=%#v", tt.name, reloaded, decision)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -344,9 +341,6 @@ func TestFilesystemLoaderReadsAcceptedNormalizeWithoutStageDependencies(t *testi
|
|||||||
if checkpoint.Output.Artifact.Content == nil || string(checkpoint.Output.Artifact.Content) != string(fixture.normalize.Artifact.Content) {
|
if checkpoint.Output.Artifact.Content == nil || string(checkpoint.Output.Artifact.Content) != string(fixture.normalize.Artifact.Content) {
|
||||||
t.Fatalf("accepted normalize output = %#v, want recorded artifact", checkpoint.Output)
|
t.Fatalf("accepted normalize output = %#v, want recorded artifact", checkpoint.Output)
|
||||||
}
|
}
|
||||||
if len(checkpoint.Warnings) != 1 || checkpoint.Warnings[0].ReasonCode != "normalized" {
|
|
||||||
t.Fatalf("accepted normalize warnings = %#v", checkpoint.Warnings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilesystemLoaderRejectsInvalidAcceptedNormalize(t *testing.T) {
|
func TestFilesystemLoaderRejectsInvalidAcceptedNormalize(t *testing.T) {
|
||||||
@@ -409,8 +403,7 @@ func seedAcceptedNormalizeCheckpoint(t *testing.T) filesystemCheckpointFixture {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
stepRecorder := recorder.(pipeline.StepCheckpointRecorder)
|
stepRecorder := recorder.(pipeline.StepCheckpointRecorder)
|
||||||
warnings := []contracts.Warning{{Scope: "normalize", ReasonCode: "normalized", Message: "normalized warning"}}
|
if err := stepRecorder.NormalizeSucceededForStep("step-1", "lane-a", "normalize-module", []pipeline.CheckpointFingerprint{{Name: "merge", Value: "sha256:unavailable"}}, artifact); err != nil {
|
||||||
if err := stepRecorder.NormalizeSucceededForStep("step-1", "lane-a", "normalize-module", []pipeline.CheckpointFingerprint{{Name: "merge", Value: "sha256:unavailable"}}, artifact, warnings); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
loader, err := NewFilesystemLoader(root, identity)
|
loader, err := NewFilesystemLoader(root, identity)
|
||||||
@@ -500,20 +493,18 @@ func seedFilesystemCheckpoints(t *testing.T) filesystemCheckpointFixture {
|
|||||||
merge: checkpointArtifact("merge", `{"spells":["fire"]}`),
|
merge: checkpointArtifact("merge", `{"spells":["fire"]}`),
|
||||||
normalize: checkpointArtifact("normalize", `{"spells":["fire"],"normalized":true}`),
|
normalize: checkpointArtifact("normalize", `{"spells":["fire"],"normalized":true}`),
|
||||||
dependencies: []pipeline.CheckpointFingerprint{{Name: "source", Value: "sha256:source"}, {Name: "chunk-plan", Value: "sha256:plan"}},
|
dependencies: []pipeline.CheckpointFingerprint{{Name: "source", Value: "sha256:source"}, {Name: "chunk-plan", Value: "sha256:plan"}},
|
||||||
warnings: []contracts.Warning{{Scope: "extract", ReasonCode: "partial", Message: "partial output"}},
|
|
||||||
rejected: []contracts.RejectedOutput{{Stage: "extract", LaneID: "lane-a", ModuleKey: "extract-module", ChunkID: "chunk-a", ValidatorName: "source_refs", ReasonCode: "invalid_source", Message: "source reference is invalid", AttemptCount: 1}},
|
rejected: []contracts.RejectedOutput{{Stage: "extract", LaneID: "lane-a", ModuleKey: "extract-module", ChunkID: "chunk-a", ValidatorName: "source_refs", ReasonCode: "invalid_source", Message: "source reference is invalid", AttemptCount: 1}},
|
||||||
}
|
}
|
||||||
if err := recorder.SourceSucceeded("source-module", &fixture.doc); err != nil {
|
if err := recorder.SourceSucceeded("source-module", &fixture.doc); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := recorder.ExtractSucceeded("lane-a", "extract-module", fixture.dependencies, []pipeline.CheckpointArtifact{fixture.extract}, fixture.rejected, fixture.warnings); err != nil {
|
if err := recorder.ExtractSucceeded("lane-a", "extract-module", fixture.dependencies, []pipeline.CheckpointArtifact{fixture.extract}, fixture.rejected); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mergeWarnings := []contracts.Warning{{Scope: "merge", ReasonCode: "review", Message: "review manually"}}
|
if err := recorder.MergeSucceeded("lane-a", "merge-module", fixture.dependencies, fixture.merge); err != nil {
|
||||||
if err := recorder.MergeSucceeded("lane-a", "merge-module", fixture.dependencies, fixture.merge, mergeWarnings); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := recorder.NormalizeSucceeded("lane-a", "normalize-module", fixture.dependencies, fixture.normalize, mergeWarnings); err != nil {
|
if err := recorder.NormalizeSucceeded("lane-a", "normalize-module", fixture.dependencies, fixture.normalize); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
fixture.loader, err = NewFilesystemLoader(root, identity)
|
fixture.loader, err = NewFilesystemLoader(root, identity)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (l *FilesystemLoader) ExtractForStep(stepID, laneID, moduleKey string, depe
|
|||||||
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(outputs)) {
|
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(outputs)) {
|
||||||
return pipeline.ExtractCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
return pipeline.ExtractCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
||||||
}
|
}
|
||||||
return pipeline.ExtractCheckpoint{Outputs: outputs, Rejected: cloneRejectedOutputs(payload.Rejected), Warnings: cloneWarnings(payload.Warnings)}, reusedDecision()
|
return pipeline.ExtractCheckpoint{Outputs: outputs, Rejected: cloneRejectedOutputs(payload.Rejected)}, reusedDecision()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *FilesystemLoader) Merge(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint) (pipeline.MergeCheckpoint, pipeline.CheckpointDecision) {
|
func (l *FilesystemLoader) Merge(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint) (pipeline.MergeCheckpoint, pipeline.CheckpointDecision) {
|
||||||
@@ -112,7 +112,7 @@ func (l *FilesystemLoader) MergeForStep(stepID, laneID, moduleKey string, depend
|
|||||||
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(values)) {
|
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(values)) {
|
||||||
return pipeline.MergeCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
return pipeline.MergeCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
||||||
}
|
}
|
||||||
return pipeline.MergeCheckpoint{Output: values[0], Warnings: cloneWarnings(payload.Warnings)}, reusedDecision()
|
return pipeline.MergeCheckpoint{Output: values[0]}, reusedDecision()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *FilesystemLoader) Normalize(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint) (pipeline.NormalizeCheckpoint, pipeline.CheckpointDecision) {
|
func (l *FilesystemLoader) Normalize(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint) (pipeline.NormalizeCheckpoint, pipeline.CheckpointDecision) {
|
||||||
@@ -138,7 +138,7 @@ func (l *FilesystemLoader) NormalizeForStep(stepID, laneID, moduleKey string, de
|
|||||||
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(values)) {
|
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(values)) {
|
||||||
return pipeline.NormalizeCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
return pipeline.NormalizeCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
||||||
}
|
}
|
||||||
return pipeline.NormalizeCheckpoint{Output: values[0], Warnings: cloneWarnings(payload.Warnings)}, reusedDecision()
|
return pipeline.NormalizeCheckpoint{Output: values[0]}, reusedDecision()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *FilesystemLoader) AcceptedNormalize(stepID, laneID, moduleKey string) (pipeline.NormalizeCheckpoint, pipeline.CheckpointDecision) {
|
func (l *FilesystemLoader) AcceptedNormalize(stepID, laneID, moduleKey string) (pipeline.NormalizeCheckpoint, pipeline.CheckpointDecision) {
|
||||||
@@ -163,7 +163,7 @@ func (l *FilesystemLoader) AcceptedNormalize(stepID, laneID, moduleKey string) (
|
|||||||
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(values)) {
|
if !fingerprintsEqual(checkpointToPipelineFingerprints(manifest.OutputDigests), artifactOutputDigests(values)) {
|
||||||
return pipeline.NormalizeCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
return pipeline.NormalizeCheckpoint{}, decision(pipeline.CheckpointDecisionExecuted, pipeline.CheckpointReasonArtifactDigestMismatch)
|
||||||
}
|
}
|
||||||
return pipeline.NormalizeCheckpoint{Output: values[0], Warnings: cloneWarnings(payload.Warnings)}, decision(pipeline.CheckpointDecisionReused, pipeline.CheckpointReasonAcceptedArtifactReused)
|
return pipeline.NormalizeCheckpoint{Output: values[0]}, decision(pipeline.CheckpointDecisionReused, pipeline.CheckpointReasonAcceptedArtifactReused)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *FilesystemLoader) validateAcceptedNormalizeManifest(manifest StageManifest, stepID, laneID, moduleKey string) pipeline.CheckpointDecision {
|
func (l *FilesystemLoader) validateAcceptedNormalizeManifest(manifest StageManifest, stepID, laneID, moduleKey string) pipeline.CheckpointDecision {
|
||||||
|
|||||||
@@ -81,18 +81,18 @@ func (r *FilesystemRecorder) ExtractRunningForStep(stepID, laneID string, module
|
|||||||
return r.writeManifest(laneManifestPath("extract", stepID, laneID), ExtractLaneManifest{StageManifest: manifest})
|
return r.writeManifest(laneManifestPath("extract", stepID, laneID), ExtractLaneManifest{StageManifest: manifest})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilesystemRecorder) ExtractSucceeded(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, outputs []pipeline.CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
func (r *FilesystemRecorder) ExtractSucceeded(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, outputs []pipeline.CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||||
return r.ExtractSucceededForStep("", laneID, moduleKey, dependencies, outputs, rejected, warnings)
|
return r.ExtractSucceededForStep("", laneID, moduleKey, dependencies, outputs, rejected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilesystemRecorder) ExtractSucceededForStep(stepID, laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, outputs []pipeline.CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
func (r *FilesystemRecorder) ExtractSucceededForStep(stepID, laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, outputs []pipeline.CheckpointArtifact, rejected []contracts.RejectedOutput) error {
|
||||||
payload := artifactExtractEnvelope{Outputs: artifactCheckpointEnvelopes(outputs), Rejected: cloneRejectedOutputs(rejected), Warnings: cloneWarnings(warnings)}
|
payload := artifactExtractEnvelope{Outputs: artifactCheckpointEnvelopes(outputs), Rejected: cloneRejectedOutputs(rejected)}
|
||||||
if err := r.writePayload(lanePayloadPath("extract", stepID, laneID, "outputs.json"), payload); err != nil {
|
if err := r.writePayload(lanePayloadPath("extract", stepID, laneID, "outputs.json"), payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
manifest := r.laneManifest(StageExtract, statusForRejected(rejected), stepID, laneID, moduleKey, dependencies)
|
manifest := r.laneManifest(StageExtract, statusForRejected(rejected), stepID, laneID, moduleKey, dependencies)
|
||||||
manifest.OutputDigests = checkpointFingerprints(artifactOutputDigests(outputs))
|
manifest.OutputDigests = checkpointFingerprints(artifactOutputDigests(outputs))
|
||||||
manifest.ValidationStatus = validationStatusString(warnings, rejected)
|
manifest.ValidationStatus = validationStatusString(rejected)
|
||||||
manifest.Rejections = rejectionSummaries(rejected)
|
manifest.Rejections = rejectionSummaries(rejected)
|
||||||
manifest.CompletedAt = timePtr(r.timestamp())
|
manifest.CompletedAt = timePtr(r.timestamp())
|
||||||
return r.writeManifest(laneManifestPath("extract", stepID, laneID), ExtractLaneManifest{StageManifest: manifest, ChunkCount: len(outputs) + len(rejected), OutputCount: len(outputs)})
|
return r.writeManifest(laneManifestPath("extract", stepID, laneID), ExtractLaneManifest{StageManifest: manifest, ChunkCount: len(outputs) + len(rejected), OutputCount: len(outputs)})
|
||||||
@@ -119,17 +119,17 @@ func (r *FilesystemRecorder) MergeRunningForStep(stepID, laneID string, moduleKe
|
|||||||
return r.writeManifest(laneManifestPath("merge", stepID, laneID), MergeLaneManifest{StageManifest: manifest})
|
return r.writeManifest(laneManifestPath("merge", stepID, laneID), MergeLaneManifest{StageManifest: manifest})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilesystemRecorder) MergeSucceeded(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact, warnings []contracts.Warning) error {
|
func (r *FilesystemRecorder) MergeSucceeded(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact) error {
|
||||||
return r.MergeSucceededForStep("", laneID, moduleKey, dependencies, output, warnings)
|
return r.MergeSucceededForStep("", laneID, moduleKey, dependencies, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilesystemRecorder) MergeSucceededForStep(stepID, laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact, warnings []contracts.Warning) error {
|
func (r *FilesystemRecorder) MergeSucceededForStep(stepID, laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact) error {
|
||||||
if err := r.writePayload(lanePayloadPath("merge", stepID, laneID, "output.json"), artifactSingleEnvelope{Output: artifactCheckpointEnvelopeFromOutput(output), Warnings: cloneWarnings(warnings)}); err != nil {
|
if err := r.writePayload(lanePayloadPath("merge", stepID, laneID, "output.json"), artifactSingleEnvelope{Output: artifactCheckpointEnvelopeFromOutput(output)}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
manifest := r.laneManifest(StageMerge, StatusSucceeded, stepID, laneID, moduleKey, dependencies)
|
manifest := r.laneManifest(StageMerge, StatusSucceeded, stepID, laneID, moduleKey, dependencies)
|
||||||
manifest.OutputDigests = checkpointFingerprints(artifactOutputDigests([]pipeline.CheckpointArtifact{output}))
|
manifest.OutputDigests = checkpointFingerprints(artifactOutputDigests([]pipeline.CheckpointArtifact{output}))
|
||||||
manifest.ValidationStatus = validationStatusString(warnings, nil)
|
manifest.ValidationStatus = validationStatusString(nil)
|
||||||
manifest.CompletedAt = timePtr(r.timestamp())
|
manifest.CompletedAt = timePtr(r.timestamp())
|
||||||
return r.writeManifest(laneManifestPath("merge", stepID, laneID), MergeLaneManifest{StageManifest: manifest, InputCount: len(dependencies)})
|
return r.writeManifest(laneManifestPath("merge", stepID, laneID), MergeLaneManifest{StageManifest: manifest, InputCount: len(dependencies)})
|
||||||
}
|
}
|
||||||
@@ -167,17 +167,17 @@ func (r *FilesystemRecorder) NormalizeRunningForStep(stepID, laneID string, modu
|
|||||||
return r.writeManifest(laneManifestPath("normalize", stepID, laneID), NormalizeLaneManifest{StageManifest: manifest})
|
return r.writeManifest(laneManifestPath("normalize", stepID, laneID), NormalizeLaneManifest{StageManifest: manifest})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilesystemRecorder) NormalizeSucceeded(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact, warnings []contracts.Warning) error {
|
func (r *FilesystemRecorder) NormalizeSucceeded(laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact) error {
|
||||||
return r.NormalizeSucceededForStep("", laneID, moduleKey, dependencies, output, warnings)
|
return r.NormalizeSucceededForStep("", laneID, moduleKey, dependencies, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilesystemRecorder) NormalizeSucceededForStep(stepID, laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact, warnings []contracts.Warning) error {
|
func (r *FilesystemRecorder) NormalizeSucceededForStep(stepID, laneID, moduleKey string, dependencies []pipeline.CheckpointFingerprint, output pipeline.CheckpointArtifact) error {
|
||||||
if err := r.writePayload(lanePayloadPath("normalize", stepID, laneID, "output.json"), artifactSingleEnvelope{Output: artifactCheckpointEnvelopeFromOutput(output), Warnings: cloneWarnings(warnings)}); err != nil {
|
if err := r.writePayload(lanePayloadPath("normalize", stepID, laneID, "output.json"), artifactSingleEnvelope{Output: artifactCheckpointEnvelopeFromOutput(output)}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
manifest := r.laneManifest(StageNormalize, StatusSucceeded, stepID, laneID, moduleKey, dependencies)
|
manifest := r.laneManifest(StageNormalize, StatusSucceeded, stepID, laneID, moduleKey, dependencies)
|
||||||
manifest.OutputDigests = checkpointFingerprints(artifactOutputDigests([]pipeline.CheckpointArtifact{output}))
|
manifest.OutputDigests = checkpointFingerprints(artifactOutputDigests([]pipeline.CheckpointArtifact{output}))
|
||||||
manifest.ValidationStatus = validationStatusString(warnings, nil)
|
manifest.ValidationStatus = validationStatusString(nil)
|
||||||
manifest.CompletedAt = timePtr(r.timestamp())
|
manifest.CompletedAt = timePtr(r.timestamp())
|
||||||
return r.writeManifest(laneManifestPath("normalize", stepID, laneID), NormalizeLaneManifest{StageManifest: manifest, InputCount: len(dependencies)})
|
return r.writeManifest(laneManifestPath("normalize", stepID, laneID), NormalizeLaneManifest{StageManifest: manifest, InputCount: len(dependencies)})
|
||||||
}
|
}
|
||||||
@@ -249,11 +249,10 @@ type sourceDocumentEnvelope struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type binaryEnvelope struct {
|
type binaryEnvelope struct {
|
||||||
ContentBase64 string `json:"content_base64,omitempty"`
|
ContentBase64 string `json:"content_base64,omitempty"`
|
||||||
ContentDigest string `json:"content_digest,omitempty"`
|
ContentDigest string `json:"content_digest,omitempty"`
|
||||||
MediaType string `json:"media_type,omitempty"`
|
MediaType string `json:"media_type,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type artifactCheckpointEnvelope struct {
|
type artifactCheckpointEnvelope struct {
|
||||||
@@ -272,17 +271,15 @@ type artifactCheckpointEnvelope struct {
|
|||||||
type artifactExtractEnvelope struct {
|
type artifactExtractEnvelope struct {
|
||||||
Outputs []artifactCheckpointEnvelope `json:"outputs"`
|
Outputs []artifactCheckpointEnvelope `json:"outputs"`
|
||||||
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
}
|
||||||
type artifactSingleEnvelope struct {
|
type artifactSingleEnvelope struct {
|
||||||
Output artifactCheckpointEnvelope `json:"output"`
|
Output artifactCheckpointEnvelope `json:"output"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func artifactCheckpointEnvelopeFromOutput(output pipeline.CheckpointArtifact) artifactCheckpointEnvelope {
|
func artifactCheckpointEnvelopeFromOutput(output pipeline.CheckpointArtifact) artifactCheckpointEnvelope {
|
||||||
schema := contracts.CloneArtifactSchema(output.Artifact.Schema)
|
schema := contracts.CloneArtifactSchema(output.Artifact.Schema)
|
||||||
schema.JSONSchema = nil
|
schema.JSONSchema = nil
|
||||||
return artifactCheckpointEnvelope{LaneID: output.LaneID, ModuleKey: output.ModuleKey, SourceID: output.SourceID, ChunkID: output.ChunkID, ChunkIndex: output.ChunkIndex, ChunkRef: output.ChunkRef, Kind: output.Artifact.Kind, Schema: schema, SchemaDigest: output.SchemaDigest, Content: binaryEnvelopeFromContent(output.Artifact.Content, output.Artifact.MediaType, output.Artifact.Metadata, nil), Diagnostics: cloneCheckpointDiagnostics(output.Diagnostics)}
|
return artifactCheckpointEnvelope{LaneID: output.LaneID, ModuleKey: output.ModuleKey, SourceID: output.SourceID, ChunkID: output.ChunkID, ChunkIndex: output.ChunkIndex, ChunkRef: output.ChunkRef, Kind: output.Artifact.Kind, Schema: schema, SchemaDigest: output.SchemaDigest, Content: binaryEnvelopeFromContent(output.Artifact.Content, output.Artifact.MediaType, output.Artifact.Metadata), Diagnostics: cloneCheckpointDiagnostics(output.Diagnostics)}
|
||||||
}
|
}
|
||||||
func artifactCheckpointEnvelopes(outputs []pipeline.CheckpointArtifact) []artifactCheckpointEnvelope {
|
func artifactCheckpointEnvelopes(outputs []pipeline.CheckpointArtifact) []artifactCheckpointEnvelope {
|
||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
@@ -316,13 +313,12 @@ func artifactOutputDigests(outputs []pipeline.CheckpointArtifact) []pipeline.Che
|
|||||||
return normalizeFingerprints(values)
|
return normalizeFingerprints(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func binaryEnvelopeFromContent(content []byte, mediaType string, metadata map[string]any, warnings []contracts.Warning) binaryEnvelope {
|
func binaryEnvelopeFromContent(content []byte, mediaType string, metadata map[string]any) binaryEnvelope {
|
||||||
return binaryEnvelope{
|
return binaryEnvelope{
|
||||||
ContentBase64: base64.StdEncoding.EncodeToString(content),
|
ContentBase64: base64.StdEncoding.EncodeToString(content),
|
||||||
ContentDigest: contentDigest(content),
|
ContentDigest: contentDigest(content),
|
||||||
MediaType: mediaType,
|
MediaType: mediaType,
|
||||||
Metadata: cloneMetadata(metadata),
|
Metadata: cloneMetadata(metadata),
|
||||||
Warnings: cloneWarnings(warnings),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,13 +345,6 @@ func cloneSourceUnits(units []source.SourceUnit) []source.SourceUnit {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneWarnings(warnings []contracts.Warning) []contracts.Warning {
|
|
||||||
if len(warnings) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return append([]contracts.Warning(nil), warnings...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneRejectedOutputs(rejected []contracts.RejectedOutput) []contracts.RejectedOutput {
|
func cloneRejectedOutputs(rejected []contracts.RejectedOutput) []contracts.RejectedOutput {
|
||||||
if len(rejected) == 0 {
|
if len(rejected) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -476,13 +465,10 @@ func statusForRejected(rejected []contracts.RejectedOutput) StageStatus {
|
|||||||
return StatusSucceeded
|
return StatusSucceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
func validationStatusString(warnings []contracts.Warning, rejected []contracts.RejectedOutput) string {
|
func validationStatusString(rejected []contracts.RejectedOutput) string {
|
||||||
if len(rejected) > 0 {
|
if len(rejected) > 0 {
|
||||||
return "rejected"
|
return "rejected"
|
||||||
}
|
}
|
||||||
if len(warnings) > 0 {
|
|
||||||
return "approved_with_warnings"
|
|
||||||
}
|
|
||||||
return "approved"
|
return "approved"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func TestRootBasedRecorderOutputIsReusable(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := recorder.ExtractSucceeded("lane", "module", nil, nil, nil, nil); err != nil {
|
if err := recorder.ExtractSucceeded("lane", "module", nil, nil, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
loader, err := NewFilesystemLoader(root, identity)
|
loader, err := NewFilesystemLoader(root, identity)
|
||||||
@@ -44,7 +44,7 @@ func TestStepAwareRecorderAndLoaderIsolateLaneState(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := recorder.(pipeline.StepCheckpointRecorder).ExtractSucceededForStep("step-a", "lane", "module", nil, nil, nil, nil); err != nil {
|
if err := recorder.(pipeline.StepCheckpointRecorder).ExtractSucceededForStep("step-a", "lane", "module", nil, nil, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
loader, err := NewFilesystemLoader(root, identity)
|
loader, err := NewFilesystemLoader(root, identity)
|
||||||
@@ -87,7 +87,7 @@ func TestStepAwareCheckpointPreservesDistinctDotIdentities(t *testing.T) {
|
|||||||
LaneID: "lane", ModuleKey: "normalize-module", SourceID: "source", ChunkID: "chunk", ChunkRef: source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 1}, SchemaDigest: "sha256:schema",
|
LaneID: "lane", ModuleKey: "normalize-module", SourceID: "source", ChunkID: "chunk", ChunkRef: source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 1}, SchemaDigest: "sha256:schema",
|
||||||
Artifact: contracts.SerializedArtifact{Kind: "kind", Schema: contracts.ArtifactSchema{ID: "schema", Name: "Schema", Version: "1"}, MediaType: "application/json", Content: []byte(test.content)},
|
Artifact: contracts.SerializedArtifact{Kind: "kind", Schema: contracts.ArtifactSchema{ID: "schema", Name: "Schema", Version: "1"}, MediaType: "application/json", Content: []byte(test.content)},
|
||||||
}
|
}
|
||||||
if err := stepRecorder.NormalizeSucceededForStep(test.stepID, "lane", "normalize-module", nil, artifact, nil); err != nil {
|
if err := stepRecorder.NormalizeSucceededForStep(test.stepID, "lane", "normalize-module", nil, artifact); err != nil {
|
||||||
t.Fatalf("record %q: %v", test.stepID, err)
|
t.Fatalf("record %q: %v", test.stepID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,7 +498,6 @@ func testRecord(t *testing.T, value int) pipeline.ChunkPlanRecord {
|
|||||||
References: []artifacts.ReferenceProvenance{{Stage: "chunk", SlotName: "guide", OriginType: "file", OriginURI: "file:///guide.txt", Digest: "sha256:reference"}},
|
References: []artifacts.ReferenceProvenance{{Stage: "chunk", SlotName: "guide", OriginType: "file", OriginURI: "file:///guide.txt", Digest: "sha256:reference"}},
|
||||||
Metadata: map[string]any{"prompt_id": "test/prompt", "enabled": true},
|
Metadata: map[string]any{"prompt_id": "test/prompt", "enabled": true},
|
||||||
},
|
},
|
||||||
Warnings: []contracts.Warning{{Scope: "chunk/test", ReasonCode: "observed", Message: "warning"}},
|
|
||||||
Diagnostics: []contracts.ProducerDiagnostic{{
|
Diagnostics: []contracts.ProducerDiagnostic{{
|
||||||
Disposition: contracts.DiagnosticDispositionWarning,
|
Disposition: contracts.DiagnosticDispositionWarning,
|
||||||
Category: contracts.DiagnosticCategoryConfiguration,
|
Category: contracts.DiagnosticCategoryConfiguration,
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ type ChunkRequest struct {
|
|||||||
|
|
||||||
type ChunkPlanResult struct {
|
type ChunkPlanResult struct {
|
||||||
Plan source.ChunkPlan `json:"plan"`
|
Plan source.ChunkPlan `json:"plan"`
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
Diagnostics []ProducerDiagnostic `json:"diagnostics,omitempty"`
|
Diagnostics []ProducerDiagnostic `json:"diagnostics,omitempty"`
|
||||||
ModelCandidate *ModelCandidate `json:"-"`
|
ModelCandidate *ModelCandidate `json:"-"`
|
||||||
}
|
}
|
||||||
@@ -289,21 +288,13 @@ type ValidationResult struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
CorrectionGuidance string `json:"-"`
|
CorrectionGuidance string `json:"-"`
|
||||||
DiagnosticArtifactPath string `json:"diagnostic_artifact_path,omitempty"`
|
DiagnosticArtifactPath string `json:"diagnostic_artifact_path,omitempty"`
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
Diagnostics []ProducerDiagnostic `json:"diagnostics,omitempty"`
|
Diagnostics []ProducerDiagnostic `json:"diagnostics,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Warning struct {
|
|
||||||
Scope string `json:"scope,omitempty"`
|
|
||||||
ReasonCode string `json:"reason_code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputRequest struct {
|
type OutputRequest struct {
|
||||||
Manifest artifacts.RunManifest `json:"manifest"`
|
Manifest artifacts.RunManifest `json:"manifest"`
|
||||||
NormalizeOutputs []SerializedOutput `json:"normalize_outputs,omitempty"`
|
NormalizeOutputs []SerializedOutput `json:"normalize_outputs,omitempty"`
|
||||||
Rejected []RejectedOutput `json:"rejected,omitempty"`
|
Rejected []RejectedOutput `json:"rejected,omitempty"`
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
Diagnostics DiagnosticCollection `json:"diagnostics,omitempty"`
|
Diagnostics DiagnosticCollection `json:"diagnostics,omitempty"`
|
||||||
LLMProfile string `json:"llm_profile,omitempty"`
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
StructuredOutputRepairAttempts *int `json:"structured_output_repair_attempts,omitempty"`
|
StructuredOutputRepairAttempts *int `json:"structured_output_repair_attempts,omitempty"`
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ type TypedExtractionRequest struct {
|
|||||||
|
|
||||||
type TypedExtractionResult[T any] struct {
|
type TypedExtractionResult[T any] struct {
|
||||||
Value T
|
Value T
|
||||||
Warnings []Warning
|
|
||||||
Diagnostics []ProducerDiagnostic
|
Diagnostics []ProducerDiagnostic
|
||||||
ModelCandidate *ModelCandidate
|
ModelCandidate *ModelCandidate
|
||||||
}
|
}
|
||||||
@@ -74,7 +73,6 @@ type TypedMergeRequest[T any] struct {
|
|||||||
|
|
||||||
type TypedMergeResult[T any] struct {
|
type TypedMergeResult[T any] struct {
|
||||||
Value T
|
Value T
|
||||||
Warnings []Warning
|
|
||||||
Diagnostics []ProducerDiagnostic
|
Diagnostics []ProducerDiagnostic
|
||||||
ModelCandidate *ModelCandidate
|
ModelCandidate *ModelCandidate
|
||||||
}
|
}
|
||||||
@@ -99,7 +97,6 @@ type TypedNormalizeRequest[T any] struct {
|
|||||||
|
|
||||||
type TypedNormalizeResult[T any] struct {
|
type TypedNormalizeResult[T any] struct {
|
||||||
Value T
|
Value T
|
||||||
Warnings []Warning
|
|
||||||
Diagnostics []ProducerDiagnostic
|
Diagnostics []ProducerDiagnostic
|
||||||
Retry *NormalizeRetry
|
Retry *NormalizeRetry
|
||||||
ModelCandidate *ModelCandidate
|
ModelCandidate *ModelCandidate
|
||||||
@@ -117,7 +114,6 @@ const (
|
|||||||
type NormalizeRetry struct {
|
type NormalizeRetry struct {
|
||||||
ReasonCode string
|
ReasonCode string
|
||||||
Message string
|
Message string
|
||||||
FallbackWarnings []Warning
|
|
||||||
FallbackDiagnostics []ProducerDiagnostic
|
FallbackDiagnostics []ProducerDiagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ type CheckpointRecorder interface {
|
|||||||
SourceSucceeded(moduleKey string, doc *source.SourceDocument) error
|
SourceSucceeded(moduleKey string, doc *source.SourceDocument) error
|
||||||
SourceFailed(moduleKey string, err error) error
|
SourceFailed(moduleKey string, err error) error
|
||||||
ExtractRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) 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
|
ExtractFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||||
MergeRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) 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
|
MergeRejected(laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||||
MergeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
MergeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||||
NormalizeRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) 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
|
NormalizeRejected(laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||||
NormalizeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) 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.
|
// remain available for callers that do not have step context.
|
||||||
type StepCheckpointRecorder interface {
|
type StepCheckpointRecorder interface {
|
||||||
ExtractRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
|
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
|
ExtractFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||||
MergeRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) 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
|
MergeRejectedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||||
MergeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
MergeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||||
NormalizeRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) 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
|
NormalizeRejectedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
|
||||||
NormalizeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
NormalizeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
|
||||||
}
|
}
|
||||||
@@ -293,16 +293,13 @@ type CheckpointDiagnostic struct {
|
|||||||
type ExtractCheckpoint struct {
|
type ExtractCheckpoint struct {
|
||||||
Outputs []CheckpointArtifact
|
Outputs []CheckpointArtifact
|
||||||
Rejected []contracts.RejectedOutput
|
Rejected []contracts.RejectedOutput
|
||||||
Warnings []contracts.Warning
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MergeCheckpoint struct {
|
type MergeCheckpoint struct {
|
||||||
Output CheckpointArtifact
|
Output CheckpointArtifact
|
||||||
Warnings []contracts.Warning
|
|
||||||
}
|
}
|
||||||
type NormalizeCheckpoint struct {
|
type NormalizeCheckpoint struct {
|
||||||
Output CheckpointArtifact
|
Output CheckpointArtifact
|
||||||
Warnings []contracts.Warning
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckpointLoader interface {
|
type CheckpointLoader interface {
|
||||||
@@ -335,14 +332,14 @@ func (noopCheckpointRecorder) SourceFailed(string, error) error
|
|||||||
func (noopCheckpointRecorder) ExtractRunning(string, string, []CheckpointFingerprint) error {
|
func (noopCheckpointRecorder) ExtractRunning(string, string, []CheckpointFingerprint) error {
|
||||||
return nil
|
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
|
return nil
|
||||||
}
|
}
|
||||||
func (noopCheckpointRecorder) ExtractFailed(string, string, []CheckpointFingerprint, error) error {
|
func (noopCheckpointRecorder) ExtractFailed(string, string, []CheckpointFingerprint, error) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (noopCheckpointRecorder) MergeRunning(string, string, []CheckpointFingerprint) 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
|
return nil
|
||||||
}
|
}
|
||||||
func (noopCheckpointRecorder) MergeRejected(string, string, []CheckpointFingerprint, contracts.RejectedOutput) error {
|
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 {
|
func (noopCheckpointRecorder) NormalizeRunning(string, string, []CheckpointFingerprint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (noopCheckpointRecorder) NormalizeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact, []contracts.Warning) error {
|
func (noopCheckpointRecorder) NormalizeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (noopCheckpointRecorder) NormalizeRejected(string, string, []CheckpointFingerprint, contracts.RejectedOutput) error {
|
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)
|
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 {
|
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 {
|
func checkpointExtractFailed(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, err error) error {
|
||||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||||
@@ -405,11 +402,11 @@ func checkpointMergeRunning(recorder CheckpointRecorder, stepID, laneID, moduleK
|
|||||||
}
|
}
|
||||||
return recorder.MergeRunning(laneID, moduleKey, deps)
|
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 {
|
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 {
|
func checkpointMergeRejected(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||||
@@ -429,11 +426,11 @@ func checkpointNormalizeRunning(recorder CheckpointRecorder, stepID, laneID, mod
|
|||||||
}
|
}
|
||||||
return recorder.NormalizeRunning(laneID, moduleKey, deps)
|
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 {
|
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 {
|
func checkpointNormalizeRejected(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||||
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ type ChunkPlanRecord struct {
|
|||||||
PlanDigest string `json:"plan_digest"`
|
PlanDigest string `json:"plan_digest"`
|
||||||
Plan source.ChunkPlan `json:"plan"`
|
Plan source.ChunkPlan `json:"plan"`
|
||||||
Producer ChunkPlanProducer `json:"producer"`
|
Producer ChunkPlanProducer `json:"producer"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
Diagnostics []contracts.ProducerDiagnostic `json:"diagnostics,omitempty"`
|
Diagnostics []contracts.ProducerDiagnostic `json:"diagnostics,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,10 @@ type debugTimedEnvelope struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type debugBinaryEnvelope struct {
|
type debugBinaryEnvelope struct {
|
||||||
ContentBase64 string `json:"content_base64,omitempty"`
|
ContentBase64 string `json:"content_base64,omitempty"`
|
||||||
ContentDigest string `json:"content_digest,omitempty"`
|
ContentDigest string `json:"content_digest,omitempty"`
|
||||||
MediaType string `json:"media_type,omitempty"`
|
MediaType string `json:"media_type,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type debugSourceInput struct {
|
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)
|
content = redactSecretBytes(content)
|
||||||
return debugBinaryEnvelope{
|
return debugBinaryEnvelope{
|
||||||
ContentBase64: base64.StdEncoding.EncodeToString(content),
|
ContentBase64: base64.StdEncoding.EncodeToString(content),
|
||||||
ContentDigest: debugContentDigest(content),
|
ContentDigest: debugContentDigest(content),
|
||||||
MediaType: mediaType,
|
MediaType: mediaType,
|
||||||
Metadata: redactSensitiveMap(metadata),
|
Metadata: redactSensitiveMap(metadata),
|
||||||
Warnings: cloneWarnings(warnings),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,20 +729,9 @@ func debugValidationResultEnvelope(result contracts.ValidationResult) contracts.
|
|||||||
result.Message = string(redactSecretBytes([]byte(result.Message)))
|
result.Message = string(redactSecretBytes([]byte(result.Message)))
|
||||||
result.CorrectionGuidance = ""
|
result.CorrectionGuidance = ""
|
||||||
result.DiagnosticArtifactPath = string(redactSecretBytes([]byte(result.DiagnosticArtifactPath)))
|
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
|
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 {
|
func debugRejectedOutputEnvelope(rejected contracts.RejectedOutput) contracts.RejectedOutput {
|
||||||
rejected.Message = string(redactSecretBytes([]byte(rejected.Message)))
|
rejected.Message = string(redactSecretBytes([]byte(rejected.Message)))
|
||||||
rejected.DiagnosticArtifactPath = string(redactSecretBytes([]byte(rejected.DiagnosticArtifactPath)))
|
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
|
cloned := contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: groups}).Groups
|
||||||
output.diagnosticGroups = append(output.diagnosticGroups, cloned...)
|
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 {
|
func finalizeDiagnostics(output *RunOutput) error {
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ func TestTerminalDiagnosticGroupsPreserveValidatorOrigin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppendDiagnosticGroupsProjectsOnlyMissingLegacyWarnings(t *testing.T) {
|
func TestAppendDiagnosticGroupsRetainsOnlyStructuredDiagnostics(t *testing.T) {
|
||||||
output := RunOutput{Warnings: []contracts.Warning{{Scope: "spells[0]", ReasonCode: "source_unrelated", Message: "not found"}}}
|
output := RunOutput{}
|
||||||
appendDiagnosticGroups(&output, []contracts.DiagnosticGroup{{
|
appendDiagnosticGroups(&output, []contracts.DiagnosticGroup{{
|
||||||
Disposition: contracts.DiagnosticDispositionAdvisory,
|
Disposition: contracts.DiagnosticDispositionAdvisory,
|
||||||
Category: contracts.DiagnosticCategoryDataQuality,
|
Category: contracts.DiagnosticCategoryDataQuality,
|
||||||
@@ -56,9 +56,6 @@ func TestAppendDiagnosticGroupsProjectsOnlyMissingLegacyWarnings(t *testing.T) {
|
|||||||
OccurrenceCount: 2,
|
OccurrenceCount: 2,
|
||||||
Samples: []contracts.DiagnosticSample{{Scope: "spells[0]", Message: "not found"}, {Scope: "spells[1]", Message: "also not found"}},
|
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 {
|
if err := finalizeDiagnostics(&output); err != nil {
|
||||||
t.Fatalf("finalizeDiagnostics() error = %v", err)
|
t.Fatalf("finalizeDiagnostics() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func RegisterExtractorBuilder[T any](registry *ExtractorRegistry, spec ModuleSpe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return erasedTypedResult{}, fmt.Errorf("clone extraction model candidate: %w", err)
|
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 {
|
if registry.typedEntries == nil {
|
||||||
registry.typedEntries = map[string]typedExtractorEntry{}
|
registry.typedEntries = map[string]typedExtractorEntry{}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func RegisterMergerBuilder[T any](registry *MergerRegistry, spec ModuleSpec, val
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return erasedTypedResult{}, fmt.Errorf("clone merge model candidate: %w", err)
|
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
|
return nil
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func RegisterNormalizerBuilder[T any](registry *NormalizerRegistry, spec ModuleS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return erasedTypedResult{}, fmt.Errorf("clone normalize model candidate: %w", err)
|
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
|
return nil
|
||||||
@@ -101,7 +101,6 @@ func cloneNormalizeRetry(retry *contracts.NormalizeRetry) *contracts.NormalizeRe
|
|||||||
return &contracts.NormalizeRetry{
|
return &contracts.NormalizeRetry{
|
||||||
ReasonCode: retry.ReasonCode,
|
ReasonCode: retry.ReasonCode,
|
||||||
Message: retry.Message,
|
Message: retry.Message,
|
||||||
FallbackWarnings: cloneWarnings(retry.FallbackWarnings),
|
|
||||||
FallbackDiagnostics: contracts.CloneProducerDiagnostics(retry.FallbackDiagnostics),
|
FallbackDiagnostics: contracts.CloneProducerDiagnostics(retry.FallbackDiagnostics),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,26 +8,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type retryingNotesNormalizer struct {
|
type retryingNotesNormalizer struct {
|
||||||
warnings []contracts.Warning
|
retry *contracts.NormalizeRetry
|
||||||
retry *contracts.NormalizeRetry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (retryingNotesNormalizer) Key() string { return "test/retry-normalize" }
|
func (retryingNotesNormalizer) Key() string { return "test/retry-normalize" }
|
||||||
func (retryingNotesNormalizer) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
func (retryingNotesNormalizer) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
||||||
func (n retryingNotesNormalizer) Normalize(_ context.Context, req contracts.TypedNormalizeRequest[codecNotes]) (contracts.TypedNormalizeResult[codecNotes], error) {
|
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) {
|
func TestNormalizerRegistryErasureClonesRetryDirective(t *testing.T) {
|
||||||
warnings := []contracts.Warning{{Scope: "attempt", ReasonCode: "ordinary", Message: "ordinary warning"}}
|
|
||||||
retry := &contracts.NormalizeRetry{
|
retry := &contracts.NormalizeRetry{
|
||||||
ReasonCode: "retryable",
|
ReasonCode: "retryable",
|
||||||
Message: "safe fallback available",
|
Message: "safe fallback available",
|
||||||
FallbackWarnings: []contracts.Warning{{Scope: "fallback", ReasonCode: "omitted", Message: "fallback warning"}},
|
|
||||||
}
|
}
|
||||||
registry := NewNormalizerRegistry()
|
registry := NewNormalizerRegistry()
|
||||||
if err := RegisterNormalizer(registry, ModuleSpec{Key: "test/retry-normalize", Stage: StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, ArtifactKind: "test/notes"}, func() (contracts.Normalizer[codecNotes], error) {
|
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 {
|
}); err != nil {
|
||||||
t.Fatalf("RegisterNormalizer() error = %v", err)
|
t.Fatalf("RegisterNormalizer() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -43,10 +40,8 @@ func TestNormalizerRegistryErasureClonesRetryDirective(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("normalize() error = %v", err)
|
t.Fatalf("normalize() error = %v", err)
|
||||||
}
|
}
|
||||||
warnings[0].Message = "mutated"
|
|
||||||
retry.Message = "mutated"
|
retry.Message = "mutated"
|
||||||
retry.FallbackWarnings[0].Message = "mutated"
|
if result.Retry == nil || result.Retry.Message != "safe fallback available" {
|
||||||
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 retry data", result)
|
||||||
t.Fatalf("erased retry result = %#v, want independent warning data", result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ type producerAttemptRequest struct {
|
|||||||
// the current value as a safe fallback if its shared budget is exhausted.
|
// the current value as a safe fallback if its shared budget is exhausted.
|
||||||
// Artifact-specific adapters are responsible for validating and populating it.
|
// Artifact-specific adapters are responsible for validating and populating it.
|
||||||
type producerRetryDirective struct {
|
type producerRetryDirective struct {
|
||||||
FallbackWarnings []contracts.Warning
|
|
||||||
FallbackDiagnostics []contracts.ProducerDiagnostic
|
FallbackDiagnostics []contracts.ProducerDiagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ func (directive *producerRetryDirective) clone() *producerRetryDirective {
|
|||||||
if directive == nil {
|
if directive == nil {
|
||||||
return 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
|
// producerAttemptOutput is intentionally artifact-neutral. Value remains
|
||||||
@@ -69,7 +68,6 @@ func (directive *producerRetryDirective) clone() *producerRetryDirective {
|
|||||||
type producerAttemptOutput struct {
|
type producerAttemptOutput struct {
|
||||||
Value any
|
Value any
|
||||||
Candidate *contracts.ModelCandidate
|
Candidate *contracts.ModelCandidate
|
||||||
Warnings []contracts.Warning
|
|
||||||
Diagnostics []contracts.ProducerDiagnostic
|
Diagnostics []contracts.ProducerDiagnostic
|
||||||
Retry *producerRetryDirective
|
Retry *producerRetryDirective
|
||||||
}
|
}
|
||||||
@@ -82,7 +80,6 @@ func (output producerAttemptOutput) clone() (producerAttemptOutput, error) {
|
|||||||
return producerAttemptOutput{
|
return producerAttemptOutput{
|
||||||
Value: output.Value,
|
Value: output.Value,
|
||||||
Candidate: candidate,
|
Candidate: candidate,
|
||||||
Warnings: cloneWarnings(output.Warnings),
|
|
||||||
Diagnostics: contracts.CloneProducerDiagnostics(output.Diagnostics),
|
Diagnostics: contracts.CloneProducerDiagnostics(output.Diagnostics),
|
||||||
Retry: output.Retry.clone(),
|
Retry: output.Retry.clone(),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -116,7 +113,6 @@ func (provenance producerAttemptProvenance) clone() producerAttemptProvenance {
|
|||||||
type producerAttemptTerminal struct {
|
type producerAttemptTerminal struct {
|
||||||
Action producerTerminalAction
|
Action producerTerminalAction
|
||||||
Value any
|
Value any
|
||||||
Warnings []contracts.Warning
|
|
||||||
Diagnostics []contracts.ProducerDiagnostic
|
Diagnostics []contracts.ProducerDiagnostic
|
||||||
Rejection *contracts.RejectedOutput
|
Rejection *contracts.RejectedOutput
|
||||||
Validation validationReport
|
Validation validationReport
|
||||||
@@ -125,7 +121,6 @@ type producerAttemptTerminal struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (terminal producerAttemptTerminal) clone() producerAttemptTerminal {
|
func (terminal producerAttemptTerminal) clone() producerAttemptTerminal {
|
||||||
terminal.Warnings = cloneWarnings(terminal.Warnings)
|
|
||||||
terminal.Diagnostics = contracts.CloneProducerDiagnostics(terminal.Diagnostics)
|
terminal.Diagnostics = contracts.CloneProducerDiagnostics(terminal.Diagnostics)
|
||||||
if terminal.Rejection != nil {
|
if terminal.Rejection != nil {
|
||||||
rejection := *terminal.Rejection
|
rejection := *terminal.Rejection
|
||||||
@@ -210,7 +205,6 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if output.Retry != nil {
|
if output.Retry != nil {
|
||||||
output.Warnings = append(output.Warnings, cloneWarnings(output.Retry.FallbackWarnings)...)
|
|
||||||
output.Diagnostics = append(output.Diagnostics, contracts.CloneProducerDiagnostics(output.Retry.FallbackDiagnostics)...)
|
output.Diagnostics = append(output.Diagnostics, contracts.CloneProducerDiagnostics(output.Retry.FallbackDiagnostics)...)
|
||||||
}
|
}
|
||||||
correctionCandidate, err := contracts.CloneModelCandidate(output.Candidate)
|
correctionCandidate, err := contracts.CloneModelCandidate(output.Candidate)
|
||||||
@@ -259,14 +253,13 @@ func runProducerAttempts(ctx context.Context, config producerAttemptConfig, prod
|
|||||||
if incomplete := firstIncompleteValidation(report); incomplete != nil {
|
if incomplete := firstIncompleteValidation(report); incomplete != nil {
|
||||||
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)
|
return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptAccepted, Validation: report})
|
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")
|
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}
|
rejected := contracts.RejectedOutput{ValidatorName: rejection.validatorName, ReasonCode: rejection.reasonCode, Message: rejection.message, AttemptCount: number, DiagnosticArtifactPath: rejection.diagnosticPath}
|
||||||
switch policy.SemanticRejection {
|
switch policy.SemanticRejection {
|
||||||
case SemanticRejectionRejectOutput:
|
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:
|
case SemanticRejectionFailRun:
|
||||||
return failedProducerAttempt(provenance), fmt.Errorf("producer candidate rejected after %d attempt(s): %s", number, rejection.message)
|
return failedProducerAttempt(provenance), fmt.Errorf("producer candidate rejected after %d attempt(s): %s", number, rejection.message)
|
||||||
default:
|
default:
|
||||||
@@ -313,12 +306,6 @@ func firstIncompleteValidation(report validationReport) *validationRecord {
|
|||||||
return nil
|
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 {
|
func cloneProducerDiagnostics(diagnostics []contracts.ProducerDiagnostic) []contracts.ProducerDiagnostic {
|
||||||
return contracts.CloneProducerDiagnostics(diagnostics)
|
return contracts.CloneProducerDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,27 +234,27 @@ func TestRunProducerAttemptsUsesModuleRetryBudgetAndFallback(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fallback", func(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) {
|
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)
|
}, approveAttempt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("runProducerAttempts() error = %v", err)
|
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)
|
t.Fatalf("terminal = %#v", terminal)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunProducerAttemptsRejectionWinsOverValidatorFailure(t *testing.T) {
|
func TestRunProducerAttemptsRejectionWinsOverValidatorFailure(t *testing.T) {
|
||||||
firstWarnings := []contracts.Warning{{ReasonCode: "discarded", Message: "discarded warning"}}
|
firstDiagnostics := []contracts.ProducerDiagnostic{producerDiagnostic("discarded", "discarded warning")}
|
||||||
secondWarnings := []contracts.Warning{{ReasonCode: "accepted", Message: "accepted 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) {
|
terminal, err := runProducerAttempts(context.Background(), producerAttemptConfig{Retries: 1, Policy: DefaultValidationPolicy()}, func(_ context.Context, request producerAttemptRequest) (producerAttemptOutput, error) {
|
||||||
if request.Number == 1 {
|
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) {
|
}, func(_ context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||||
if output.Value == "first" {
|
if output.Value == "first" {
|
||||||
report := rejectedAttemptReport("defect")
|
report := rejectedAttemptReport("defect")
|
||||||
@@ -272,8 +272,8 @@ func TestRunProducerAttemptsRejectionWinsOverValidatorFailure(t *testing.T) {
|
|||||||
if got := attemptKinds(terminal.Provenance); !reflect.DeepEqual(got, []producerAttemptKind{producerAttemptInitial, producerAttemptSemanticRetry}) {
|
if got := attemptKinds(terminal.Provenance); !reflect.DeepEqual(got, []producerAttemptKind{producerAttemptInitial, producerAttemptSemanticRetry}) {
|
||||||
t.Fatalf("attempt kinds = %v", got)
|
t.Fatalf("attempt kinds = %v", got)
|
||||||
}
|
}
|
||||||
if got := terminal.Warnings; !reflect.DeepEqual(got, secondWarnings) {
|
if got := terminal.Diagnostics; !reflect.DeepEqual(got, secondDiagnostics) {
|
||||||
t.Fatalf("terminal warnings = %#v, want %#v", got, secondWarnings)
|
t.Fatalf("terminal diagnostics = %#v, want %#v", got, secondDiagnostics)
|
||||||
}
|
}
|
||||||
if len(terminal.Provenance[0].Validation.records) != 2 {
|
if len(terminal.Provenance[0].Validation.records) != 2 {
|
||||||
t.Fatalf("first validation records = %#v, want rejection and failure", terminal.Provenance[0].Validation.records)
|
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 {
|
if len(summary.ReasonCodes) != 0 || len(summary.RejectingValidators) != 0 || len(summary.IncompleteValidators) != 0 {
|
||||||
t.Fatalf("corrected success summary retained prior findings: %#v", summary)
|
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) {
|
func TestWarnContinueRecordsOneWarningForEachExhaustedValidator(t *testing.T) {
|
||||||
@@ -324,9 +321,6 @@ 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 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)
|
groups, groupErr := terminalDiagnosticGroups(terminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: "step", LaneID: "lane", ModuleKey: "module"}, nil)
|
||||||
if groupErr != nil {
|
if groupErr != nil {
|
||||||
t.Fatalf("terminalDiagnosticGroups() error = %v", groupErr)
|
t.Fatalf("terminalDiagnosticGroups() error = %v", groupErr)
|
||||||
|
|||||||
@@ -30,45 +30,45 @@ type ReferenceMaterializationOptions struct {
|
|||||||
WorkingDir string
|
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 := resolved
|
||||||
out.ChunkReferences = CloneReferenceTarget(resolved.ChunkReferences)
|
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 {
|
if err != nil {
|
||||||
return ResolvedPipeline{}, nil, err
|
return ResolvedPipeline{}, nil, err
|
||||||
}
|
}
|
||||||
out.ChunkReferences.ReferenceSet = chunkReferenceSet
|
out.ChunkReferences.ReferenceSet = chunkReferenceSet
|
||||||
warnings := append([]contracts.Warning(nil), chunkWarnings...)
|
diagnostics := contracts.CloneProducerDiagnostics(chunkDiagnostics)
|
||||||
if len(resolved.Steps) == 0 {
|
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
|
materializedLane := lane
|
||||||
var allWarnings []contracts.Warning
|
var allDiagnostics []contracts.ProducerDiagnostic
|
||||||
materializedLane.ExtractReferences = CloneReferenceTarget(lane.ExtractReferences)
|
materializedLane.ExtractReferences = CloneReferenceTarget(lane.ExtractReferences)
|
||||||
materializedLane.MergeReferences = CloneReferenceTarget(lane.MergeReferences)
|
materializedLane.MergeReferences = CloneReferenceTarget(lane.MergeReferences)
|
||||||
materializedLane.NormalizeReferences = CloneReferenceTarget(lane.NormalizeReferences)
|
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 {
|
if err != nil {
|
||||||
return ResolvedArtifactLane{}, nil, err
|
return ResolvedArtifactLane{}, nil, err
|
||||||
}
|
}
|
||||||
materializedLane.ExtractReferences.ReferenceSet = extractReferenceSet
|
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 {
|
if err != nil {
|
||||||
return ResolvedArtifactLane{}, nil, err
|
return ResolvedArtifactLane{}, nil, err
|
||||||
}
|
}
|
||||||
materializedLane.MergeReferences.ReferenceSet = mergeReferenceSet
|
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 {
|
if err != nil {
|
||||||
return ResolvedArtifactLane{}, nil, err
|
return ResolvedArtifactLane{}, nil, err
|
||||||
}
|
}
|
||||||
materializedLane.NormalizeReferences.ReferenceSet = normalizeReferenceSet
|
materializedLane.NormalizeReferences.ReferenceSet = normalizeReferenceSet
|
||||||
allWarnings = append(allWarnings, laneWarnings...)
|
allDiagnostics = append(allDiagnostics, laneDiagnostics...)
|
||||||
return materializedLane, allWarnings, nil
|
return materializedLane, allDiagnostics, nil
|
||||||
}
|
}
|
||||||
if len(resolved.Steps) > 0 {
|
if len(resolved.Steps) > 0 {
|
||||||
out.Steps = make([]ResolvedPipelineStep, len(resolved.Steps))
|
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].ID = step.ID
|
||||||
out.Steps[i].ArtifactLanes = make([]ResolvedArtifactLane, len(step.ArtifactLanes))
|
out.Steps[i].ArtifactLanes = make([]ResolvedArtifactLane, len(step.ArtifactLanes))
|
||||||
for j, lane := range step.ArtifactLanes {
|
for j, lane := range step.ArtifactLanes {
|
||||||
materializedLane, laneWarnings, err := materializeLane(lane)
|
materializedLane, laneDiagnostics, err := materializeLane(lane)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ResolvedPipeline{}, nil, err
|
return ResolvedPipeline{}, nil, err
|
||||||
}
|
}
|
||||||
out.Steps[i].ArtifactLanes[j] = materializedLane
|
out.Steps[i].ArtifactLanes[j] = materializedLane
|
||||||
warnings = append(warnings, laneWarnings...)
|
diagnostics = append(diagnostics, laneDiagnostics...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out, warnings, nil
|
return out, diagnostics, 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(
|
||||||
@@ -114,7 +94,7 @@ func materializeReferenceTarget(
|
|||||||
artifactKind contracts.ArtifactKind,
|
artifactKind contracts.ArtifactKind,
|
||||||
catalog ModuleCatalog,
|
catalog ModuleCatalog,
|
||||||
options ReferenceMaterializationOptions,
|
options ReferenceMaterializationOptions,
|
||||||
) (contracts.ReferenceSet, []contracts.Warning, error) {
|
) (contracts.ReferenceSet, []contracts.ProducerDiagnostic, error) {
|
||||||
if len(target.Bindings) == 0 {
|
if len(target.Bindings) == 0 {
|
||||||
return contracts.ReferenceSet{}, nil, nil
|
return contracts.ReferenceSet{}, nil, nil
|
||||||
}
|
}
|
||||||
@@ -129,7 +109,7 @@ func materializeReferenceTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set := contracts.ReferenceSet{Slots: make(map[string]contracts.ResolvedReferenceSlot, len(target.Bindings))}
|
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 {
|
for _, binding := range target.Bindings {
|
||||||
slotName := strings.TrimSpace(binding.SlotName)
|
slotName := strings.TrimSpace(binding.SlotName)
|
||||||
slot, ok := slotByName[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)
|
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 {
|
if len(content) == 0 {
|
||||||
warnings = append(warnings, contracts.Warning{
|
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))}}})
|
||||||
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)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item := contracts.ReferenceItem{
|
item := contracts.ReferenceItem{
|
||||||
@@ -200,7 +176,7 @@ func materializeReferenceTarget(
|
|||||||
Items: []contracts.ReferenceItem{item},
|
Items: []contracts.ReferenceItem{item},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return set, warnings, nil
|
return set, diagnostics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type referenceSizeLimitError struct {
|
type referenceSizeLimitError struct {
|
||||||
|
|||||||
@@ -445,17 +445,13 @@ func TestMaterializeReferencesWarnsForEmptyFiles(t *testing.T) {
|
|||||||
writeReferenceFile(t, path, nil)
|
writeReferenceFile(t, path, nil)
|
||||||
|
|
||||||
resolved := resolvedPipelineWithReference(t, "roster", "empty.txt", contracts.ReferenceBindingSourceConfig, contracts.ReferenceSlot{Name: "roster"})
|
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"),
|
ConfigPath: filepath.Join(configDir, "config.yml"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MaterializeReferences() error = %v, want nil", err)
|
t.Fatalf("MaterializeReferences() error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
if len(warnings) != 1 || warnings[0].ReasonCode != "empty_reference" {
|
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("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)
|
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]
|
||||||
@@ -487,13 +483,13 @@ func TestMaterializeReferencesWarningScopesIncludeTargetContext(t *testing.T) {
|
|||||||
t.Fatalf("ResolvePipeline() error = %v, want nil", err)
|
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"),
|
ConfigPath: filepath.Join(configDir, "config.yml"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MaterializeReferences() error = %v, want nil", err)
|
t.Fatalf("MaterializeReferences() error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
got := warningScopes(warnings)
|
got := diagnosticScopes(diagnostics)
|
||||||
want := []string{
|
want := []string{
|
||||||
"pipeline.baseline.chunk.reference.scene_guide",
|
"pipeline.baseline.chunk.reference.scene_guide",
|
||||||
"pipeline.baseline.lane.events.extract.reference.roster",
|
"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 {
|
func diagnosticScopes(diagnostics []contracts.ProducerDiagnostic) []string {
|
||||||
scopes := make([]string, 0, len(warnings))
|
scopes := make([]string, 0, len(diagnostics))
|
||||||
for _, warning := range warnings {
|
for _, diagnostic := range diagnostics {
|
||||||
scopes = append(scopes, warning.Scope)
|
scopes = append(scopes, diagnostic.Samples[0].Scope)
|
||||||
}
|
}
|
||||||
sort.Strings(scopes)
|
sort.Strings(scopes)
|
||||||
return scopes
|
return scopes
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ type RunInput struct {
|
|||||||
StartedAt time.Time
|
StartedAt time.Time
|
||||||
LLMProfiles []artifacts.LLMProfileManifest
|
LLMProfiles []artifacts.LLMProfileManifest
|
||||||
Metadata map[string]any
|
Metadata map[string]any
|
||||||
Warnings []contracts.Warning
|
|
||||||
Diagnostics []contracts.ProducerDiagnostic
|
Diagnostics []contracts.ProducerDiagnostic
|
||||||
ChunkCacheMode ChunkCacheMode
|
ChunkCacheMode ChunkCacheMode
|
||||||
ChunkPlans ChunkPlanStore
|
ChunkPlans ChunkPlanStore
|
||||||
@@ -74,7 +73,6 @@ type RunOutput struct {
|
|||||||
ChunkPlan *artifacts.ChunkPlanSummary `json:"chunk_plan,omitempty"`
|
ChunkPlan *artifacts.ChunkPlanSummary `json:"chunk_plan,omitempty"`
|
||||||
NormalizeOutputs []contracts.SerializedOutput `json:"normalize_outputs,omitempty"`
|
NormalizeOutputs []contracts.SerializedOutput `json:"normalize_outputs,omitempty"`
|
||||||
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
Rejected []contracts.RejectedOutput `json:"rejected,omitempty"`
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
Diagnostics contracts.DiagnosticCollection `json:"diagnostics,omitempty"`
|
Diagnostics contracts.DiagnosticCollection `json:"diagnostics,omitempty"`
|
||||||
OutputFiles []contracts.OutputFile `json:"-"`
|
OutputFiles []contracts.OutputFile `json:"-"`
|
||||||
CheckpointEvents []CheckpointEvent `json:"checkpoint_events,omitempty"`
|
CheckpointEvents []CheckpointEvent `json:"checkpoint_events,omitempty"`
|
||||||
@@ -132,7 +130,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
defer func() {
|
defer func() {
|
||||||
output.Manifest.LLMProfiles = mergeLLMProfileManifests(input.LLMProfiles, llmProfileManifests(input.llmClient))
|
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)
|
inputDiagnostics, diagnosticErr := promoteProducerDiagnostics(input.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageReferences}, nil)
|
||||||
if diagnosticErr != nil {
|
if diagnosticErr != nil {
|
||||||
return failOutput(output), fmt.Errorf("promote input diagnostics: %w", diagnosticErr)
|
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 {
|
if chunkResult.validation != nil {
|
||||||
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(*chunkResult.validation))
|
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(*chunkResult.validation))
|
||||||
}
|
}
|
||||||
output.Warnings = append(output.Warnings, chunkResult.warnings...)
|
|
||||||
appendDiagnosticGroups(&output, chunkResult.diagnostics)
|
appendDiagnosticGroups(&output, chunkResult.diagnostics)
|
||||||
chunkDebugPayload := map[string]any{
|
chunkDebugPayload := map[string]any{
|
||||||
"cache_mode": chunkMode,
|
"cache_mode": chunkMode,
|
||||||
"lookup": chunkResult.lookup,
|
"lookup": chunkResult.lookup,
|
||||||
"accepted": chunkResult.accepted,
|
"accepted": chunkResult.accepted,
|
||||||
"materialized_chunks": debugSourceChunkEnvelopes(chunkResult.chunks),
|
"materialized_chunks": debugSourceChunkEnvelopes(chunkResult.chunks),
|
||||||
"warnings": chunkResult.warnings,
|
|
||||||
}
|
}
|
||||||
if chunkResult.plan != nil {
|
if chunkResult.plan != nil {
|
||||||
chunkDebugPayload["plan"] = debugChunkPlanEnvelope(*chunkResult.plan)
|
chunkDebugPayload["plan"] = debugChunkPlanEnvelope(*chunkResult.plan)
|
||||||
@@ -356,7 +351,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
"manifest": output.Manifest,
|
"manifest": output.Manifest,
|
||||||
"normalize_outputs": debugSerializedOutputEnvelopes(output.NormalizeOutputs),
|
"normalize_outputs": debugSerializedOutputEnvelopes(output.NormalizeOutputs),
|
||||||
"rejected": debugRejectedOutputEnvelopes(output.Rejected),
|
"rejected": debugRejectedOutputEnvelopes(output.Rejected),
|
||||||
"warnings": output.Warnings,
|
|
||||||
"options": redactSensitiveMap(input.pipeline.Output.Options),
|
"options": redactSensitiveMap(input.pipeline.Output.Options),
|
||||||
"metadata": redactSensitiveMap(input.Metadata),
|
"metadata": redactSensitiveMap(input.Metadata),
|
||||||
}
|
}
|
||||||
@@ -385,7 +379,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
Manifest: output.Manifest,
|
Manifest: output.Manifest,
|
||||||
NormalizeOutputs: cloneSerializedOutputs(output.NormalizeOutputs),
|
NormalizeOutputs: cloneSerializedOutputs(output.NormalizeOutputs),
|
||||||
Rejected: cloneRejectedOutputs(output.Rejected),
|
Rejected: cloneRejectedOutputs(output.Rejected),
|
||||||
Warnings: output.Warnings,
|
|
||||||
Diagnostics: contracts.CloneDiagnosticCollection(output.Diagnostics),
|
Diagnostics: contracts.CloneDiagnosticCollection(output.Diagnostics),
|
||||||
LLMProfile: input.pipeline.Output.LLMProfile,
|
LLMProfile: input.pipeline.Output.LLMProfile,
|
||||||
StructuredOutputRepairAttempts: cloneStructuredOutputRepairAttempts(input.pipeline.Output.StructuredOutputRepairAttempts),
|
StructuredOutputRepairAttempts: cloneStructuredOutputRepairAttempts(input.pipeline.Output.StructuredOutputRepairAttempts),
|
||||||
@@ -455,7 +448,6 @@ func (r *Runner) runPreparedSteps(ctx context.Context, input RunInput, checkpoin
|
|||||||
type retryAttemptResult struct {
|
type retryAttemptResult struct {
|
||||||
accepted bool
|
accepted bool
|
||||||
rejection *contracts.RejectedOutput
|
rejection *contracts.RejectedOutput
|
||||||
warnings []contracts.Warning
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSimpleRetry(ctx context.Context, retries int, run func(attempt int) (retryAttemptResult, error)) (retryAttemptResult, error) {
|
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 {
|
if result.rejection != nil {
|
||||||
rejection := *result.rejection
|
rejection := *result.rejection
|
||||||
rejection.AttemptCount = attempt
|
rejection.AttemptCount = attempt
|
||||||
last = retryAttemptResult{rejection: &rejection, warnings: cloneWarnings(result.warnings)}
|
last = retryAttemptResult{rejection: &rejection}
|
||||||
}
|
}
|
||||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
return retryAttemptResult{}, ctxErr
|
return retryAttemptResult{}, ctxErr
|
||||||
@@ -708,6 +700,10 @@ func validatorChainManifests(chains []ResolvedValidatorChain) []artifacts.Valida
|
|||||||
}
|
}
|
||||||
|
|
||||||
func failOutput(output RunOutput) RunOutput {
|
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 != "" {
|
if output.Manifest.PipelineID != "" {
|
||||||
populateOutputManifest(&output)
|
populateOutputManifest(&output)
|
||||||
output.Manifest.ValidationStatus = "failed"
|
output.Manifest.ValidationStatus = "failed"
|
||||||
@@ -982,13 +978,6 @@ func manifestMetadataWithSessionID(metadata map[string]any, sessionID string) (m
|
|||||||
return out, nil
|
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) {
|
func cloneSourceChunkPtr(chunk *source.Chunk) (*source.Chunk, error) {
|
||||||
if chunk == nil {
|
if chunk == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ func TestRunnerHydratesRequiredNormalizedArtifact(t *testing.T) {
|
|||||||
loader := newAcceptedCheckpointLoader()
|
loader := newAcceptedCheckpointLoader()
|
||||||
producerKey := CheckpointLaneKey(producer.resolved.StepID, producer.resolved.ID)
|
producerKey := CheckpointLaneKey(producer.resolved.StepID, producer.resolved.ID)
|
||||||
consumerKey := CheckpointLaneKey(consumer.resolved.StepID, consumer.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)
|
loader.acceptedDecision[producerKey] = NewCheckpointDecision(CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
||||||
policy := CheckpointExecutionPolicy{
|
policy := CheckpointExecutionPolicy{
|
||||||
RequireReusableLanes: map[string]struct{}{producerKey: {}},
|
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 {
|
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)
|
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" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "stored-warning" {
|
||||||
t.Fatalf("hydrated warnings = %#v, want normalize checkpoint warnings only", output.Warnings)
|
t.Fatalf("hydrated diagnostics = %#v, want normalize checkpoint diagnostics only", output.Diagnostics)
|
||||||
}
|
}
|
||||||
assertAcceptedNormalizeEvent(t, output.CheckpointEvents, producer.resolved.StepID, producer.resolved.ID, CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
assertAcceptedNormalizeEvent(t, output.CheckpointEvents, producer.resolved.StepID, producer.resolved.ID, CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
||||||
for _, event := range output.CheckpointEvents {
|
for _, event := range output.CheckpointEvents {
|
||||||
@@ -225,7 +226,8 @@ func TestRunnerRetainsEarlierHydratedProducerWhenLaterRequiredProducerFails(t *t
|
|||||||
loader := newAcceptedCheckpointLoader()
|
loader := newAcceptedCheckpointLoader()
|
||||||
firstKey := CheckpointLaneKey(first.resolved.StepID, first.resolved.ID)
|
firstKey := CheckpointLaneKey(first.resolved.StepID, first.resolved.ID)
|
||||||
secondKey := CheckpointLaneKey(second.resolved.StepID, second.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[firstKey] = NewCheckpointDecision(CheckpointDecisionReused, CheckpointReasonAcceptedArtifactReused)
|
||||||
loader.acceptedDecision[secondKey] = NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing)
|
loader.acceptedDecision[secondKey] = NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing)
|
||||||
policy := CheckpointExecutionPolicy{RequireReusableLanes: map[string]struct{}{firstKey: {}, secondKey: {}}}
|
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) {
|
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)
|
t.Fatalf("retained normalize outputs = %#v, want first producer", output.NormalizeOutputs)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != "retained-warning" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "retained-warning" {
|
||||||
t.Fatalf("retained warnings = %#v", output.Warnings)
|
t.Fatalf("retained diagnostics = %#v", output.Diagnostics)
|
||||||
}
|
}
|
||||||
type decisionExpectation struct {
|
type decisionExpectation struct {
|
||||||
step, lane string
|
step, lane string
|
||||||
|
|||||||
@@ -162,13 +162,13 @@ func TestRunnerWritesAttemptScopedMergeAndNormalizeDebug(t *testing.T) {
|
|||||||
if err := callAttemptDebugLLM(ctx, client, "merge"); err != nil {
|
if err := callAttemptDebugLLM(ctx, client, "merge"); err != nil {
|
||||||
return erasedTypedResult{}, err
|
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) {
|
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 {
|
if err := callAttemptDebugLLM(ctx, client, "normalize"); err != nil {
|
||||||
return erasedTypedResult{}, err
|
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})
|
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} {
|
for _, stage := range []ModuleStage{StageMerge, StageNormalize} {
|
||||||
t.Run(string(stage), func(t *testing.T) {
|
t.Run(string(stage), func(t *testing.T) {
|
||||||
prepared := preparedAttemptDebugPipeline(t)
|
prepared := preparedAttemptDebugPipeline(t)
|
||||||
@@ -230,7 +230,7 @@ func TestRunnerRecordsDistinctRetryAttemptsAndPromotesAcceptedWarningsOnly(t *te
|
|||||||
if attempts == 1 {
|
if attempts == 1 {
|
||||||
scope = "discarded"
|
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
|
validatorCalls := 0
|
||||||
validator := preparedValidator{
|
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 {
|
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)
|
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") {
|
if !strings.Contains(string(debug.json[firstPath]), "rejection") {
|
||||||
t.Fatalf("first attempt envelope = %s, want discarded warning and rejection", debug.json[firstPath])
|
t.Fatalf("first attempt envelope = %s, want rejection", debug.json[firstPath])
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].Scope != "accepted" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].Samples[0].Scope != "accepted" {
|
||||||
t.Fatalf("promoted warnings = %#v, want accepted attempt only", output.Warnings)
|
t.Fatalf("promoted diagnostics = %#v, want accepted attempt only", output.Diagnostics)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,13 +101,13 @@ type candidateCheckpointRecorder struct {
|
|||||||
normalizeOutput CheckpointArtifact
|
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.mergeSucceeded++
|
||||||
r.mergeOutput = cloneCheckpointArtifact(output)
|
r.mergeOutput = cloneCheckpointArtifact(output)
|
||||||
return nil
|
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.normalizeSucceeded++
|
||||||
r.normalizeOutput = cloneCheckpointArtifact(output)
|
r.normalizeOutput = cloneCheckpointArtifact(output)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ type chunkPlanExecution struct {
|
|||||||
accepted bool
|
accepted bool
|
||||||
chunks []source.Chunk
|
chunks []source.Chunk
|
||||||
plan *source.ChunkPlan
|
plan *source.ChunkPlan
|
||||||
warnings []contracts.Warning
|
|
||||||
diagnostics []contracts.DiagnosticGroup
|
diagnostics []contracts.DiagnosticGroup
|
||||||
rejection *contracts.RejectedOutput
|
rejection *contracts.RejectedOutput
|
||||||
lookup ChunkPlanDecision
|
lookup ChunkPlanDecision
|
||||||
@@ -29,7 +28,6 @@ type generatedChunkPlanCandidate struct {
|
|||||||
plan source.ChunkPlan
|
plan source.ChunkPlan
|
||||||
chunks []source.Chunk
|
chunks []source.Chunk
|
||||||
record ChunkPlanRecord
|
record ChunkPlanRecord
|
||||||
producerWarnings []contracts.Warning
|
|
||||||
producerDiagnostics []contracts.ProducerDiagnostic
|
producerDiagnostics []contracts.ProducerDiagnostic
|
||||||
terminal *attemptTerminalRecorder
|
terminal *attemptTerminalRecorder
|
||||||
}
|
}
|
||||||
@@ -71,7 +69,7 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
|||||||
if validationErr == nil {
|
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)
|
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 {
|
if err != nil {
|
||||||
result.setValidation(report.Warnings(), nil, err)
|
result.setValidation(nil, err)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
rejection := report.FirstRejection()
|
rejection := report.FirstRejection()
|
||||||
@@ -82,9 +80,8 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
|||||||
}
|
}
|
||||||
result.plan = &plan
|
result.plan = &plan
|
||||||
result.chunks = chunks
|
result.chunks = chunks
|
||||||
result.warnings = append(cloneWarnings(record.Warnings), report.Warnings()...)
|
|
||||||
result.accepted = true
|
result.accepted = true
|
||||||
result.setValidation(report.Warnings(), nil, nil)
|
result.setValidation(nil, nil)
|
||||||
cachedTerminal := producerAttemptTerminal{Action: producerTerminalAccepted, Diagnostics: contracts.CloneProducerDiagnostics(record.Diagnostics), Validation: report}
|
cachedTerminal := producerAttemptTerminal{Action: producerTerminalAccepted, Diagnostics: contracts.CloneProducerDiagnostics(record.Diagnostics), Validation: report}
|
||||||
diagnostics, diagnosticErr := terminalDiagnosticGroups(cachedTerminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageChunk, ModuleKey: chunker.Key()}, nil)
|
diagnostics, diagnosticErr := terminalDiagnosticGroups(cachedTerminal, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageChunk, ModuleKey: chunker.Key()}, nil)
|
||||||
if diagnosticErr != 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 {
|
if incomplete != nil && input.pipeline.ChunkValidationPolicy.ValidatorFailure == ValidatorFailureFailRun {
|
||||||
failure := validatorFailureError(*incomplete)
|
failure := validatorFailureError(*incomplete)
|
||||||
result.setValidation(report.Warnings(), nil, failure)
|
result.setValidation(nil, failure)
|
||||||
failedTerminal := producerAttemptTerminal{Action: producerTerminalFailed, Validation: report, ValidationIncomplete: true}
|
failedTerminal := producerAttemptTerminal{Action: producerTerminalFailed, Validation: report, ValidationIncomplete: true}
|
||||||
summary := validationSummary(failedTerminal, StageChunk, "", "", chunker.Key(), "", 0)
|
summary := validationSummary(failedTerminal, StageChunk, "", "", chunker.Key(), "", 0)
|
||||||
result.validation = &summary
|
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)
|
plan, chunks, validationErr := validateAndMaterializeChunkPlan(doc, chunkResult.Plan)
|
||||||
if validationErr != nil {
|
if validationErr != nil {
|
||||||
attemptErr := fmt.Errorf("validate chunk plan from chunker %q: %w", chunker.Key(), validationErr)
|
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))
|
return producerAttemptOutput{}, attemptTerminal.record(payload, fmt.Errorf("%w: %v", contracts.ErrInvalidStructuredOutput, attemptErr))
|
||||||
}
|
}
|
||||||
planDigest, digestErr := source.DigestChunkPlan(plan)
|
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)...),
|
References: append([]artifacts.ReferenceProvenance(nil), referenceTargetProvenance(input.pipeline.ChunkReferences)...),
|
||||||
Metadata: producerMetadata,
|
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) {
|
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||||
candidate, ok := output.Value.(generatedChunkPlanCandidate)
|
candidate, ok := output.Value.(generatedChunkPlanCandidate)
|
||||||
if !ok {
|
if !ok {
|
||||||
return validationReport{}, fmt.Errorf("chunk attempt candidate has incompatible type")
|
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)
|
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{
|
payload := map[string]any{
|
||||||
"plan": debugChunkPlanEnvelope(candidate.plan), "materialized_chunks": debugSourceChunkEnvelopes(candidate.chunks),
|
"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 {
|
if validationErr != nil {
|
||||||
return report, candidate.terminal.record(payload, validationErr)
|
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.Stage = string(StageChunk)
|
||||||
result.rejection.ModuleKey = chunker.Key()
|
result.rejection.ModuleKey = chunker.Key()
|
||||||
}
|
}
|
||||||
result.warnings = cloneWarnings(terminal.Warnings)
|
|
||||||
result.validation = &terminalSummary
|
result.validation = &terminalSummary
|
||||||
if result.rejection != nil {
|
if result.rejection != nil {
|
||||||
result.rejection.Validation = cloneValidationSummaryPtr(result.validation)
|
result.rejection.Validation = cloneValidationSummaryPtr(result.validation)
|
||||||
}
|
}
|
||||||
result.setValidation(terminal.Validation.Warnings(), result.rejection, nil)
|
result.setValidation(result.rejection, nil)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
candidate, ok := terminal.Value.(generatedChunkPlanCandidate)
|
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.accepted = true
|
||||||
result.plan = &candidate.plan
|
result.plan = &candidate.plan
|
||||||
result.chunks = candidate.chunks
|
result.chunks = candidate.chunks
|
||||||
result.warnings = cloneWarnings(terminal.Warnings)
|
|
||||||
result.validation = &terminalSummary
|
result.validation = &terminalSummary
|
||||||
result.setValidation(terminal.Validation.Warnings(), nil, nil)
|
result.setValidation(nil, nil)
|
||||||
if terminal.ValidationIncomplete {
|
if terminal.ValidationIncomplete {
|
||||||
result.summary.ValidationStatus = "incomplete"
|
result.summary.ValidationStatus = "incomplete"
|
||||||
}
|
}
|
||||||
@@ -246,7 +240,6 @@ func (r *Runner) runChunkPlan(ctx context.Context, input RunInput, doc *source.S
|
|||||||
if cloneErr != nil {
|
if cloneErr != nil {
|
||||||
return result, fmt.Errorf("clone chunk plan record for publication: %w", cloneErr)
|
return result, fmt.Errorf("clone chunk plan record for publication: %w", cloneErr)
|
||||||
}
|
}
|
||||||
record.Warnings = cloneWarnings(candidate.producerWarnings)
|
|
||||||
record.Diagnostics = contracts.CloneProducerDiagnostics(candidate.producerDiagnostics)
|
record.Diagnostics = contracts.CloneProducerDiagnostics(candidate.producerDiagnostics)
|
||||||
if err := input.ChunkPlans.Save(record); err != nil {
|
if err := input.ChunkPlans.Save(record); err != nil {
|
||||||
return result, fmt.Errorf("save chunk plan: %w", err)
|
return result, fmt.Errorf("save chunk plan: %w", err)
|
||||||
@@ -303,14 +296,12 @@ func (result *chunkPlanExecution) setCandidate(record ChunkPlanRecord, action st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (result *chunkPlanExecution) setValidation(warnings []contracts.Warning, rejection *contracts.RejectedOutput, err error) {
|
func (result *chunkPlanExecution) setValidation(rejection *contracts.RejectedOutput, err error) {
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
result.summary.ValidationStatus = "error"
|
result.summary.ValidationStatus = "error"
|
||||||
case rejection != nil:
|
case rejection != nil:
|
||||||
result.summary.ValidationStatus = "rejected"
|
result.summary.ValidationStatus = "rejected"
|
||||||
case len(warnings) > 0:
|
|
||||||
result.summary.ValidationStatus = "approved_with_warnings"
|
|
||||||
default:
|
default:
|
||||||
result.summary.ValidationStatus = "approved"
|
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)
|
return ChunkPlanRecord{}, fmt.Errorf("clone chunk plan producer metadata: %w", err)
|
||||||
}
|
}
|
||||||
record.Producer.Metadata = metadata
|
record.Producer.Metadata = metadata
|
||||||
record.Warnings = cloneWarnings(record.Warnings)
|
|
||||||
record.Diagnostics = contracts.CloneProducerDiagnostics(record.Diagnostics)
|
record.Diagnostics = contracts.CloneProducerDiagnostics(record.Diagnostics)
|
||||||
return record, nil
|
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) {
|
func (c *retryingChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||||
c.calls++
|
c.calls++
|
||||||
if c.calls == 1 {
|
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 {
|
type dependencyLoader struct {
|
||||||
@@ -399,7 +399,7 @@ func TestRunnerRegeneratesValidationIncompleteChunkPlanHit(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
}}
|
}}
|
||||||
record := chunkPlanRecord(t, prepared, plan)
|
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}}
|
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||||
debug := newCapturedDebugRecorder()
|
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" {
|
if output.ChunkPlan == nil || output.ChunkPlan.Action != "generated" || output.ChunkPlan.LookupStatus != "invalid" || output.ChunkPlan.ValidationStatus != "approved" {
|
||||||
t.Fatalf("chunk plan summary = %#v", output.ChunkPlan)
|
t.Fatalf("chunk plan summary = %#v", output.ChunkPlan)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 0 {
|
if len(output.Diagnostics.Groups) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want discarded cache warnings omitted", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want discarded cache diagnostics omitted", output.Diagnostics)
|
||||||
}
|
}
|
||||||
assertAttemptEnvelopeSequence(t, debug, "chunk", 1)
|
assertAttemptEnvelopeSequence(t, debug, "chunk", 1)
|
||||||
}
|
}
|
||||||
@@ -427,7 +427,7 @@ func TestRunnerKeepsStoredPlanWhenCacheAndGeneratedValidationAreIncomplete(t *te
|
|||||||
validator := &countingChunkValidator{err: errors.New("validator unavailable")}
|
validator := &countingChunkValidator{err: errors.New("validator unavailable")}
|
||||||
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||||
record := chunkPlanRecord(t, prepared, plan)
|
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}}
|
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||||
|
|
||||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto, ChunkPlans: store})
|
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" {
|
if output.ChunkPlan == nil || output.ChunkPlan.Action != "generated" || output.ChunkPlan.LookupStatus != "invalid" || output.ChunkPlan.ValidationStatus != "incomplete" {
|
||||||
t.Fatalf("chunk plan summary = %#v", output.ChunkPlan)
|
t.Fatalf("chunk plan summary = %#v", output.ChunkPlan)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != "validator_execution_incomplete" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
|
||||||
t.Fatalf("warnings = %#v, want only generated incomplete warning", output.Warnings)
|
t.Fatalf("diagnostics = %#v, want only generated incomplete diagnostic", output.Diagnostics)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(store.record, record) {
|
if !reflect.DeepEqual(store.record, record) {
|
||||||
t.Fatal("discarded incomplete candidates mutated the stored 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 {
|
if chunker.calls != 2 || store.saves != 1 {
|
||||||
t.Fatalf("module calls = %d saves = %d, want 2 and 1", chunker.calls, store.saves)
|
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" {
|
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 warnings = %#v stored warnings = %#v", output.Warnings, store.saved.Warnings)
|
t.Fatalf("output diagnostics = %#v stored diagnostics = %#v", output.Diagnostics, store.saved.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +624,7 @@ func TestRunnerAutoHitValidatesOnceWithoutRegenerationOrMutation(t *testing.T) {
|
|||||||
wantError string
|
wantError string
|
||||||
wantReject bool
|
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: "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"},
|
{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}
|
validator := &countingChunkValidator{result: tc.result, err: tc.validatorErr}
|
||||||
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||||
record := chunkPlanRecord(t, prepared, plan)
|
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}}
|
store := &recordingChunkPlanStore{record: record, decision: ChunkPlanDecision{Status: ChunkPlanHit}}
|
||||||
debug := newCapturedDebugRecorder()
|
debug := newCapturedDebugRecorder()
|
||||||
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheAuto, ChunkPlans: store, Debug: debug})
|
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") {
|
if tc.wantReject && (len(output.Rejected) != 1 || output.Rejected[0].ReasonCode != "rejected") {
|
||||||
t.Fatalf("rejected = %#v", output.Rejected)
|
t.Fatalf("rejected = %#v", output.Rejected)
|
||||||
}
|
}
|
||||||
if tc.wantError == "" && !tc.wantReject && len(output.Warnings) != 1+len(tc.result.Warnings) {
|
if tc.wantError == "" && !tc.wantReject && len(output.Diagnostics.Groups) != 1+len(tc.result.Diagnostics) {
|
||||||
t.Fatalf("warnings = %#v, want stored warning once plus current warnings", output.Warnings)
|
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, plan := preparedTerminalDebugPipeline(t)
|
||||||
prepared.resolved.Chunk.LLMProfile = "chunk-profile"
|
prepared.resolved.Chunk.LLMProfile = "chunk-profile"
|
||||||
prepared.resolved.ChunkExecutionClass = contracts.ExecutionClassLLMBacked
|
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")}}},
|
"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"}}
|
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, Warnings: []contracts.Warning{{Scope: "validator", ReasonCode: "observed", Message: "validator warning"}}}}
|
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}}
|
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk}, chunk: validator}}
|
||||||
store := &recordingChunkPlanStore{}
|
store := &recordingChunkPlanStore{}
|
||||||
if _, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), ChunkCacheMode: ChunkCacheRefresh, ChunkPlans: store}); err != nil {
|
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" {
|
if len(producer.References) != 1 || producer.References[0].Digest != "sha256:guide" {
|
||||||
t.Fatalf("producer references = %#v", producer.References)
|
t.Fatalf("producer references = %#v", producer.References)
|
||||||
}
|
}
|
||||||
if len(store.saved.Warnings) != 1 || store.saved.Warnings[0].Scope != "producer" {
|
if len(store.saved.Diagnostics) != 1 || store.saved.Diagnostics[0].Samples[0].Scope != "producer" {
|
||||||
t.Fatalf("stored warnings = %#v, want only producer warning", store.saved.Warnings)
|
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 {
|
if len(output.OutputFiles) != 0 {
|
||||||
t.Fatalf("output files = %#v, want none", output.OutputFiles)
|
t.Fatalf("output files = %#v, want none", output.OutputFiles)
|
||||||
}
|
}
|
||||||
for _, warning := range output.Warnings {
|
for _, group := range output.Diagnostics.Groups {
|
||||||
if warning.ReasonCode == "returned-after-cancel" {
|
if group.ReasonCode == "returned-after-cancel" {
|
||||||
t.Fatalf("output warnings include encoder warning after cancellation")
|
t.Fatalf("output diagnostics include encoder diagnostic after cancellation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,7 +367,7 @@ func TestRunnerBoundsExtractJobsAndStabilizesReverseCompletion(t *testing.T) {
|
|||||||
return erasedTypedResult{}, ctx.Err()
|
return erasedTypedResult{}, ctx.Err()
|
||||||
}
|
}
|
||||||
scope := fmt.Sprintf("lane-%d/chunk-%d", lane, request.Chunk.Index)
|
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 {
|
if got := maximum.Load(); got != 2 {
|
||||||
t.Fatalf("maximum concurrent extract jobs = %d, want 2", got)
|
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 {
|
if len(result.output.Diagnostics.Groups) != 2 {
|
||||||
t.Fatalf("diagnostic groups = %#v, want one deterministic group per lane", result.output.Diagnostics.Groups)
|
t.Fatalf("diagnostic groups = %#v, want one deterministic group per lane", result.output.Diagnostics.Groups)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ type laneExtractState struct {
|
|||||||
reuseEligible bool
|
reuseEligible bool
|
||||||
values []erasedExtractArtifact
|
values []erasedExtractArtifact
|
||||||
serialized []CheckpointArtifact
|
serialized []CheckpointArtifact
|
||||||
warnings []contracts.Warning
|
|
||||||
diagnostics []contracts.DiagnosticGroup
|
diagnostics []contracts.DiagnosticGroup
|
||||||
rejected []contracts.RejectedOutput
|
rejected []contracts.RejectedOutput
|
||||||
incomplete []int
|
incomplete []int
|
||||||
@@ -39,7 +38,6 @@ type laneExtractState struct {
|
|||||||
type finalizedExtractResults struct {
|
type finalizedExtractResults struct {
|
||||||
accepted []erasedExtractArtifact
|
accepted []erasedExtractArtifact
|
||||||
serialized []CheckpointArtifact
|
serialized []CheckpointArtifact
|
||||||
warnings []contracts.Warning
|
|
||||||
diagnostics []contracts.DiagnosticGroup
|
diagnostics []contracts.DiagnosticGroup
|
||||||
rejected []contracts.RejectedOutput
|
rejected []contracts.RejectedOutput
|
||||||
incomplete []int
|
incomplete []int
|
||||||
@@ -55,8 +53,8 @@ func loadExtract(loader CheckpointLoader, stepID, laneID, moduleKey string, deps
|
|||||||
return loader.Extract(laneID, moduleKey, 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 {
|
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, warnings)
|
return checkpointExtractSucceeded(recorder, stepID, laneID, moduleKey, deps, outputs, rejected)
|
||||||
}
|
}
|
||||||
|
|
||||||
type extractJob struct {
|
type extractJob struct {
|
||||||
@@ -69,7 +67,6 @@ type extractJobResult struct {
|
|||||||
chunkIndex int
|
chunkIndex int
|
||||||
value erasedExtractArtifact
|
value erasedExtractArtifact
|
||||||
serialized CheckpointArtifact
|
serialized CheckpointArtifact
|
||||||
warnings []contracts.Warning
|
|
||||||
diagnostics []contracts.DiagnosticGroup
|
diagnostics []contracts.DiagnosticGroup
|
||||||
rejected *contracts.RejectedOutput
|
rejected *contracts.RejectedOutput
|
||||||
validationIncomplete bool
|
validationIncomplete bool
|
||||||
@@ -356,7 +353,6 @@ func hydrateRequiredLane(input RunInput, loader CheckpointLoader, doc *source.So
|
|||||||
return state, err
|
return state, err
|
||||||
}
|
}
|
||||||
hydrated := resolution.artifacts[0]
|
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)
|
diagnostics, diagnosticErr := promoteCheckpointDiagnostics(hydrated.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module}, nil)
|
||||||
if diagnosticErr != nil {
|
if diagnosticErr != nil {
|
||||||
return state, fmt.Errorf("promote reused accepted diagnostics: %w", diagnosticErr)
|
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.diagnostics = append(state.diagnostics, diagnostics...)
|
||||||
}
|
}
|
||||||
state.warnings, state.rejected = cloneWarnings(cp.Warnings), cloneRejectedOutputs(cp.Rejected)
|
state.rejected = cloneRejectedOutputs(cp.Rejected)
|
||||||
}
|
}
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
@@ -464,15 +460,14 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
|||||||
return producerAttemptOutput{}, terminal.record(nil, attemptErr)
|
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}
|
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)
|
serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, artifact.LaneID, artifact.ExtractorKey, artifact.SourceID, artifact.Value)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
attemptErr := fmt.Errorf("serialize extract candidate for lane %q chunk %q: %w", lane.ID, chunk.ID, encodeErr)
|
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)
|
return producerAttemptOutput{}, terminal.record(payload, attemptErr)
|
||||||
}
|
}
|
||||||
serializedCandidate.ChunkID, serializedCandidate.ChunkIndex, serializedCandidate.ChunkRef = artifact.ChunkID, artifact.ChunkIndex, artifact.ChunkRef
|
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) {
|
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||||
candidate, ok := output.Value.(extractAttemptValue)
|
candidate, ok := output.Value.(extractAttemptValue)
|
||||||
if !ok {
|
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)
|
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{
|
payload := map[string]any{
|
||||||
"output": debugCheckpointArtifact(candidate.serialized),
|
"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)),
|
"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 {
|
if validationErr != nil {
|
||||||
@@ -510,7 +504,6 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
|||||||
result.validationSummary = &summary
|
result.validationSummary = &summary
|
||||||
result.rejected.Validation = cloneValidationSummaryPtr(result.validationSummary)
|
result.rejected.Validation = cloneValidationSummaryPtr(result.validationSummary)
|
||||||
}
|
}
|
||||||
result.warnings = cloneWarnings(terminalResult.Warnings)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -520,7 +513,7 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
stored, encodeErr := checkpointArtifact(typed.codec, candidate.artifact.LaneID, candidate.artifact.ExtractorKey, candidate.artifact.SourceID, candidate.artifact.Value)
|
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 {
|
if encodeErr != nil {
|
||||||
attemptErr := fmt.Errorf("serialize accepted extract output for lane %q chunk %q: %w", lane.ID, chunk.ID, encodeErr)
|
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)
|
result.err = candidate.terminal.record(payload, attemptErr)
|
||||||
@@ -533,7 +526,6 @@ func (r *Runner) runExtractJob(ctx context.Context, input RunInput, doc *source.
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
result.value, result.serialized = candidate.artifact, stored
|
result.value, result.serialized = candidate.artifact, stored
|
||||||
result.warnings = cloneWarnings(terminalResult.Warnings)
|
|
||||||
result.validationIncomplete = terminalResult.ValidationIncomplete
|
result.validationIncomplete = terminalResult.ValidationIncomplete
|
||||||
result.validationSummary = &summary
|
result.validationSummary = &summary
|
||||||
}
|
}
|
||||||
@@ -554,13 +546,11 @@ func finalizeLaneExtract(checkpoints CheckpointRecorder, stepID string, state *l
|
|||||||
}
|
}
|
||||||
if result.rejected != nil {
|
if result.rejected != nil {
|
||||||
state.rejected = append(state.rejected, *result.rejected)
|
state.rejected = append(state.rejected, *result.rejected)
|
||||||
state.warnings = append(state.warnings, result.warnings...)
|
|
||||||
state.diagnostics = append(state.diagnostics, result.diagnostics...)
|
state.diagnostics = append(state.diagnostics, result.diagnostics...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
state.values = append(state.values, result.value)
|
state.values = append(state.values, result.value)
|
||||||
state.serialized = append(state.serialized, result.serialized)
|
state.serialized = append(state.serialized, result.serialized)
|
||||||
state.warnings = append(state.warnings, result.warnings...)
|
|
||||||
state.diagnostics = append(state.diagnostics, result.diagnostics...)
|
state.diagnostics = append(state.diagnostics, result.diagnostics...)
|
||||||
if result.validationIncomplete {
|
if result.validationIncomplete {
|
||||||
state.incomplete = append(state.incomplete, result.chunkIndex)
|
state.incomplete = append(state.incomplete, result.chunkIndex)
|
||||||
@@ -574,7 +564,7 @@ func finalizeLaneExtract(checkpoints CheckpointRecorder, stepID string, state *l
|
|||||||
state.reuseEligible = false
|
state.reuseEligible = false
|
||||||
}
|
}
|
||||||
if !state.decision.Reused && state.reuseEligible {
|
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)
|
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{
|
results := finalizedExtractResults{
|
||||||
accepted: state.values,
|
accepted: state.values,
|
||||||
serialized: state.serialized,
|
serialized: state.serialized,
|
||||||
warnings: state.warnings,
|
|
||||||
diagnostics: state.diagnostics,
|
diagnostics: state.diagnostics,
|
||||||
rejected: state.rejected,
|
rejected: state.rejected,
|
||||||
incomplete: state.incomplete,
|
incomplete: state.incomplete,
|
||||||
@@ -595,14 +584,13 @@ func (r *Runner) continueLane(ctx context.Context, input RunInput, checkpoints C
|
|||||||
decision: state.decision,
|
decision: state.decision,
|
||||||
reuseEligible: state.reuseEligible,
|
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.diagnosticGroups = append(local.diagnosticGroups, contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: results.diagnostics}).Groups...)
|
||||||
local.Rejected = append(local.Rejected, cloneRejectedOutputs(results.rejected)...)
|
local.Rejected = append(local.Rejected, cloneRejectedOutputs(results.rejected)...)
|
||||||
local.ValidationSummaries = append(local.ValidationSummaries, cloneValidationSummaries(results.validationSummaries)...)
|
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 {
|
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}
|
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}
|
return local, &laneRunError{stage: StageExtract, err: err}
|
||||||
}
|
}
|
||||||
if len(results.accepted) == 0 {
|
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.NormalizeOutputs = append(dst.NormalizeOutputs, cloneSerializedOutputs(src.NormalizeOutputs)...)
|
||||||
dst.Rejected = append(dst.Rejected, cloneRejectedOutputs(src.Rejected)...)
|
dst.Rejected = append(dst.Rejected, cloneRejectedOutputs(src.Rejected)...)
|
||||||
dst.Warnings = append(dst.Warnings, cloneWarnings(src.Warnings)...)
|
|
||||||
appendDiagnosticGroups(dst, src.diagnosticGroups)
|
appendDiagnosticGroups(dst, src.diagnosticGroups)
|
||||||
dst.CheckpointEvents = append(dst.CheckpointEvents, src.CheckpointEvents...)
|
dst.CheckpointEvents = append(dst.CheckpointEvents, src.CheckpointEvents...)
|
||||||
dst.ValidationSummaries = append(dst.ValidationSummaries, cloneValidationSummaries(src.ValidationSummaries)...)
|
dst.ValidationSummaries = append(dst.ValidationSummaries, cloneValidationSummaries(src.ValidationSummaries)...)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"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 {
|
func containsValidationSummary(summaries []artifacts.ValidationSummary, status string) bool {
|
||||||
for _, summary := range summaries {
|
for _, summary := range summaries {
|
||||||
if summary.Status == status {
|
if summary.Status == status {
|
||||||
@@ -126,8 +116,8 @@ func TestRunnerContinuesValidationIncompleteExtractWithoutCheckpoint(t *testing.
|
|||||||
if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
|
if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
|
||||||
t.Fatalf("manifest validation = %#v, want incomplete extract provenance", output.Manifest)
|
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) {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
|
||||||
t.Fatalf("warnings = %#v, want %#v", got, want)
|
t.Fatalf("diagnostics = %#v, want validation-incomplete group", output.Diagnostics)
|
||||||
}
|
}
|
||||||
if len(recorder.checkpoint.Outputs) != 0 || len(recorder.checkpoint.Rejected) != 0 {
|
if len(recorder.checkpoint.Outputs) != 0 || len(recorder.checkpoint.Rejected) != 0 {
|
||||||
t.Fatalf("extract checkpoint = %#v, want no persisted incomplete output", recorder.checkpoint)
|
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
|
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{
|
r.checkpoint = ExtractCheckpoint{
|
||||||
Outputs: cloneCheckpointArtifacts(outputs),
|
Outputs: cloneCheckpointArtifacts(outputs),
|
||||||
Rejected: cloneRejectedOutputs(rejected),
|
Rejected: cloneRejectedOutputs(rejected),
|
||||||
Warnings: cloneWarnings(warnings),
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -56,7 +55,6 @@ func (l *extractResultLoader) Extract(string, string, []CheckpointFingerprint) (
|
|||||||
return ExtractCheckpoint{
|
return ExtractCheckpoint{
|
||||||
Outputs: cloneCheckpointArtifacts(l.checkpoint.Outputs),
|
Outputs: cloneCheckpointArtifacts(l.checkpoint.Outputs),
|
||||||
Rejected: cloneRejectedOutputs(l.checkpoint.Rejected),
|
Rejected: cloneRejectedOutputs(l.checkpoint.Rejected),
|
||||||
Warnings: cloneWarnings(l.checkpoint.Warnings),
|
|
||||||
}, l.decision
|
}, l.decision
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +154,7 @@ func TestRunnerContinuesFromFreshAndReusedExtractResults(t *testing.T) {
|
|||||||
installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
||||||
extractCalls++
|
extractCalls++
|
||||||
return erasedTypedResult{
|
return erasedTypedResult{
|
||||||
Value: typedValueForLane(0, request.Chunk.Index),
|
Value: typedValueForLane(0, request.Chunk.Index),
|
||||||
Warnings: []contracts.Warning{{Scope: "extract", ReasonCode: "observed", Message: "accepted extract"}},
|
|
||||||
Diagnostics: []contracts.ProducerDiagnostic{{
|
Diagnostics: []contracts.ProducerDiagnostic{{
|
||||||
Disposition: contracts.DiagnosticDispositionObservation,
|
Disposition: contracts.DiagnosticDispositionObservation,
|
||||||
Category: contracts.DiagnosticCategoryNormalization,
|
Category: contracts.DiagnosticCategoryNormalization,
|
||||||
@@ -213,15 +210,12 @@ func TestRunnerContinuesFromFreshAndReusedExtractResults(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(reused.NormalizeOutputs, fresh.NormalizeOutputs) {
|
if !reflect.DeepEqual(reused.NormalizeOutputs, fresh.NormalizeOutputs) {
|
||||||
t.Fatalf("reused normalize outputs = %#v, want fresh outputs %#v", 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) {
|
if !reflect.DeepEqual(reused.Diagnostics, fresh.Diagnostics) {
|
||||||
t.Fatalf("reused diagnostics = %#v, want fresh diagnostics %#v", 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 := preparedAttemptDebugPipeline(t)
|
||||||
prepared.Steps[0].lanes[0].resolved.Extract.Retries = 1
|
prepared.Steps[0].lanes[0].resolved.Extract.Retries = 1
|
||||||
attempts := 0
|
attempts := 0
|
||||||
@@ -233,7 +227,7 @@ func TestRunnerPromotesOnlyAcceptedExtractRetryWarnings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return erasedTypedResult{
|
return erasedTypedResult{
|
||||||
Value: typedValueForLane(0, request.Chunk.Index),
|
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)),
|
ModelCandidate: attemptCandidate(t, fmt.Sprintf(`{"items":["%s"]}`, scope)),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
@@ -250,8 +244,8 @@ func TestRunnerPromotesOnlyAcceptedExtractRetryWarnings(t *testing.T) {
|
|||||||
if attempts != 2 {
|
if attempts != 2 {
|
||||||
t.Fatalf("extract attempts = %d, want 2", attempts)
|
t.Fatalf("extract attempts = %d, want 2", attempts)
|
||||||
}
|
}
|
||||||
if len(output.Warnings) != 1 || output.Warnings[0].Scope != "accepted" {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].Samples[0].Scope != "accepted" {
|
||||||
t.Fatalf("promoted warnings = %#v, want accepted attempt only", output.Warnings)
|
t.Fatalf("promoted diagnostics = %#v, want accepted attempt only", output.Diagnostics)
|
||||||
}
|
}
|
||||||
assertAttemptEnvelopeSequence(t, debug, "extract/notes/chunk-000001", 1, 2)
|
assertAttemptEnvelopeSequence(t, debug, "extract/notes/chunk-000001", 1, 2)
|
||||||
first := debug.envelope(t, "extract/notes/chunk-000001/attempt-01.json")
|
first := debug.envelope(t, "extract/notes/chunk-000001/attempt-01.json")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
|||||||
validator *preparedValidator
|
validator *preparedValidator
|
||||||
wantCalls int
|
wantCalls int
|
||||||
wantItem string
|
wantItem string
|
||||||
wantWarnings []string
|
wantMessages []string
|
||||||
wantRejected int
|
wantRejected int
|
||||||
wantDebug []string
|
wantDebug []string
|
||||||
wantCheckpoint int
|
wantCheckpoint int
|
||||||
@@ -30,7 +30,7 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantCalls: 1,
|
wantCalls: 1,
|
||||||
wantItem: "fallback",
|
wantItem: "fallback",
|
||||||
wantWarnings: []string{"ordinary", "fallback-warning"},
|
wantMessages: []string{"ordinary", "fallback-warning"},
|
||||||
wantDebug: []string{`"another_attempt":false`, `"fallback_accepted":true`},
|
wantDebug: []string{`"another_attempt":false`, `"fallback_accepted":true`},
|
||||||
wantCheckpoint: 1,
|
wantCheckpoint: 1,
|
||||||
},
|
},
|
||||||
@@ -41,11 +41,11 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
|||||||
if attempt == 1 {
|
if attempt == 1 {
|
||||||
return retryableNormalizeResult("discarded", "discarded-ordinary", "discarded-fallback")
|
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,
|
wantCalls: 2,
|
||||||
wantItem: "accepted",
|
wantItem: "accepted",
|
||||||
wantWarnings: []string{"accepted-warning"},
|
wantMessages: []string{"accepted-warning"},
|
||||||
wantDebug: []string{`"another_attempt":true`, `"fallback_accepted":false`},
|
wantDebug: []string{`"another_attempt":true`, `"fallback_accepted":false`},
|
||||||
wantCheckpoint: 1,
|
wantCheckpoint: 1,
|
||||||
},
|
},
|
||||||
@@ -57,7 +57,7 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantCalls: 2,
|
wantCalls: 2,
|
||||||
wantItem: "fallback-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`},
|
wantDebug: []string{`"another_attempt":false`, `"fallback_accepted":true`},
|
||||||
wantCheckpoint: 1,
|
wantCheckpoint: 1,
|
||||||
},
|
},
|
||||||
@@ -140,12 +140,14 @@ func TestRunnerHandlesRetryableNormalizeFallbacks(t *testing.T) {
|
|||||||
if got := firstNote(normalized); got != tc.wantItem {
|
if got := firstNote(normalized); got != tc.wantItem {
|
||||||
t.Fatalf("normalized item = %q, want %q", got, tc.wantItem)
|
t.Fatalf("normalized item = %q, want %q", got, tc.wantItem)
|
||||||
}
|
}
|
||||||
gotWarnings := make([]string, len(output.Warnings))
|
var gotMessages []string
|
||||||
for index, warning := range output.Warnings {
|
for _, group := range output.Diagnostics.Groups {
|
||||||
gotWarnings[index] = warning.Message
|
for _, sample := range group.Samples {
|
||||||
|
gotMessages = append(gotMessages, sample.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.Join(gotWarnings, "|") != strings.Join(tc.wantWarnings, "|") {
|
if strings.Join(gotMessages, "|") != strings.Join(tc.wantMessages, "|") {
|
||||||
t.Fatalf("durable warnings = %#v, want %#v", gotWarnings, tc.wantWarnings)
|
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 {
|
func retryableNormalizeResult(item, ordinary, fallback string) erasedTypedResult {
|
||||||
return erasedTypedResult{
|
return erasedTypedResult{
|
||||||
Value: codecNotes{Items: []string{item}},
|
Value: codecNotes{Items: []string{item}},
|
||||||
Warnings: []contracts.Warning{{Scope: "attempt", ReasonCode: "ordinary", Message: ordinary}},
|
Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("attempt", ordinary)},
|
||||||
Retry: &contracts.NormalizeRetry{
|
Retry: &contracts.NormalizeRetry{
|
||||||
ReasonCode: "retryable_normalization",
|
ReasonCode: "retryable_normalization",
|
||||||
Message: "safe fallback is available",
|
Message: "safe fallback is available",
|
||||||
FallbackWarnings: []contracts.Warning{{Scope: "fallback", ReasonCode: "fallback", Message: fallback}},
|
FallbackDiagnostics: []contracts.ProducerDiagnostic{producerDiagnostic("fallback", fallback)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type warningChunker struct {
|
type diagnosticChunker struct {
|
||||||
key string
|
key string
|
||||||
plan source.ChunkPlan
|
plan source.ChunkPlan
|
||||||
calls int
|
calls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *warningChunker) Key() string { return c.key }
|
func (c *diagnosticChunker) Key() string { return c.key }
|
||||||
func (*warningChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
func (*diagnosticChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
||||||
func (c *warningChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
func (c *diagnosticChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||||
c.calls++
|
c.calls++
|
||||||
return contracts.ChunkPlanResult{
|
return contracts.ChunkPlanResult{
|
||||||
Plan: source.CloneChunkPlan(c.plan),
|
Plan: source.CloneChunkPlan(c.plan),
|
||||||
Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", c.calls), ReasonCode: "operation", Message: "operation warning"}},
|
Diagnostics: []contracts.ProducerDiagnostic{producerDiagnostic(fmt.Sprintf("operation-%d", c.calls), "operation diagnostic")},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,14 +40,14 @@ func (v chunkValidationFunc) Validate(ctx context.Context, request contracts.Chu
|
|||||||
return v.validate(ctx, request)
|
return v.validate(ctx, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
|
func TestRunnerPromotesOnlyTerminalRejectionDiagnostics(t *testing.T) {
|
||||||
for _, target := range []ModuleStage{StageChunk, StageExtract, StageMerge, StageNormalize} {
|
for _, target := range []ModuleStage{StageChunk, StageExtract, StageMerge, StageNormalize} {
|
||||||
t.Run(string(target), func(t *testing.T) {
|
t.Run(string(target), func(t *testing.T) {
|
||||||
prepared := preparedAttemptDebugPipeline(t)
|
prepared := preparedAttemptDebugPipeline(t)
|
||||||
lane := &prepared.Steps[0].lanes[0]
|
lane := &prepared.Steps[0].lanes[0]
|
||||||
attempts := 0
|
attempts := 0
|
||||||
first := func() contracts.ValidationResult {
|
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 {
|
reject := func() contracts.ValidationResult {
|
||||||
return contracts.ValidationResult{Approved: false, ReasonCode: "rejected", Message: "rejected", CorrectionGuidance: "return an acceptable candidate"}
|
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:
|
case StageChunk:
|
||||||
prepared.resolved.ChunkValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
prepared.resolved.ChunkValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||||
chunker := prepared.chunker.(*typedTestChunker)
|
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.resolved.Chunk.Retries = 1
|
||||||
prepared.chunkValidators.validators = []preparedValidator{
|
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) {
|
{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
|
return reject(), nil
|
||||||
}}},
|
}}},
|
||||||
}
|
}
|
||||||
chunkerWithWarnings := prepared.chunker.(*warningChunker)
|
chunkerWithDiagnostics := prepared.chunker.(*diagnosticChunker)
|
||||||
first = func() contracts.ValidationResult {
|
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:
|
case StageExtract:
|
||||||
lane.resolved.Extract.Retries = 1
|
lane.resolved.Extract.Retries = 1
|
||||||
lane.resolved.ExtractValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
lane.resolved.ExtractValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||||
installExtractOperation(prepared, 0, func(context.Context, contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
installExtractOperation(prepared, 0, func(context.Context, contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
||||||
attempts++
|
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:
|
case StageMerge:
|
||||||
lane.resolved.Merge.Retries = 1
|
lane.resolved.Merge.Retries = 1
|
||||||
lane.resolved.MergeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
lane.resolved.MergeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||||
lane.typed.merge = func(context.Context, any, contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
|
lane.typed.merge = func(context.Context, any, contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
|
||||||
attempts++
|
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:
|
case StageNormalize:
|
||||||
lane.resolved.Normalize.Retries = 1
|
lane.resolved.Normalize.Retries = 1
|
||||||
lane.resolved.NormalizeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
lane.resolved.NormalizeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
|
||||||
lane.typed.normalize = func(context.Context, any, contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
|
lane.typed.normalize = func(context.Context, any, contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
|
||||||
attempts++
|
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})
|
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"}
|
wantScopes = []string{"operation-1", "validator-1"}
|
||||||
wantAttempts = 1
|
wantAttempts = 1
|
||||||
}
|
}
|
||||||
if got := rejectionWarningScopes(output.Warnings); !reflect.DeepEqual(got, wantScopes) {
|
if got := rejectionDiagnosticScopes(output.Diagnostics.Groups); !reflect.DeepEqual(got, wantScopes) {
|
||||||
t.Fatalf("published warning scopes = %#v, want %#v", got, wantScopes)
|
t.Fatalf("published diagnostic scopes = %#v, want %#v", got, wantScopes)
|
||||||
}
|
}
|
||||||
if len(output.Rejected) != 1 || output.Rejected[0].AttemptCount != wantAttempts {
|
if len(output.Rejected) != 1 || output.Rejected[0].AttemptCount != wantAttempts {
|
||||||
t.Fatalf("rejections = %#v, want terminal rejection after %d attempt(s)", output.Rejected, 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)
|
attemptPath := fmt.Sprintf("%s/notes/attempt-01.json", target)
|
||||||
if target == StageChunk {
|
if target == StageChunk {
|
||||||
attemptPath = "chunk/attempt-01.json"
|
attemptPath = "chunk/attempt-01.json"
|
||||||
} else if target == StageExtract {
|
} else if target == StageExtract {
|
||||||
attemptPath = "extract/notes/chunk-000001/attempt-01.json"
|
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") {
|
if !strings.Contains(string(debug.json[attemptPath]), "rejection") {
|
||||||
t.Fatalf("first attempt debug = %s, want discarded warnings", debug.json[attemptPath])
|
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{
|
return []preparedValidator{
|
||||||
{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
|
{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
|
||||||
return first(), nil
|
return first(), nil
|
||||||
@@ -142,10 +139,10 @@ func rejectionWarningTypedValidators(first func() contracts.ValidationResult, re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rejectionWarningScopes(warnings []contracts.Warning) []string {
|
func rejectionDiagnosticScopes(groups []contracts.DiagnosticGroup) []string {
|
||||||
scopes := make([]string, len(warnings))
|
scopes := make([]string, len(groups))
|
||||||
for index := range warnings {
|
for index := range groups {
|
||||||
scopes[index] = warnings[index].Scope
|
scopes[index] = groups[index].Samples[0].Scope
|
||||||
}
|
}
|
||||||
return scopes
|
return scopes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (s *reuseLineageCheckpointSpy) ExtractRunning(laneID, _ string, _ []Checkpo
|
|||||||
return nil
|
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)
|
s.write("extract", "succeeded", laneID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ func (s *reuseLineageCheckpointSpy) MergeRunning(laneID, _ string, _ []Checkpoin
|
|||||||
return nil
|
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)
|
s.write("merge", "succeeded", laneID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ func (s *reuseLineageCheckpointSpy) NormalizeRunning(laneID, _ string, _ []Check
|
|||||||
return nil
|
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)
|
s.write("normalize", "succeeded", laneID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func TestRunnerContinuesNormalizeAfterValidatorFailure(t *testing.T) {
|
|||||||
if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
|
if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
|
||||||
t.Fatalf("manifest validation = %#v, want incomplete normalization provenance", output.Manifest)
|
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) {
|
if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
|
||||||
t.Fatalf("warnings = %#v, want %#v", got, want)
|
t.Fatalf("diagnostics = %#v, want validation-incomplete group", output.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
type terminalChunker struct {
|
type terminalChunker struct {
|
||||||
key string
|
key string
|
||||||
plan source.ChunkPlan
|
plan source.ChunkPlan
|
||||||
warnings []contracts.Warning
|
|
||||||
diagnostics []contracts.ProducerDiagnostic
|
diagnostics []contracts.ProducerDiagnostic
|
||||||
err error
|
err error
|
||||||
calls *int
|
calls *int
|
||||||
@@ -30,7 +29,7 @@ func (c terminalChunker) Plan(context.Context, contracts.ChunkRequest) (contract
|
|||||||
if c.calls != nil {
|
if c.calls != nil {
|
||||||
(*c.calls)++
|
(*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 {
|
type terminalChunkValidator struct {
|
||||||
@@ -168,7 +167,7 @@ func TestRunnerRecordsChunkTerminalOutcomes(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
prepared, plan := preparedTerminalDebugPipeline(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}}
|
prepared.chunkValidators.validators = []preparedValidator{{resolved: ResolvedValidator{Binding: Binding("terminal/chunk-validator"), Target: ValidatorTargetChunk}, chunk: tc.validator}}
|
||||||
debug := newCapturedDebugRecorder()
|
debug := newCapturedDebugRecorder()
|
||||||
|
|
||||||
@@ -337,7 +336,7 @@ func TestRunnerRecordsExtractTerminalOutcomes(t *testing.T) {
|
|||||||
if tc.moduleError != nil {
|
if tc.moduleError != nil {
|
||||||
return erasedTypedResult{}, tc.moduleError
|
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{{
|
prepared.Steps[0].lanes[0].extractValidators.validators = []preparedValidator{{
|
||||||
resolved: ResolvedValidator{Binding: Binding("terminal/extract-validator"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"},
|
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)
|
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)
|
return loader.Normalize(laneID, moduleKey, deps)
|
||||||
}
|
}
|
||||||
func recordMerge(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
func recordMerge(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||||
return checkpointMergeSucceeded(recorder, stepID, laneID, moduleKey, deps, output, warnings)
|
return checkpointMergeSucceeded(recorder, stepID, laneID, moduleKey, deps, output)
|
||||||
}
|
}
|
||||||
func recordNormalize(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
|
func recordNormalize(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
|
||||||
return checkpointNormalizeSucceeded(recorder, stepID, laneID, moduleKey, deps, output, warnings)
|
return checkpointNormalizeSucceeded(recorder, stepID, laneID, moduleKey, deps, output)
|
||||||
}
|
}
|
||||||
func cloneCheckpointArtifact(output CheckpointArtifact) CheckpointArtifact {
|
func cloneCheckpointArtifact(output CheckpointArtifact) CheckpointArtifact {
|
||||||
output.Artifact = contracts.CloneSerializedArtifact(output.Artifact)
|
output.Artifact = contracts.CloneSerializedArtifact(output.Artifact)
|
||||||
@@ -265,12 +265,9 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
|||||||
}
|
}
|
||||||
var merged erasedMergeArtifact
|
var merged erasedMergeArtifact
|
||||||
var serializedMerge CheckpointArtifact
|
var serializedMerge CheckpointArtifact
|
||||||
var mergeWarnings []contracts.Warning
|
|
||||||
if mergeDecision.Reused {
|
if mergeDecision.Reused {
|
||||||
merged = erasedMergeArtifact{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: mergeResolution.values[0]}
|
merged = erasedMergeArtifact{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: mergeResolution.values[0]}
|
||||||
serializedMerge = mergeResolution.artifacts[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)
|
diagnostics, diagnosticErr := promoteCheckpointDiagnostics(serializedMerge.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageMerge, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module}, nil)
|
||||||
if diagnosticErr != nil {
|
if diagnosticErr != nil {
|
||||||
return stageResult, fmt.Errorf("promote reused merge diagnostics: %w", diagnosticErr)
|
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))
|
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}
|
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)
|
serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, candidate.LaneID, candidate.MergerKey, candidate.SourceID, candidate.Value)
|
||||||
if encodeErr != nil {
|
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) {
|
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||||
candidate, ok := output.Value.(mergeAttemptValue)
|
candidate, ok := output.Value.(mergeAttemptValue)
|
||||||
if !ok {
|
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}
|
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)
|
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 {
|
if validationErr != nil {
|
||||||
return report, candidate.terminal.record(payload, validationErr)
|
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.Stage, rejected.StepID, rejected.LaneID, rejected.ModuleKey = string(StageMerge), input.stepID, lane.ID, lane.Merge.Module
|
||||||
rejected.Validation = &terminalSummary
|
rejected.Validation = &terminalSummary
|
||||||
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(terminalSummary))
|
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(terminalSummary))
|
||||||
output.Warnings = append(output.Warnings, terminalResult.Warnings...)
|
|
||||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||||
output.Rejected = append(output.Rejected, *rejected)
|
output.Rejected = append(output.Rejected, *rejected)
|
||||||
if stageResult.reuseEligible {
|
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")
|
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)
|
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 {
|
if encodeErr != nil {
|
||||||
attemptErr := fmt.Errorf("serialize accepted merge output for lane %q: %w", lane.ID, encodeErr)
|
attemptErr := fmt.Errorf("serialize accepted merge output for lane %q: %w", lane.ID, encodeErr)
|
||||||
if stageResult.reuseEligible {
|
if stageResult.reuseEligible {
|
||||||
@@ -372,21 +367,19 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
|||||||
return stageResult, debugErr
|
return stageResult, debugErr
|
||||||
}
|
}
|
||||||
merged, serializedMerge = candidate.artifact, stored
|
merged, serializedMerge = candidate.artifact, stored
|
||||||
mergeWarnings = cloneWarnings(terminalResult.Warnings)
|
|
||||||
output.ValidationSummaries = append(output.ValidationSummaries, terminalSummary)
|
output.ValidationSummaries = append(output.ValidationSummaries, terminalSummary)
|
||||||
stageResult.validationIncomplete = terminalResult.ValidationIncomplete
|
stageResult.validationIncomplete = terminalResult.ValidationIncomplete
|
||||||
if stageResult.validationIncomplete {
|
if stageResult.validationIncomplete {
|
||||||
stageResult.reuseEligible = false
|
stageResult.reuseEligible = false
|
||||||
}
|
}
|
||||||
output.Warnings = append(output.Warnings, mergeWarnings...)
|
|
||||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||||
if stageResult.reuseEligible {
|
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
|
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
|
return stageResult, err
|
||||||
}
|
}
|
||||||
stageResult.artifact = merged
|
stageResult.artifact = merged
|
||||||
@@ -396,7 +389,6 @@ func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints
|
|||||||
|
|
||||||
type normalizeStageResult struct {
|
type normalizeStageResult struct {
|
||||||
serialized CheckpointArtifact
|
serialized CheckpointArtifact
|
||||||
warnings []contracts.Warning
|
|
||||||
accepted bool
|
accepted bool
|
||||||
reuseEligible bool
|
reuseEligible bool
|
||||||
}
|
}
|
||||||
@@ -422,11 +414,8 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
|||||||
return stageResult, err
|
return stageResult, err
|
||||||
}
|
}
|
||||||
var serializedNormalize CheckpointArtifact
|
var serializedNormalize CheckpointArtifact
|
||||||
var normalizeWarnings []contracts.Warning
|
|
||||||
if normalizeDecision.Reused {
|
if normalizeDecision.Reused {
|
||||||
serializedNormalize = normalizeResolution.artifacts[0]
|
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)
|
diagnostics, diagnosticErr := promoteCheckpointDiagnostics(serializedNormalize.Diagnostics, contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageNormalize, StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module}, nil)
|
||||||
if diagnosticErr != nil {
|
if diagnosticErr != nil {
|
||||||
return stageResult, fmt.Errorf("promote reused normalize diagnostics: %w", diagnosticErr)
|
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 {
|
if callErr != nil {
|
||||||
return producerAttemptOutput{}, terminal.record(nil, fmt.Errorf("normalize lane %q with normalizer %q: %w", lane.ID, lane.Normalize.Module, callErr))
|
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)
|
serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, lane.ID, lane.Normalize.Module, doc.ID, result.Value)
|
||||||
if encodeErr != nil {
|
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}
|
attemptValue := normalizeAttemptValue{value: result.Value, candidate: serializedCandidate, terminal: &terminal}
|
||||||
var directive *producerRetryDirective
|
var directive *producerRetryDirective
|
||||||
if result.Retry != nil {
|
if result.Retry != nil {
|
||||||
if err := validateNormalizeRetry(result.Retry); err != 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
|
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}
|
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 {
|
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 {
|
if debugErr := terminal.record(payload, nil); debugErr != nil {
|
||||||
return producerAttemptOutput{}, debugErr
|
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) {
|
}, func(validationCtx context.Context, output producerAttemptOutput) (validationReport, error) {
|
||||||
candidate, ok := output.Value.(normalizeAttemptValue)
|
candidate, ok := output.Value.(normalizeAttemptValue)
|
||||||
if !ok {
|
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}
|
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)
|
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 {
|
if candidate.retry != nil {
|
||||||
payload["retry"] = candidate.retry
|
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.Stage, rejected.StepID, rejected.LaneID, rejected.ModuleKey = string(StageNormalize), input.stepID, lane.ID, lane.Normalize.Module
|
||||||
rejected.Validation = &terminalSummary
|
rejected.Validation = &terminalSummary
|
||||||
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(terminalSummary))
|
output.ValidationSummaries = append(output.ValidationSummaries, artifacts.CloneValidationSummary(terminalSummary))
|
||||||
output.Warnings = append(output.Warnings, terminalResult.Warnings...)
|
|
||||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||||
output.Rejected = append(output.Rejected, *rejected)
|
output.Rejected = append(output.Rejected, *rejected)
|
||||||
if stageResult.reuseEligible {
|
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")
|
return stageResult, fmt.Errorf("normalize attempt terminal has incompatible value")
|
||||||
}
|
}
|
||||||
stored, encodeErr := checkpointArtifact(typed.codec, lane.ID, lane.Normalize.Module, doc.ID, candidate.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 {
|
if candidate.retry != nil {
|
||||||
payload["retry"] = candidate.retry
|
payload["retry"] = candidate.retry
|
||||||
}
|
}
|
||||||
@@ -548,24 +535,21 @@ func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoi
|
|||||||
return stageResult, debugErr
|
return stageResult, debugErr
|
||||||
}
|
}
|
||||||
serializedNormalize = stored
|
serializedNormalize = stored
|
||||||
normalizeWarnings = cloneWarnings(terminalResult.Warnings)
|
|
||||||
output.ValidationSummaries = append(output.ValidationSummaries, terminalSummary)
|
output.ValidationSummaries = append(output.ValidationSummaries, terminalSummary)
|
||||||
output.Warnings = append(output.Warnings, normalizeWarnings...)
|
|
||||||
appendDiagnosticGroups(output, terminalDiagnostics)
|
appendDiagnosticGroups(output, terminalDiagnostics)
|
||||||
if terminalResult.ValidationIncomplete {
|
if terminalResult.ValidationIncomplete {
|
||||||
stageResult.reuseEligible = false
|
stageResult.reuseEligible = false
|
||||||
}
|
}
|
||||||
if stageResult.reuseEligible {
|
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
|
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
|
return stageResult, err
|
||||||
}
|
}
|
||||||
stageResult.serialized = serializedNormalize
|
stageResult.serialized = serializedNormalize
|
||||||
stageResult.warnings = normalizeWarnings
|
|
||||||
stageResult.accepted = true
|
stageResult.accepted = true
|
||||||
return stageResult, nil
|
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 {
|
func (r *lockedCheckpointRecorder) ExtractRunning(lane, key string, deps []CheckpointFingerprint) error {
|
||||||
return r.call(func() error { return r.inner.ExtractRunning(lane, key, deps) })
|
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 {
|
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, warnings) })
|
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 {
|
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) })
|
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 {
|
func (r *lockedCheckpointRecorder) MergeRunning(lane, key string, deps []CheckpointFingerprint) error {
|
||||||
return r.call(func() error { return r.inner.MergeRunning(lane, key, deps) })
|
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 {
|
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, warnings) })
|
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 {
|
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) })
|
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 {
|
func (r *lockedCheckpointRecorder) NormalizeRunning(lane, key string, deps []CheckpointFingerprint) error {
|
||||||
return r.call(func() error { return r.inner.NormalizeRunning(lane, key, deps) })
|
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 {
|
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, warnings) })
|
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 {
|
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) })
|
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)
|
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 {
|
return r.call(func() error {
|
||||||
if v, ok := r.inner.(StepCheckpointRecorder); ok {
|
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 {
|
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)
|
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 {
|
return r.call(func() error {
|
||||||
if v, ok := r.inner.(StepCheckpointRecorder); ok {
|
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 {
|
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)
|
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 {
|
return r.call(func() error {
|
||||||
if v, ok := r.inner.(StepCheckpointRecorder); ok {
|
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 {
|
func (r *lockedCheckpointRecorder) NormalizeRejectedForStep(step, lane, key string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ type erasedMergeArtifact struct {
|
|||||||
|
|
||||||
type erasedTypedResult struct {
|
type erasedTypedResult struct {
|
||||||
Value any
|
Value any
|
||||||
Warnings []contracts.Warning
|
|
||||||
Diagnostics []contracts.ProducerDiagnostic
|
Diagnostics []contracts.ProducerDiagnostic
|
||||||
Retry *contracts.NormalizeRetry
|
Retry *contracts.NormalizeRetry
|
||||||
ModelCandidate *contracts.ModelCandidate
|
ModelCandidate *contracts.ModelCandidate
|
||||||
|
|||||||
@@ -29,14 +29,12 @@ type validationRecord struct {
|
|||||||
reasonCode string
|
reasonCode string
|
||||||
message string
|
message string
|
||||||
diagnosticPath string
|
diagnosticPath string
|
||||||
warnings []contracts.Warning
|
|
||||||
diagnostics []contracts.ProducerDiagnostic
|
diagnostics []contracts.ProducerDiagnostic
|
||||||
correctionGuidance string
|
correctionGuidance string
|
||||||
failure error
|
failure error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (record validationRecord) clone() validationRecord {
|
func (record validationRecord) clone() validationRecord {
|
||||||
record.warnings = cloneWarnings(record.warnings)
|
|
||||||
record.diagnostics = contracts.CloneProducerDiagnostics(record.diagnostics)
|
record.diagnostics = contracts.CloneProducerDiagnostics(record.diagnostics)
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
@@ -54,16 +52,6 @@ func (report validationReport) Records() []validationRecord {
|
|||||||
return records
|
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 {
|
func (report validationReport) Diagnostics() []validationDiagnosticRecord {
|
||||||
var diagnostics []validationDiagnosticRecord
|
var diagnostics []validationDiagnosticRecord
|
||||||
for _, record := range report.records {
|
for _, record := range report.records {
|
||||||
@@ -207,14 +195,14 @@ func executeValidationChain(ctx context.Context, chain preparedValidatorChain, i
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if invocation.result.Approved {
|
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
|
break
|
||||||
}
|
}
|
||||||
message := invocation.result.Message
|
message := invocation.result.Message
|
||||||
if message == "" {
|
if message == "" {
|
||||||
message = "output rejected"
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,24 +12,24 @@ import (
|
|||||||
|
|
||||||
func TestExecuteValidationChainSettlesEveryValidatorInOrder(t *testing.T) {
|
func TestExecuteValidationChainSettlesEveryValidatorInOrder(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
chain preparedValidatorChain
|
chain preparedValidatorChain
|
||||||
invoke validationInvoker
|
invoke validationInvoker
|
||||||
want []validationOutcome
|
want []validationOutcome
|
||||||
wantCalls []string
|
wantCalls []string
|
||||||
wantGuidance []string
|
wantGuidance []string
|
||||||
wantWarnings []string
|
wantDiagnostics []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all approved",
|
name: "all approved",
|
||||||
chain: validationChain(validationSpec("shape", contracts.ExecutionClassDeterministic, 0), validationSpec("refs", contracts.ExecutionClassDeterministic, 0)),
|
chain: validationChain(validationSpec("shape", contracts.ExecutionClassDeterministic, 0), validationSpec("refs", contracts.ExecutionClassDeterministic, 0)),
|
||||||
invoke: validationSequence(map[string][]validationStep{
|
invoke: validationSequence(map[string][]validationStep{
|
||||||
"shape": {{result: contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{ReasonCode: "shape"}}}}},
|
"shape": {{result: contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{validationDiagnostic("shape")}}}},
|
||||||
"refs": {{result: contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{ReasonCode: "refs"}}}}},
|
"refs": {{result: contracts.ValidationResult{Approved: true, Diagnostics: []contracts.ProducerDiagnostic{validationDiagnostic("refs")}}}},
|
||||||
}),
|
}),
|
||||||
want: []validationOutcome{validationApproved, validationApproved},
|
want: []validationOutcome{validationApproved, validationApproved},
|
||||||
wantCalls: []string{"shape:1", "refs:1"},
|
wantCalls: []string{"shape:1", "refs:1"},
|
||||||
wantWarnings: []string{"shape", "refs"},
|
wantDiagnostics: []string{"shape", "refs"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple rejections deduplicate guidance",
|
name: "multiple rejections deduplicate guidance",
|
||||||
@@ -117,13 +117,13 @@ func TestExecuteValidationChainSettlesEveryValidatorInOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warnings := report.Warnings()
|
diagnostics := report.Diagnostics()
|
||||||
var warningCodes []string
|
var diagnosticCodes []string
|
||||||
for _, warning := range warnings {
|
for _, record := range diagnostics {
|
||||||
warningCodes = append(warningCodes, warning.ReasonCode)
|
diagnosticCodes = append(diagnosticCodes, record.diagnostic.ReasonCode)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(warningCodes, test.wantWarnings) {
|
if !reflect.DeepEqual(diagnosticCodes, test.wantDiagnostics) {
|
||||||
t.Fatalf("Warnings() = %#v, want codes %#v", warnings, test.wantWarnings)
|
t.Fatalf("Diagnostics() = %#v, want codes %#v", diagnostics, test.wantDiagnostics)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -158,14 +158,24 @@ func TestValidationReportRejectsOversizedGuidanceAndOwnsRecords(t *testing.T) {
|
|||||||
unique[0] = byte('a' + index)
|
unique[0] = byte('a' + index)
|
||||||
report.records[index] = validationRecord{validatorName: "validator", outcome: validationRejected, correctionGuidance: string(unique)}
|
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 {
|
if _, err := report.CorrectionRequest(); err == nil {
|
||||||
t.Fatal("CorrectionRequest() error = nil, want aggregate overflow error")
|
t.Fatal("CorrectionRequest() error = nil, want aggregate overflow error")
|
||||||
}
|
}
|
||||||
records := report.Records()
|
records := report.Records()
|
||||||
records[0].warnings[0].ReasonCode = "changed"
|
records[0].diagnostics[0].ReasonCode = "changed"
|
||||||
if report.Records()[0].warnings[0].ReasonCode != "warning" {
|
if report.Records()[0].diagnostics[0].ReasonCode != "diagnostic" {
|
||||||
t.Fatal("Records() exposed mutable warning storage")
|
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}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,9 +179,6 @@ func TestPlanReturnsAnnotationFreeSceneRangesFromStructuredOutput(t *testing.T)
|
|||||||
if len(result.Plan.Annotations) != 0 {
|
if len(result.Plan.Annotations) != 0 {
|
||||||
t.Fatalf("plan annotations = %#v, want absent", result.Plan.Annotations)
|
t.Fatalf("plan annotations = %#v, want absent", result.Plan.Annotations)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 {
|
|
||||||
t.Fatalf("warnings = %#v, want absent", result.Warnings)
|
|
||||||
}
|
|
||||||
wantCandidate, err := json.Marshal(client.response)
|
wantCandidate, err := json.Marshal(client.response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal expected candidate: %v", err)
|
t.Fatalf("marshal expected candidate: %v", err)
|
||||||
|
|||||||
@@ -310,10 +310,10 @@ func TestExtractAppliesSceneEligibilityBeforePromptConstruction(t *testing.T) {
|
|||||||
t.Fatal("CombatTurns = nil, want accepted non-nil empty list")
|
t.Fatal("CombatTurns = nil, want accepted non-nil empty list")
|
||||||
}
|
}
|
||||||
if test.wantWarning != "" {
|
if test.wantWarning != "" {
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot || result.Diagnostics[0].ReasonCode != test.wantWarning || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation {
|
if len(result.Diagnostics) != 1 || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot || result.Diagnostics[0].ReasonCode != test.wantWarning || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
} else if len(result.Warnings) != 0 || len(result.Diagnostics) != 0 {
|
} else if len(result.Diagnostics) != 0 {
|
||||||
t.Fatalf("diagnostics = %#v, want none", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v, want none", result.Diagnostics)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ func TestExtractSkipsModelForIneligibleScenes(t *testing.T) {
|
|||||||
t.Fatalf("result = %#v, calls = %d", result, len(client.requests))
|
t.Fatalf("result = %#v, calls = %d", result, len(client.requests))
|
||||||
}
|
}
|
||||||
if test.wantWarning {
|
if test.wantWarning {
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != "scene_classification_unavailable" || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot {
|
if len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != "scene_classification_unavailable" || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionWarning || result.Diagnostics[0].Category != contracts.DiagnosticCategoryDegradation || result.Diagnostics[0].Samples[0].Scope != SceneDescriptionReferenceSlot {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
} else if len(result.Warnings) != 0 || len(result.Diagnostics) != 0 {
|
} else if len(result.Diagnostics) != 0 {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ func TestExtractReturnsCanonicalSpellListFromPrivateResponse(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(result.Value, want) {
|
if !reflect.DeepEqual(result.Value, want) {
|
||||||
t.Fatalf("Value = %#v, want %#v", result.Value, want)
|
t.Fatalf("Value = %#v, want %#v", result.Value, want)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 {
|
|
||||||
t.Fatalf("Warnings = %#v, want none", result.Warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(client.requests) != 1 {
|
if len(client.requests) != 1 {
|
||||||
t.Fatalf("LLM calls = %d, want 1", len(client.requests))
|
t.Fatalf("LLM calls = %d, want 1", len(client.requests))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,13 +132,13 @@ type actorCanonicalization struct {
|
|||||||
to string
|
to string
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurnList, []contracts.Warning) {
|
func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurnList, []diagnostics.Finding) {
|
||||||
if input.CombatTurns == nil {
|
if input.CombatTurns == nil {
|
||||||
return dnd.CombatTurnList{}, nil
|
return dnd.CombatTurnList{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
records := make([]normalizedRecord, len(input.CombatTurns))
|
records := make([]normalizedRecord, len(input.CombatTurns))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputTurn := range input.CombatTurns {
|
for index, inputTurn := range input.CombatTurns {
|
||||||
turn, actorChange, refsChanged := normalizeTurn(inputTurn, order, registry)
|
turn, actorChange, refsChanged := normalizeTurn(inputTurn, order, registry)
|
||||||
earliest, hasEvidence := order.EarliestValid(turn.SourceRefs)
|
earliest, hasEvidence := order.EarliestValid(turn.SourceRefs)
|
||||||
@@ -149,7 +149,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
|||||||
hasEvidence: hasEvidence,
|
hasEvidence: hasEvidence,
|
||||||
}
|
}
|
||||||
if actorChange != nil {
|
if actorChange != nil {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: turnScope(index),
|
Scope: turnScope(index),
|
||||||
ReasonCode: ReasonCodeActorCanonicalized,
|
ReasonCode: ReasonCodeActorCanonicalized,
|
||||||
Message: fmt.Sprintf("input index %d: actor canonicalized from %s to %s",
|
Message: fmt.Sprintf("input index %d: actor canonicalized from %s to %s",
|
||||||
@@ -157,7 +157,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: turnScope(index),
|
Scope: turnScope(index),
|
||||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||||
@@ -179,7 +179,7 @@ func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex,
|
|||||||
if position == record.inputIndex {
|
if position == record.inputIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: turnScope(record.inputIndex),
|
Scope: turnScope(record.inputIndex),
|
||||||
ReasonCode: ReasonCodeTurnsReordered,
|
ReasonCode: ReasonCodeTurnsReordered,
|
||||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology",
|
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology",
|
||||||
@@ -236,7 +236,7 @@ type duplicateGroup struct {
|
|||||||
removed []int
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.CombatTurn, []contracts.Warning) {
|
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.CombatTurn, []diagnostics.Finding) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return make([]dnd.CombatTurn, 0), nil
|
return make([]dnd.CombatTurn, 0), nil
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if len(group.removed) == 0 {
|
if len(group.removed) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -309,12 +309,12 @@ func writeKeyInt(builder *strings.Builder, value int) {
|
|||||||
builder.WriteByte(';')
|
builder.WriteByte(';')
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
issues := make([]string, len(removed))
|
issues := make([]string, len(removed))
|
||||||
for index, removedIndex := range removed {
|
for index, removedIndex := range removed {
|
||||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: turnScope(retainedIndex),
|
Scope: turnScope(retainedIndex),
|
||||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||||
Message: diagnostics.Aggregate(
|
Message: diagnostics.Aggregate(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeCanonicalizesFieldsAndRegistryIdentities(t *testing.T) {
|
func TestNormalizeCanonicalizesFieldsAndRegistryIdentities(t *testing.T) {
|
||||||
@@ -61,7 +60,7 @@ func TestNormalizeCanonicalizesFieldsAndRegistryIdentities(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeLimitsWarningsWithoutChangingCombatTurnValues(t *testing.T) {
|
func TestNormalizeLimitsWarningsWithoutChangingCombatTurnValues(t *testing.T) {
|
||||||
units := make([]source.SourceUnit, diagnostics.MaxWarnings+1)
|
units := make([]source.SourceUnit, contracts.MaxDiagnosticSamples+1)
|
||||||
turns := make([]dnd.CombatTurn, len(units))
|
turns := make([]dnd.CombatTurn, len(units))
|
||||||
for index := range units {
|
for index := range units {
|
||||||
unitID := index + 1
|
unitID := index + 1
|
||||||
@@ -82,7 +81,7 @@ func TestNormalizeLimitsWarningsWithoutChangingCombatTurnValues(t *testing.T) {
|
|||||||
if len(result.Value.CombatTurns) != len(turns) || result.Value.CombatTurns[0].Actor != "Aria" {
|
if len(result.Value.CombatTurns) != len(turns) || result.Value.CombatTurns[0].Actor != "Aria" {
|
||||||
t.Fatalf("normalized turns = %#v, want canonicalized values", result.Value.CombatTurns)
|
t.Fatalf("normalized turns = %#v, want canonicalized values", result.Value.CombatTurns)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,17 +123,17 @@ type nameCanonicalization struct {
|
|||||||
to string
|
to string
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.EnemyEventList, []contracts.Warning) {
|
func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.EnemyEventList, []diagnostics.Finding) {
|
||||||
if input.Events == nil {
|
if input.Events == nil {
|
||||||
return dnd.EnemyEventList{}, nil
|
return dnd.EnemyEventList{}, nil
|
||||||
}
|
}
|
||||||
records := make([]normalizedRecord, len(input.Events))
|
records := make([]normalizedRecord, len(input.Events))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputEvent := range input.Events {
|
for index, inputEvent := range input.Events {
|
||||||
event, nameChange, refsChanged := normalizeEvent(inputEvent, order, registry)
|
event, nameChange, refsChanged := normalizeEvent(inputEvent, order, registry)
|
||||||
records[index] = normalizedRecord{event: event, identity: enemyeventmodel.CanonicalIdentity(event), inputIndex: index}
|
records[index] = normalizedRecord{event: event, identity: enemyeventmodel.CanonicalIdentity(event), inputIndex: index}
|
||||||
if nameChange != nil {
|
if nameChange != nil {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: eventScope(index),
|
Scope: eventScope(index),
|
||||||
ReasonCode: ReasonCodeNameCanonicalized,
|
ReasonCode: ReasonCodeNameCanonicalized,
|
||||||
Message: fmt.Sprintf("input index %d: subject canonicalized from %s to %s",
|
Message: fmt.Sprintf("input index %d: subject canonicalized from %s to %s",
|
||||||
@@ -141,7 +141,7 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: eventScope(index),
|
Scope: eventScope(index),
|
||||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||||
@@ -157,7 +157,7 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
|
|||||||
if position == record.inputIndex {
|
if position == record.inputIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: eventScope(record.inputIndex),
|
Scope: eventScope(record.inputIndex),
|
||||||
ReasonCode: ReasonCodeEventsReordered,
|
ReasonCode: ReasonCodeEventsReordered,
|
||||||
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
||||||
@@ -208,7 +208,7 @@ type duplicateGroup struct {
|
|||||||
removed []int
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contracts.Warning) {
|
func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []diagnostics.Finding) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return make([]dnd.EnemyEvent, 0), nil
|
return make([]dnd.EnemyEvent, 0), nil
|
||||||
}
|
}
|
||||||
@@ -235,7 +235,7 @@ func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contrac
|
|||||||
for index, record := range kept {
|
for index, record := range kept {
|
||||||
output[index] = cloneEvent(record.event)
|
output[index] = cloneEvent(record.event)
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if len(group.removed) == 0 {
|
if len(group.removed) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -244,7 +244,7 @@ func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contrac
|
|||||||
for index, removed := range group.removed {
|
for index, removed := range group.removed {
|
||||||
issues[index] = fmt.Sprintf("removed input index %d", removed)
|
issues[index] = fmt.Sprintf("removed input index %d", removed)
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: eventScope(group.retainedIndex),
|
Scope: eventScope(group.retainedIndex),
|
||||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||||
Message: diagnostics.Aggregate(
|
Message: diagnostics.Aggregate(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeCanonicalizesSubjectsEvidenceOrderAndDuplicates(t *testing.T) {
|
func TestNormalizeCanonicalizesSubjectsEvidenceOrderAndDuplicates(t *testing.T) {
|
||||||
@@ -162,7 +161,7 @@ func TestNormalizerContractAndWarningBound(t *testing.T) {
|
|||||||
t.Fatalf("unsafe metadata = %s, %v", encoded, err)
|
t.Fatalf("unsafe metadata = %s, %v", encoded, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
count := diagnostics.MaxWarnings + 5
|
count := contracts.MaxDiagnosticSamples + 5
|
||||||
document := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
document := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||||
input := dnd.EnemyEventList{Events: make([]dnd.EnemyEvent, count)}
|
input := dnd.EnemyEventList{Events: make([]dnd.EnemyEvent, count)}
|
||||||
for index := range document.Units {
|
for index := range document.Units {
|
||||||
@@ -170,7 +169,7 @@ func TestNormalizerContractAndWarningBound(t *testing.T) {
|
|||||||
input.Events[index] = dnd.EnemyEvent{Name: " ÁRIA ", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: document.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
input.Events[index] = dnd.EnemyEvent{Name: " ÁRIA ", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: document.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
||||||
}
|
}
|
||||||
result, err := normalizer.Normalize(context.Background(), normalizeRequest(document, input, contracts.ReferenceSet{}))
|
result, err := normalizer.Normalize(context.Background(), normalizeRequest(document, input, contracts.ReferenceSet{}))
|
||||||
if err != nil || len(result.Warnings) != 0 || len(result.Diagnostics) == 0 {
|
if err != nil || len(result.Diagnostics) == 0 {
|
||||||
t.Fatalf("diagnostics = %#v, %v", result.Diagnostics, err)
|
t.Fatalf("diagnostics = %#v, %v", result.Diagnostics, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,18 +120,18 @@ type normalizedRecord struct {
|
|||||||
inputIndex int
|
inputIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrenceList, []contracts.Warning) {
|
func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrenceList, []diagnostics.Finding) {
|
||||||
if input.Occurrences == nil {
|
if input.Occurrences == nil {
|
||||||
return dnd.ItemOccurrenceList{}, nil
|
return dnd.ItemOccurrenceList{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
records := make([]normalizedRecord, len(input.Occurrences))
|
records := make([]normalizedRecord, len(input.Occurrences))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputOccurrence := range input.Occurrences {
|
for index, inputOccurrence := range input.Occurrences {
|
||||||
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
||||||
records[index] = normalizedRecord{occurrence: occurrence, identity: itemoccurrencemodel.CanonicalExactIdentity(occurrence), inputIndex: index}
|
records[index] = normalizedRecord{occurrence: occurrence, identity: itemoccurrencemodel.CanonicalExactIdentity(occurrence), inputIndex: index}
|
||||||
if len(changedFields) != 0 {
|
if len(changedFields) != 0 {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: occurrenceScope(index),
|
Scope: occurrenceScope(index),
|
||||||
ReasonCode: ReasonCodeNameCanonicalized,
|
ReasonCode: ReasonCodeNameCanonicalized,
|
||||||
Message: fmt.Sprintf("input index %d: normalized display whitespace in %s", index,
|
Message: fmt.Sprintf("input index %d: normalized display whitespace in %s", index,
|
||||||
@@ -139,15 +139,15 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if found && inputOccurrence.Name != occurrence.Name {
|
if found && inputOccurrence.Name != occurrence.Name {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||||
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputOccurrence.Name), diagnostics.Quote(occurrence.Name))})
|
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputOccurrence.Name), diagnostics.Quote(occurrence.Name))})
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
|
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
|
||||||
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.ItemID))})
|
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.ItemID))})
|
||||||
}
|
}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: occurrenceScope(index),
|
Scope: occurrenceScope(index),
|
||||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||||
@@ -163,7 +163,7 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
|||||||
if position == record.inputIndex {
|
if position == record.inputIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: occurrenceScope(record.inputIndex),
|
Scope: occurrenceScope(record.inputIndex),
|
||||||
ReasonCode: ReasonCodeOccurrencesReordered,
|
ReasonCode: ReasonCodeOccurrencesReordered,
|
||||||
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
|
||||||
@@ -217,7 +217,7 @@ type duplicateGroup struct {
|
|||||||
removed []int
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex) ([]dnd.ItemOccurrence, []contracts.Warning) {
|
func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex) ([]dnd.ItemOccurrence, []diagnostics.Finding) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return make([]dnd.ItemOccurrence, 0), nil
|
return make([]dnd.ItemOccurrence, 0), nil
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex)
|
|||||||
output = append(output, cloneOccurrence(record.occurrence))
|
output = append(output, cloneOccurrence(record.occurrence))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if len(group.removed) != 0 {
|
if len(group.removed) != 0 {
|
||||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||||
@@ -262,12 +262,12 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex)
|
|||||||
return output, warnings
|
return output, warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
issues := make([]string, len(removed))
|
issues := make([]string, len(removed))
|
||||||
for index, removedIndex := range removed {
|
for index, removedIndex := range removed {
|
||||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: occurrenceScope(retainedIndex),
|
Scope: occurrenceScope(retainedIndex),
|
||||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||||
Message: diagnostics.Aggregate(
|
Message: diagnostics.Aggregate(
|
||||||
|
|||||||
@@ -149,11 +149,11 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
return retryResult(recordList(applied), findings, advisoryFindings, reconciliation, rejectedGroups)
|
return retryResult(recordList(applied), findings, advisoryFindings, reconciliation, rejectedGroups)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Normalizer) invalidStructuredResult(value dnd.ItemRegistry, findings []contracts.Warning) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
func (n *Normalizer) invalidStructuredResult(value dnd.ItemRegistry, findings []diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||||
return retryResultWithFallback(value, findings, nil, nil, ReasonCodeItemSemanticRetryProposalInvalid, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
return retryResultWithFallback(value, findings, nil, nil, ReasonCodeItemSemanticRetryProposalInvalid, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, reconciliation semanticreconcile.Result, rejectedGroups int) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
func retryResult(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, reconciliation semanticreconcile.Result, rejectedGroups int) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||||
details := semanticreconcile.IssueDetails(reconciliation.Issues())
|
details := semanticreconcile.IssueDetails(reconciliation.Issues())
|
||||||
if rejectedGroups > 0 {
|
if rejectedGroups > 0 {
|
||||||
details = append(details, "currency may only be consolidated with aliases of one denomination")
|
details = append(details, "currency may only be consolidated with aliases of one denomination")
|
||||||
@@ -162,15 +162,15 @@ func retryResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.
|
|||||||
return retryResultWithFallback(value, findings, advisoryFindings, reconciliation.ModelCandidate(), ReasonCodeItemSemanticRetryProposalInvalid, diagnostics.Aggregate("semantic proposal requires retry", details), semanticFallbackFinding(discardedGroups))
|
return retryResultWithFallback(value, findings, advisoryFindings, reconciliation.ModelCandidate(), ReasonCodeItemSemanticRetryProposalInvalid, diagnostics.Aggregate("semantic proposal requires retry", details), semanticFallbackFinding(discardedGroups))
|
||||||
}
|
}
|
||||||
|
|
||||||
func semanticFallbackFinding(discarded int) contracts.Warning {
|
func semanticFallbackFinding(discarded int) diagnostics.Finding {
|
||||||
message := "semantic proposal could not be applied"
|
message := "semantic proposal could not be applied"
|
||||||
if discarded >= 0 {
|
if discarded >= 0 {
|
||||||
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discarded)
|
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discarded)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: "items", ReasonCode: ReasonCodeItemSemanticReconciliationExhausted, Message: message}
|
return diagnostics.Finding{Scope: "items", ReasonCode: ReasonCodeItemSemanticReconciliationExhausted, Message: message}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizationResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
func normalizationResult(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||||
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
||||||
@@ -183,12 +183,12 @@ func normalizationResult(value dnd.ItemRegistry, findings, advisoryFindings []co
|
|||||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fallbackResult(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
func fallbackResult(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||||
result, err := normalizationResult(value, findings, advisoryFindings, nil)
|
result, err := normalizationResult(value, findings, advisoryFindings, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, err
|
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, err
|
||||||
}
|
}
|
||||||
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||||
}
|
}
|
||||||
@@ -196,12 +196,12 @@ func fallbackResult(value dnd.ItemRegistry, findings, advisoryFindings []contrac
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResultWithFallback(value dnd.ItemRegistry, findings, advisoryFindings []contracts.Warning, candidate *contracts.ModelCandidate, reasonCode, message string, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
func retryResultWithFallback(value dnd.ItemRegistry, findings, advisoryFindings []diagnostics.Finding, candidate *contracts.ModelCandidate, reasonCode, message string, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.ItemRegistry], error) {
|
||||||
result, err := normalizationResult(value, findings, advisoryFindings, candidate)
|
result, err := normalizationResult(value, findings, advisoryFindings, candidate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, err
|
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, err
|
||||||
}
|
}
|
||||||
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
return contracts.TypedNormalizeResult[dnd.ItemRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||||
}
|
}
|
||||||
@@ -215,23 +215,23 @@ type normalizedRecord struct {
|
|||||||
earliest int
|
earliest int
|
||||||
}
|
}
|
||||||
|
|
||||||
func preprocessRecords(input dnd.ItemRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
func preprocessRecords(input dnd.ItemRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding) {
|
||||||
if input.Items == nil {
|
if input.Items == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
records := make([]normalizedRecord, len(input.Items))
|
records := make([]normalizedRecord, len(input.Items))
|
||||||
findings := make([]contracts.Warning, 0)
|
findings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputItem := range input.Items {
|
for index, inputItem := range input.Items {
|
||||||
item, fieldsChanged, refsChanged := normalizeRecord(inputItem, order)
|
item, fieldsChanged, refsChanged := normalizeRecord(inputItem, order)
|
||||||
records[index] = normalizedRecord{item: item, inputIndexes: []int{index}, earliest: index}
|
records[index] = normalizedRecord{item: item, inputIndexes: []int{index}, earliest: index}
|
||||||
if fieldsChanged {
|
if fieldsChanged {
|
||||||
findings = append(findings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeItemFieldsNormalized, Message: fmt.Sprintf("input index %d: item name normalized for %s", index, diagnostics.Quote(inputItem.Name))})
|
findings = append(findings, diagnostics.Finding{Scope: itemScope(index), ReasonCode: ReasonCodeItemFieldsNormalized, Message: fmt.Sprintf("input index %d: item name normalized for %s", index, diagnostics.Quote(inputItem.Name))})
|
||||||
}
|
}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
findings = append(findings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputItem.SourceRefs), len(item.SourceRefs))})
|
findings = append(findings, diagnostics.Finding{Scope: itemScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputItem.SourceRefs), len(item.SourceRefs))})
|
||||||
}
|
}
|
||||||
if inputItem.ID != item.ID {
|
if inputItem.ID != item.ID {
|
||||||
findings = append(findings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeItemIDRecomputed, Message: fmt.Sprintf("input index %d: item ID recomputed from %s", index, diagnostics.Quote(item.Name))})
|
findings = append(findings, diagnostics.Finding{Scope: itemScope(index), ReasonCode: ReasonCodeItemIDRecomputed, Message: fmt.Sprintf("input index %d: item ID recomputed from %s", index, diagnostics.Quote(item.Name))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groups := comparisonNameGroups(records)
|
groups := comparisonNameGroups(records)
|
||||||
@@ -328,7 +328,7 @@ func recordList(records []normalizedRecord) dnd.ItemRegistry {
|
|||||||
return dnd.ItemRegistry{Items: recordValues(records)}
|
return dnd.ItemRegistry{Items: recordValues(records)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
const maxDisplayedIndices = 20
|
const maxDisplayedIndices = 20
|
||||||
displayed := removed
|
displayed := removed
|
||||||
if len(displayed) > maxDisplayedIndices {
|
if len(displayed) > maxDisplayedIndices {
|
||||||
@@ -342,7 +342,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
|||||||
if omitted := len(removed) - len(displayed); omitted > 0 {
|
if omitted := len(removed) - len(displayed); omitted > 0 {
|
||||||
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: itemScope(retainedIndex), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: message}
|
return diagnostics.Finding{Scope: itemScope(retainedIndex), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: message}
|
||||||
}
|
}
|
||||||
func itemScope(index int) string { return fmt.Sprintf("items[%d]", index) }
|
func itemScope(index int) string { return fmt.Sprintf("items[%d]", index) }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
|
||||||
@@ -31,7 +30,7 @@ func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candi
|
|||||||
return candidates, envelopes, nil
|
return candidates, envelopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Item], order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, []contracts.Warning, int, error) {
|
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Item], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, []diagnostics.Finding, int, error) {
|
||||||
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Item]{
|
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Item]{
|
||||||
CloneValue: cloneItem,
|
CloneValue: cloneItem,
|
||||||
RejectGroup: func(members []dnd.Item, _ dnd.Item) semanticreconcile.RejectionCategory {
|
RejectGroup: func(members []dnd.Item, _ dnd.Item) semanticreconcile.RejectionCategory {
|
||||||
@@ -66,7 +65,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
}
|
}
|
||||||
type orderedFinding struct {
|
type orderedFinding struct {
|
||||||
position int
|
position int
|
||||||
finding contracts.Warning
|
finding diagnostics.Finding
|
||||||
}
|
}
|
||||||
observations := make([]orderedFinding, 0, len(application.AppliedGroups()))
|
observations := make([]orderedFinding, 0, len(application.AppliedGroups()))
|
||||||
advisories := make([]orderedFinding, 0, len(application.RejectedGroups()))
|
advisories := make([]orderedFinding, 0, len(application.RejectedGroups()))
|
||||||
@@ -81,7 +80,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
provenance := event.Provenance()
|
provenance := event.Provenance()
|
||||||
advisories = append(advisories, orderedFinding{
|
advisories = append(advisories, orderedFinding{
|
||||||
position: provenance.EarliestInputPosition(),
|
position: provenance.EarliestInputPosition(),
|
||||||
finding: contracts.Warning{
|
finding: diagnostics.Finding{
|
||||||
Scope: itemScope(provenance.EarliestInputPosition()),
|
Scope: itemScope(provenance.EarliestInputPosition()),
|
||||||
ReasonCode: ReasonCodeItemSemanticProposalInvalid,
|
ReasonCode: ReasonCodeItemSemanticProposalInvalid,
|
||||||
Message: "proposal group preserved because currency may only be consolidated with aliases of one denomination",
|
Message: "proposal group preserved because currency may only be consolidated with aliases of one denomination",
|
||||||
@@ -90,11 +89,11 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
}
|
}
|
||||||
sort.SliceStable(observations, func(left, right int) bool { return observations[left].position < observations[right].position })
|
sort.SliceStable(observations, func(left, right int) bool { return observations[left].position < observations[right].position })
|
||||||
sort.SliceStable(advisories, func(left, right int) bool { return advisories[left].position < advisories[right].position })
|
sort.SliceStable(advisories, func(left, right int) bool { return advisories[left].position < advisories[right].position })
|
||||||
observationFindings := make([]contracts.Warning, len(observations))
|
observationFindings := make([]diagnostics.Finding, len(observations))
|
||||||
for index, entry := range observations {
|
for index, entry := range observations {
|
||||||
observationFindings[index] = entry.finding
|
observationFindings[index] = entry.finding
|
||||||
}
|
}
|
||||||
advisoryFindings := make([]contracts.Warning, len(advisories))
|
advisoryFindings := make([]diagnostics.Finding, len(advisories))
|
||||||
for index, entry := range advisories {
|
for index, entry := range advisories {
|
||||||
advisoryFindings[index] = entry.finding
|
advisoryFindings[index] = entry.finding
|
||||||
}
|
}
|
||||||
@@ -139,7 +138,7 @@ func currencyDenomination(name string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) contracts.Warning {
|
func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||||
inputIndexes := provenance.OriginalInputIndexes()
|
inputIndexes := provenance.OriginalInputIndexes()
|
||||||
details := make([]string, 0, len(inputIndexes)+1)
|
details := make([]string, 0, len(inputIndexes)+1)
|
||||||
for _, inputIndex := range inputIndexes {
|
for _, inputIndex := range inputIndexes {
|
||||||
@@ -148,7 +147,7 @@ func semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, cano
|
|||||||
if canonical.earliest != provenance.EarliestInputPosition() {
|
if canonical.earliest != provenance.EarliestInputPosition() {
|
||||||
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: itemScope(provenance.EarliestInputPosition()),
|
Scope: itemScope(provenance.EarliestInputPosition()),
|
||||||
ReasonCode: ReasonCodeDuplicateItemCollapsed,
|
ReasonCode: ReasonCodeDuplicateItemCollapsed,
|
||||||
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
||||||
|
|||||||
@@ -122,25 +122,25 @@ type normalizedRecord struct {
|
|||||||
|
|
||||||
type nameCanonicalization struct{ from, to string }
|
type nameCanonicalization struct{ from, to string }
|
||||||
|
|
||||||
func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *locationregistry.Registry) (dnd.LocationOccurrenceList, []contracts.Warning) {
|
func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *locationregistry.Registry) (dnd.LocationOccurrenceList, []diagnostics.Finding) {
|
||||||
if input.Occurrences == nil {
|
if input.Occurrences == nil {
|
||||||
return dnd.LocationOccurrenceList{}, nil
|
return dnd.LocationOccurrenceList{}, nil
|
||||||
}
|
}
|
||||||
records := make([]normalizedRecord, len(input.Occurrences))
|
records := make([]normalizedRecord, len(input.Occurrences))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputOccurrence := range input.Occurrences {
|
for index, inputOccurrence := range input.Occurrences {
|
||||||
occurrence, change, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
occurrence, change, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
||||||
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
||||||
if change != nil {
|
if change != nil {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
|
||||||
Message: fmt.Sprintf("input index %d: location name canonicalized from %s to %s", index, diagnostics.Quote(change.from), diagnostics.Quote(change.to))})
|
Message: fmt.Sprintf("input index %d: location name canonicalized from %s to %s", index, diagnostics.Quote(change.from), diagnostics.Quote(change.to))})
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownLocationID,
|
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownLocationID,
|
||||||
Message: fmt.Sprintf("input index %d: location ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.LocationID))})
|
Message: fmt.Sprintf("input index %d: location ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.LocationID))})
|
||||||
}
|
}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeSourceRefsNormalized,
|
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(index), ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputOccurrence.SourceRefs), len(occurrence.SourceRefs))})
|
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputOccurrence.SourceRefs), len(occurrence.SourceRefs))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ func normalizeList(input dnd.LocationOccurrenceList, documentIndex source.Docume
|
|||||||
})
|
})
|
||||||
for position, record := range records {
|
for position, record := range records {
|
||||||
if position != record.inputIndex {
|
if position != record.inputIndex {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(record.inputIndex), ReasonCode: ReasonCodeOccurrencesReordered,
|
warnings = append(warnings, diagnostics.Finding{Scope: occurrenceScope(record.inputIndex), ReasonCode: ReasonCodeOccurrencesReordered,
|
||||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical occurrence order", record.inputIndex, position)})
|
Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical occurrence order", record.inputIndex, position)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ type duplicateGroup struct {
|
|||||||
removed []int
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.LocationOccurrence, []contracts.Warning) {
|
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.LocationOccurrence, []diagnostics.Finding) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return make([]dnd.LocationOccurrence, 0), nil
|
return make([]dnd.LocationOccurrence, 0), nil
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
|||||||
output = append(output, cloneOccurrence(record.occurrence))
|
output = append(output, cloneOccurrence(record.occurrence))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if len(group.removed) > 0 {
|
if len(group.removed) > 0 {
|
||||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||||
@@ -316,12 +316,12 @@ func sourceRefsLess(order shared.SourceRefOrder, left, right []source.SourceRef)
|
|||||||
return len(left) < len(right)
|
return len(left) < len(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
issues := make([]string, len(removed))
|
issues := make([]string, len(removed))
|
||||||
for index, removedIndex := range removed {
|
for index, removedIndex := range removed {
|
||||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: occurrenceScope(retainedIndex), ReasonCode: ReasonCodeDuplicateCollapsed,
|
return diagnostics.Finding{Scope: occurrenceScope(retainedIndex), ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||||
Message: diagnostics.Aggregate(fmt.Sprintf("duplicate location occurrence collapsed; retained input index %d", retainedIndex), issues)}
|
Message: diagnostics.Aggregate(fmt.Sprintf("duplicate location occurrence collapsed; retained input index %d", retainedIndex), issues)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
|
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeCanonicalizesNamesByIDAndClonesInputs(t *testing.T) {
|
func TestNormalizeCanonicalizesNamesByIDAndClonesInputs(t *testing.T) {
|
||||||
@@ -125,7 +124,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
|
|||||||
t.Fatalf("unbound registry error = %v", err)
|
t.Fatalf("unbound registry error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
count := diagnostics.MaxWarnings + 5
|
count := contracts.MaxDiagnosticSamples + 5
|
||||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||||
input := dnd.LocationOccurrenceList{Occurrences: make([]dnd.LocationOccurrence, count)}
|
input := dnd.LocationOccurrenceList{Occurrences: make([]dnd.LocationOccurrence, count)}
|
||||||
location := registryLocations("The Mill").Locations[0]
|
location := registryLocations("The Mill").Locations[0]
|
||||||
@@ -134,7 +133,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
|
|||||||
input.Occurrences[index] = dnd.LocationOccurrence{LocationID: location.ID, Name: "not canonical", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
input.Occurrences[index] = dnd.LocationOccurrence{LocationID: location.ID, Name: "not canonical", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
||||||
}
|
}
|
||||||
bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationRegistry{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
|
bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationRegistry{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
|
||||||
if err != nil || len(bounded.Warnings) != 0 || len(bounded.Diagnostics) == 0 {
|
if err != nil || len(bounded.Diagnostics) == 0 {
|
||||||
t.Fatalf("bounded diagnostics = %#v, %v", bounded.Diagnostics, err)
|
t.Fatalf("bounded diagnostics = %#v, %v", bounded.Diagnostics, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,23 +148,23 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
return retryResult(recordList(applied), findings, reconciliation)
|
return retryResult(recordList(applied), findings, reconciliation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, findings []contracts.Warning) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, findings []diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
||||||
return retryResultWithFallback(value, findings, nil, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
return retryResultWithFallback(value, findings, nil, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResult(value dnd.LocationRegistry, findings []contracts.Warning, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
func retryResult(value dnd.LocationRegistry, findings []diagnostics.Finding, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
||||||
return retryResultWithFallback(value, findings, reconciliation.ModelCandidate(), diagnostics.Aggregate("semantic proposal requires retry", semanticreconcile.IssueDetails(reconciliation.Issues())), semanticFallbackFinding(reconciliation.DiscardedGroupCount()))
|
return retryResultWithFallback(value, findings, reconciliation.ModelCandidate(), diagnostics.Aggregate("semantic proposal requires retry", semanticreconcile.IssueDetails(reconciliation.Issues())), semanticFallbackFinding(reconciliation.DiscardedGroupCount()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func semanticFallbackFinding(discarded int) contracts.Warning {
|
func semanticFallbackFinding(discarded int) diagnostics.Finding {
|
||||||
message := "semantic proposal could not be applied"
|
message := "semantic proposal could not be applied"
|
||||||
if discarded >= 0 {
|
if discarded >= 0 {
|
||||||
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discarded)
|
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discarded)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: "locations", ReasonCode: ReasonCodeLocationSemanticReconciliationExhausted, Message: message}
|
return diagnostics.Finding{Scope: "locations", ReasonCode: ReasonCodeLocationSemanticReconciliationExhausted, Message: message}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizationResult(value dnd.LocationRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
func normalizationResult(value dnd.LocationRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
||||||
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
||||||
@@ -172,12 +172,12 @@ func normalizationResult(value dnd.LocationRegistry, findings []contracts.Warnin
|
|||||||
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fallbackResult(value dnd.LocationRegistry, findings []contracts.Warning, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
func fallbackResult(value dnd.LocationRegistry, findings []diagnostics.Finding, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
||||||
result, err := normalizationResult(value, findings, nil)
|
result, err := normalizationResult(value, findings, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, err
|
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, err
|
||||||
}
|
}
|
||||||
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||||
}
|
}
|
||||||
@@ -185,12 +185,12 @@ func fallbackResult(value dnd.LocationRegistry, findings []contracts.Warning, fa
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResultWithFallback(value dnd.LocationRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate, message string, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
func retryResultWithFallback(value dnd.LocationRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate, message string, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
|
||||||
result, err := normalizationResult(value, findings, candidate)
|
result, err := normalizationResult(value, findings, candidate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, err
|
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, err
|
||||||
}
|
}
|
||||||
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||||
}
|
}
|
||||||
@@ -204,23 +204,23 @@ type normalizedRecord struct {
|
|||||||
earliest int
|
earliest int
|
||||||
}
|
}
|
||||||
|
|
||||||
func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding) {
|
||||||
if input.Locations == nil {
|
if input.Locations == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
records := make([]normalizedRecord, len(input.Locations))
|
records := make([]normalizedRecord, len(input.Locations))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputLocation := range input.Locations {
|
for index, inputLocation := range input.Locations {
|
||||||
location, fieldsChanged, refsChanged := normalizeRecord(inputLocation, order)
|
location, fieldsChanged, refsChanged := normalizeRecord(inputLocation, order)
|
||||||
records[index] = normalizedRecord{location: location, inputIndexes: []int{index}, earliest: index}
|
records[index] = normalizedRecord{location: location, inputIndexes: []int{index}, earliest: index}
|
||||||
if fieldsChanged {
|
if fieldsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: locationScope(index), ReasonCode: ReasonCodeLocationFieldsNormalized, Message: fmt.Sprintf("input index %d: location name normalized for %s", index, diagnostics.Quote(inputLocation.Name))})
|
warnings = append(warnings, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeLocationFieldsNormalized, Message: fmt.Sprintf("input index %d: location name normalized for %s", index, diagnostics.Quote(inputLocation.Name))})
|
||||||
}
|
}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: locationScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputLocation.SourceRefs), len(location.SourceRefs))})
|
warnings = append(warnings, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputLocation.SourceRefs), len(location.SourceRefs))})
|
||||||
}
|
}
|
||||||
if inputLocation.ID != location.ID {
|
if inputLocation.ID != location.ID {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: locationScope(index), ReasonCode: ReasonCodeLocationIDRecomputed, Message: fmt.Sprintf("input index %d: location ID recomputed from %s", index, diagnostics.Quote(location.Name))})
|
warnings = append(warnings, diagnostics.Finding{Scope: locationScope(index), ReasonCode: ReasonCodeLocationIDRecomputed, Message: fmt.Sprintf("input index %d: location ID recomputed from %s", index, diagnostics.Quote(location.Name))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groups := exactDuplicateGroups(records)
|
groups := exactDuplicateGroups(records)
|
||||||
@@ -342,7 +342,7 @@ func recordList(records []normalizedRecord) dnd.LocationRegistry {
|
|||||||
return dnd.LocationRegistry{Locations: recordValues(records)}
|
return dnd.LocationRegistry{Locations: recordValues(records)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
const maxDisplayedIndices = 20
|
const maxDisplayedIndices = 20
|
||||||
displayed := removed
|
displayed := removed
|
||||||
if len(displayed) > maxDisplayedIndices {
|
if len(displayed) > maxDisplayedIndices {
|
||||||
@@ -356,7 +356,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
|||||||
if omitted := len(removed) - len(displayed); omitted > 0 {
|
if omitted := len(removed) - len(displayed); omitted > 0 {
|
||||||
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: locationScope(retainedIndex), ReasonCode: ReasonCodeDuplicateLocationCollapsed, Message: message}
|
return diagnostics.Finding{Scope: locationScope(retainedIndex), ReasonCode: ReasonCodeDuplicateLocationCollapsed, Message: message}
|
||||||
}
|
}
|
||||||
func locationScope(index int) string { return fmt.Sprintf("locations[%d]", index) }
|
func locationScope(index int) string { return fmt.Sprintf("locations[%d]", index) }
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package locationregistry
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
||||||
@@ -28,7 +27,7 @@ func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candi
|
|||||||
return candidates, envelopes, nil
|
return candidates, envelopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Location], order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, error) {
|
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Location], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, error) {
|
||||||
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Location]{
|
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Location]{
|
||||||
CloneValue: cloneLocation,
|
CloneValue: cloneLocation,
|
||||||
ConsolidateGroup: func(members []dnd.Location, canonical dnd.Location) (dnd.Location, error) {
|
ConsolidateGroup: func(members []dnd.Location, canonical dnd.Location) (dnd.Location, error) {
|
||||||
@@ -55,7 +54,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
earliest: record.EarliestInputPosition(),
|
earliest: record.EarliestInputPosition(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0, len(application.AppliedGroups()))
|
warnings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
|
||||||
for _, event := range application.AppliedGroups() {
|
for _, event := range application.AppliedGroups() {
|
||||||
provenance := event.Provenance()
|
provenance := event.Provenance()
|
||||||
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
|
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
|
||||||
@@ -63,7 +62,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
return output, warnings, nil
|
return output, warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) contracts.Warning {
|
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||||
inputIndexes := provenance.OriginalInputIndexes()
|
inputIndexes := provenance.OriginalInputIndexes()
|
||||||
details := make([]string, 0, len(inputIndexes)+1)
|
details := make([]string, 0, len(inputIndexes)+1)
|
||||||
for _, inputIndex := range inputIndexes {
|
for _, inputIndex := range inputIndexes {
|
||||||
@@ -72,7 +71,7 @@ func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, cano
|
|||||||
if canonical.earliest != provenance.EarliestInputPosition() {
|
if canonical.earliest != provenance.EarliestInputPosition() {
|
||||||
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: locationScope(provenance.EarliestInputPosition()),
|
Scope: locationScope(provenance.EarliestInputPosition()),
|
||||||
ReasonCode: ReasonCodeDuplicateLocationCollapsed,
|
ReasonCode: ReasonCodeDuplicateLocationCollapsed,
|
||||||
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
||||||
|
|||||||
@@ -124,13 +124,13 @@ type normalizedRecord struct {
|
|||||||
inputIndex int
|
inputIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrenceList, []contracts.Warning, error) {
|
func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrenceList, []diagnostics.Finding, error) {
|
||||||
if input.Occurrences == nil {
|
if input.Occurrences == nil {
|
||||||
return dnd.NPCOccurrenceList{}, nil, nil
|
return dnd.NPCOccurrenceList{}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
records := make([]normalizedRecord, len(input.Occurrences))
|
records := make([]normalizedRecord, len(input.Occurrences))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputOccurrence := range input.Occurrences {
|
for index, inputOccurrence := range input.Occurrences {
|
||||||
occurrence, refsChanged, err := normalizeOccurrence(inputOccurrence, order, registry)
|
occurrence, refsChanged, err := normalizeOccurrence(inputOccurrence, order, registry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -138,7 +138,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
|
|||||||
}
|
}
|
||||||
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
||||||
if refsChanged {
|
if refsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: occurrenceScope(index),
|
Scope: occurrenceScope(index),
|
||||||
ReasonCode: ReasonCodeSourceRefsNormalized,
|
ReasonCode: ReasonCodeSourceRefsNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
|
||||||
@@ -154,7 +154,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
|
|||||||
if position == record.inputIndex {
|
if position == record.inputIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: occurrenceScope(record.inputIndex),
|
Scope: occurrenceScope(record.inputIndex),
|
||||||
ReasonCode: ReasonCodeOccurrencesReordered,
|
ReasonCode: ReasonCodeOccurrencesReordered,
|
||||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology", record.inputIndex, position),
|
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology", record.inputIndex, position),
|
||||||
@@ -192,7 +192,7 @@ type duplicateGroup struct {
|
|||||||
removed []int
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.NPCOccurrence, []contracts.Warning) {
|
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.NPCOccurrence, []diagnostics.Finding) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return make([]dnd.NPCOccurrence, 0), nil
|
return make([]dnd.NPCOccurrence, 0), nil
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
|||||||
output = append(output, cloneOccurrence(record.occurrence))
|
output = append(output, cloneOccurrence(record.occurrence))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if len(group.removed) != 0 {
|
if len(group.removed) != 0 {
|
||||||
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
warnings = append(warnings, duplicateWarning(group.retainedIndex, group.removed))
|
||||||
@@ -229,12 +229,12 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
|
|||||||
return output, warnings
|
return output, warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
issues := make([]string, len(removed))
|
issues := make([]string, len(removed))
|
||||||
for index, removedIndex := range removed {
|
for index, removedIndex := range removed {
|
||||||
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: occurrenceScope(retainedIndex),
|
Scope: occurrenceScope(retainedIndex),
|
||||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||||
Message: diagnostics.Aggregate(
|
Message: diagnostics.Aggregate(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeValidatesPairsAndClones(t *testing.T) {
|
func TestNormalizeValidatesPairsAndClones(t *testing.T) {
|
||||||
@@ -141,7 +140,7 @@ func TestNormalizerContractAndDeterministicWarnings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeBoundsWarnings(t *testing.T) {
|
func TestNormalizeBoundsWarnings(t *testing.T) {
|
||||||
count := diagnostics.MaxWarnings + 5
|
count := contracts.MaxDiagnosticSamples + 5
|
||||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||||
input := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, count)}
|
input := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, count)}
|
||||||
for index := range doc.Units {
|
for index := range doc.Units {
|
||||||
@@ -163,7 +162,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation {
|
if len(result.Diagnostics) == 0 || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,23 +147,23 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
return retryResult(recordList(applied), findings, reconciliation)
|
return retryResult(recordList(applied), findings, reconciliation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Normalizer) invalidStructuredResult(value dnd.NPCRegistry, findings []contracts.Warning) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
func (n *Normalizer) invalidStructuredResult(value dnd.NPCRegistry, findings []diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||||
return retryResultWithFallback(value, findings, nil, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
return retryResultWithFallback(value, findings, nil, "semantic proposal requires retry: invalid structured output", semanticFallbackFinding(-1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResult(value dnd.NPCRegistry, findings []contracts.Warning, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
func retryResult(value dnd.NPCRegistry, findings []diagnostics.Finding, reconciliation semanticreconcile.Result) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||||
return retryResultWithFallback(value, findings, reconciliation.ModelCandidate(), diagnostics.Aggregate("semantic proposal requires retry", semanticreconcile.IssueDetails(reconciliation.Issues())), semanticFallbackFinding(reconciliation.DiscardedGroupCount()))
|
return retryResultWithFallback(value, findings, reconciliation.ModelCandidate(), diagnostics.Aggregate("semantic proposal requires retry", semanticreconcile.IssueDetails(reconciliation.Issues())), semanticFallbackFinding(reconciliation.DiscardedGroupCount()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func semanticFallbackFinding(discardedGroups int) contracts.Warning {
|
func semanticFallbackFinding(discardedGroups int) diagnostics.Finding {
|
||||||
message := "semantic proposal could not be applied"
|
message := "semantic proposal could not be applied"
|
||||||
if discardedGroups >= 0 {
|
if discardedGroups >= 0 {
|
||||||
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discardedGroups)
|
message = fmt.Sprintf("%d proposal group(s) omitted after semantic proposal retry exhaustion", discardedGroups)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: "npcs", ReasonCode: ReasonCodeNPCSemanticReconciliationExhausted, Message: message}
|
return diagnostics.Finding{Scope: "npcs", ReasonCode: ReasonCodeNPCSemanticReconciliationExhausted, Message: message}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizationResult(value dnd.NPCRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
func normalizationResult(value dnd.NPCRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||||
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
diagnosticGroups, err := diagnostics.Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect normalization diagnostics: %w", err)
|
||||||
@@ -171,12 +171,12 @@ func normalizationResult(value dnd.NPCRegistry, findings []contracts.Warning, ca
|
|||||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: value, Diagnostics: diagnosticGroups, ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fallbackResult(value dnd.NPCRegistry, findings []contracts.Warning, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
func fallbackResult(value dnd.NPCRegistry, findings []diagnostics.Finding, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||||
result, err := normalizationResult(value, findings, nil)
|
result, err := normalizationResult(value, findings, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, err
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, err
|
||||||
}
|
}
|
||||||
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||||
}
|
}
|
||||||
@@ -184,12 +184,12 @@ func fallbackResult(value dnd.NPCRegistry, findings []contracts.Warning, fallbac
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResultWithFallback(value dnd.NPCRegistry, findings []contracts.Warning, candidate *contracts.ModelCandidate, message string, fallback contracts.Warning) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
func retryResultWithFallback(value dnd.NPCRegistry, findings []diagnostics.Finding, candidate *contracts.ModelCandidate, message string, fallback diagnostics.Finding) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||||
result, err := normalizationResult(value, findings, candidate)
|
result, err := normalizationResult(value, findings, candidate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, err
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, err
|
||||||
}
|
}
|
||||||
fallbackGroups, err := diagnostics.Collect([]contracts.Warning{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
fallbackGroups, err := diagnostics.Collect([]diagnostics.Finding{fallback}, contracts.DiagnosticDispositionWarning, contracts.DiagnosticCategoryFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("collect fallback diagnostic: %w", err)
|
||||||
}
|
}
|
||||||
@@ -203,23 +203,23 @@ type normalizedRecord struct {
|
|||||||
earliest int
|
earliest int
|
||||||
}
|
}
|
||||||
|
|
||||||
func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding) {
|
||||||
if input.NPCs == nil {
|
if input.NPCs == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
records := make([]normalizedRecord, len(input.NPCs))
|
records := make([]normalizedRecord, len(input.NPCs))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, inputNPC := range input.NPCs {
|
for index, inputNPC := range input.NPCs {
|
||||||
npc, fieldsChanged, referencesChanged := normalizeRecord(inputNPC, order)
|
npc, fieldsChanged, referencesChanged := normalizeRecord(inputNPC, order)
|
||||||
records[index] = normalizedRecord{npc: npc, inputIndexes: []int{index}, earliest: index}
|
records[index] = normalizedRecord{npc: npc, inputIndexes: []int{index}, earliest: index}
|
||||||
if fieldsChanged {
|
if fieldsChanged {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeNPCFieldsNormalized, Message: fmt.Sprintf("input index %d: NPC name normalized for %s", index, diagnostics.Quote(inputNPC.Name))})
|
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCFieldsNormalized, Message: fmt.Sprintf("input index %d: NPC name normalized for %s", index, diagnostics.Quote(inputNPC.Name))})
|
||||||
}
|
}
|
||||||
if referencesChanged {
|
if referencesChanged {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputNPC.SourceRefs), len(npc.SourceRefs))})
|
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", index, len(inputNPC.SourceRefs), len(npc.SourceRefs))})
|
||||||
}
|
}
|
||||||
if inputNPC.ID != npc.ID {
|
if inputNPC.ID != npc.ID {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeNPCIDRecomputed, Message: fmt.Sprintf("input index %d: NPC ID recomputed from %s", index, diagnostics.Quote(npc.Name))})
|
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(index), ReasonCode: ReasonCodeNPCIDRecomputed, Message: fmt.Sprintf("input index %d: NPC ID recomputed from %s", index, diagnostics.Quote(npc.Name))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]no
|
|||||||
output = append(output, consolidated)
|
output = append(output, consolidated)
|
||||||
retainedIndex := consolidated.earliest
|
retainedIndex := consolidated.earliest
|
||||||
if referencesChanged {
|
if referencesChanged {
|
||||||
warnings = append(warnings, contracts.Warning{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized during identity consolidation (final count %d)", retainedIndex, len(consolidated.npc.SourceRefs))})
|
warnings = append(warnings, diagnostics.Finding{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeSourceReferencesNormalized, Message: fmt.Sprintf("input index %d: source references normalized during identity consolidation (final count %d)", retainedIndex, len(consolidated.npc.SourceRefs))})
|
||||||
}
|
}
|
||||||
if len(members) > 1 {
|
if len(members) > 1 {
|
||||||
warnings = append(warnings, duplicateWarning(retainedIndex, memberInputIndexes(records, members[1:])))
|
warnings = append(warnings, duplicateWarning(retainedIndex, memberInputIndexes(records, members[1:])))
|
||||||
@@ -340,7 +340,7 @@ func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
|
|||||||
return append([]source.SourceRef(nil), input...)
|
return append([]source.SourceRef(nil), input...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
const maxDisplayedIndices = 20
|
const maxDisplayedIndices = 20
|
||||||
displayed := removed
|
displayed := removed
|
||||||
if len(displayed) > maxDisplayedIndices {
|
if len(displayed) > maxDisplayedIndices {
|
||||||
@@ -354,7 +354,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
|||||||
if omitted := len(removed) - len(displayed); omitted > 0 {
|
if omitted := len(removed) - len(displayed); omitted > 0 {
|
||||||
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
||||||
}
|
}
|
||||||
return contracts.Warning{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeDuplicateNPCCollapsed, Message: message}
|
return diagnostics.Finding{Scope: npcScope(retainedIndex), ReasonCode: ReasonCodeDuplicateNPCCollapsed, Message: message}
|
||||||
}
|
}
|
||||||
|
|
||||||
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
|
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package npcregistry
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||||
@@ -28,7 +27,7 @@ func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candi
|
|||||||
return candidates, envelopes, nil
|
return candidates, envelopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.NPC], order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, error) {
|
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.NPC], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, error) {
|
||||||
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.NPC]{
|
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.NPC]{
|
||||||
CloneValue: cloneNPC,
|
CloneValue: cloneNPC,
|
||||||
ConsolidateGroup: func(members []dnd.NPC, canonical dnd.NPC) (dnd.NPC, error) {
|
ConsolidateGroup: func(members []dnd.NPC, canonical dnd.NPC) (dnd.NPC, error) {
|
||||||
@@ -55,7 +54,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
earliest: record.EarliestInputPosition(),
|
earliest: record.EarliestInputPosition(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0, len(application.AppliedGroups()))
|
warnings := make([]diagnostics.Finding, 0, len(application.AppliedGroups()))
|
||||||
for _, event := range application.AppliedGroups() {
|
for _, event := range application.AppliedGroups() {
|
||||||
provenance := event.Provenance()
|
provenance := event.Provenance()
|
||||||
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
|
warnings = append(warnings, semanticDuplicateWarning(provenance, records[provenance.CanonicalPosition()]))
|
||||||
@@ -63,7 +62,7 @@ func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRe
|
|||||||
return output, warnings, nil
|
return output, warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) contracts.Warning {
|
func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
||||||
inputIndexes := provenance.OriginalInputIndexes()
|
inputIndexes := provenance.OriginalInputIndexes()
|
||||||
details := make([]string, 0, len(inputIndexes)+1)
|
details := make([]string, 0, len(inputIndexes)+1)
|
||||||
for _, inputIndex := range inputIndexes {
|
for _, inputIndex := range inputIndexes {
|
||||||
@@ -72,7 +71,7 @@ func semanticDuplicateWarning(provenance semanticreconcile.GroupProvenance, cano
|
|||||||
if canonical.earliest != provenance.EarliestInputPosition() {
|
if canonical.earliest != provenance.EarliestInputPosition() {
|
||||||
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: npcScope(provenance.EarliestInputPosition()),
|
Scope: npcScope(provenance.EarliestInputPosition()),
|
||||||
ReasonCode: ReasonCodeDuplicateNPCCollapsed,
|
ReasonCode: ReasonCodeDuplicateNPCCollapsed,
|
||||||
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ type normalizedScene struct {
|
|||||||
inputIndex int
|
inputIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (dnd.SceneDescriptionList, []contracts.Warning, error) {
|
func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (dnd.SceneDescriptionList, []diagnostics.Finding, error) {
|
||||||
if doc == nil {
|
if doc == nil {
|
||||||
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source document must not be nil")
|
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source document must not be nil")
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
|||||||
|
|
||||||
documentIndex := source.NewDocumentIndex(doc)
|
documentIndex := source.NewDocumentIndex(doc)
|
||||||
records := make([]normalizedScene, len(input.Scenes))
|
records := make([]normalizedScene, len(input.Scenes))
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for sceneIndex, scene := range input.Scenes {
|
for sceneIndex, scene := range input.Scenes {
|
||||||
originalTitle, originalSummary := scene.Title, scene.Summary
|
originalTitle, originalSummary := scene.Title, scene.Summary
|
||||||
scene.Title = strings.TrimSpace(scene.Title)
|
scene.Title = strings.TrimSpace(scene.Title)
|
||||||
@@ -104,7 +104,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
|||||||
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes[%d].source_ref: %s", sceneIndex, diagnostics.Truncate(err.Error()))
|
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("scenes[%d].source_ref: %s", sceneIndex, diagnostics.Truncate(err.Error()))
|
||||||
}
|
}
|
||||||
if originalTitle != scene.Title || originalSummary != scene.Summary {
|
if originalTitle != scene.Title || originalSummary != scene.Summary {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: sceneScope(sceneIndex),
|
Scope: sceneScope(sceneIndex),
|
||||||
ReasonCode: ReasonCodeProseNormalized,
|
ReasonCode: ReasonCodeProseNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: title and/or summary whitespace normalized", sceneIndex),
|
Message: fmt.Sprintf("input index %d: title and/or summary whitespace normalized", sceneIndex),
|
||||||
@@ -125,7 +125,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
|||||||
if position == record.inputIndex {
|
if position == record.inputIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: sceneScope(record.inputIndex),
|
Scope: sceneScope(record.inputIndex),
|
||||||
ReasonCode: ReasonCodeOrderNormalized,
|
ReasonCode: ReasonCodeOrderNormalized,
|
||||||
Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical scene order",
|
Message: fmt.Sprintf("input index %d moved to normalized position %d by canonical scene order",
|
||||||
@@ -146,7 +146,7 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
|||||||
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source range %s has conflicting records", sourceRefLabel(scene.SourceRef))
|
return dnd.SceneDescriptionList{}, nil, fmt.Errorf("source range %s has conflicting records", sourceRefLabel(scene.SourceRef))
|
||||||
}
|
}
|
||||||
if retainedIndex, ok := seen[scene]; ok {
|
if retainedIndex, ok := seen[scene]; ok {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: sceneScope(record.inputIndex),
|
Scope: sceneScope(record.inputIndex),
|
||||||
ReasonCode: ReasonCodeDuplicateCollapsed,
|
ReasonCode: ReasonCodeDuplicateCollapsed,
|
||||||
Message: fmt.Sprintf("input index %d: exact duplicate scene collapsed; retained input index %d",
|
Message: fmt.Sprintf("input index %d: exact duplicate scene collapsed; retained input index %d",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
||||||
@@ -35,7 +34,7 @@ func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(result.Value.Scenes, want) {
|
if !reflect.DeepEqual(result.Value.Scenes, want) {
|
||||||
t.Fatalf("scenes = %#v, want %#v", result.Value.Scenes, want)
|
t.Fatalf("scenes = %#v, want %#v", result.Value.Scenes, want)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 3 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].OccurrenceCount != 4 || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != 2 || result.Diagnostics[2].ReasonCode != ReasonCodeDuplicateCollapsed || result.Diagnostics[2].OccurrenceCount != 1 {
|
if len(result.Diagnostics) != 3 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].OccurrenceCount != 4 || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != 2 || result.Diagnostics[2].ReasonCode != ReasonCodeDuplicateCollapsed || result.Diagnostics[2].OccurrenceCount != 1 {
|
||||||
t.Fatalf("diagnostics = %#v, want grouped prose, order, and duplicate observations", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v, want grouped prose, order, and duplicate observations", result.Diagnostics)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(input, before) {
|
if !reflect.DeepEqual(input, before) {
|
||||||
@@ -55,7 +54,7 @@ func TestNormalizeTrimsOrdersDeduplicatesAndOwnsOutput(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeLimitsCombinedSceneMutationWarnings(t *testing.T) {
|
func TestNormalizeLimitsCombinedSceneMutationWarnings(t *testing.T) {
|
||||||
count := diagnostics.MaxWarnings - 8
|
count := contracts.MaxDiagnosticSamples + 1
|
||||||
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
||||||
input := dnd.SceneDescriptionList{Scenes: make([]dnd.SceneDescription, count)}
|
input := dnd.SceneDescriptionList{Scenes: make([]dnd.SceneDescription, count)}
|
||||||
for index := range input.Scenes {
|
for index := range input.Scenes {
|
||||||
@@ -70,7 +69,7 @@ func TestNormalizeLimitsCombinedSceneMutationWarnings(t *testing.T) {
|
|||||||
if len(result.Value.Scenes) != count || result.Value.Scenes[0].SourceRef.StartUnitID != 1 {
|
if len(result.Value.Scenes) != count || result.Value.Scenes[0].SourceRef.StartUnitID != 1 {
|
||||||
t.Fatalf("normalized scenes = %#v, want unchanged canonical values", result.Value.Scenes)
|
t.Fatalf("normalized scenes = %#v, want unchanged canonical values", result.Value.Scenes)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 2 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != count {
|
if len(result.Diagnostics) != 2 || result.Diagnostics[0].ReasonCode != ReasonCodeProseNormalized || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionObservation || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[1].ReasonCode != ReasonCodeOrderNormalized || result.Diagnostics[1].OccurrenceCount != count {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
return contracts.TypedNormalizeResult[dnd.SpellList]{Value: value, Diagnostics: diagnosticGroups}, nil
|
return contracts.TypedNormalizeResult[dnd.SpellList]{Value: value, Diagnostics: diagnosticGroups}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatalog, order shared.SourceRefOrder) (dnd.SpellList, []contracts.Warning) {
|
func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatalog, order shared.SourceRefOrder) (dnd.SpellList, []diagnostics.Finding) {
|
||||||
var warnings []contracts.Warning
|
var warnings []diagnostics.Finding
|
||||||
if input.SpellCasts == nil {
|
if input.SpellCasts == nil {
|
||||||
return dnd.SpellList{}, nil
|
return dnd.SpellList{}, nil
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
|||||||
cast := cloneSpellCast(inputCast)
|
cast := cloneSpellCast(inputCast)
|
||||||
if canonicalName, ok := catalog.Lookup(inputCast.Spell); ok {
|
if canonicalName, ok := catalog.Lookup(inputCast.Spell); ok {
|
||||||
if inputCast.Spell != canonicalName {
|
if inputCast.Spell != canonicalName {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: spellCastScope(index),
|
Scope: spellCastScope(index),
|
||||||
ReasonCode: ReasonCodeSpellNameCanonicalized,
|
ReasonCode: ReasonCodeSpellNameCanonicalized,
|
||||||
Message: fmt.Sprintf("input index %d: spell name canonicalized from %q to %q",
|
Message: fmt.Sprintf("input index %d: spell name canonicalized from %q to %q",
|
||||||
@@ -130,7 +130,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
|||||||
}
|
}
|
||||||
cast.Spell = canonicalName
|
cast.Spell = canonicalName
|
||||||
} else {
|
} else {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: spellCastScope(index),
|
Scope: spellCastScope(index),
|
||||||
ReasonCode: ReasonCodeSpellNameUnresolved,
|
ReasonCode: ReasonCodeSpellNameUnresolved,
|
||||||
Message: fmt.Sprintf("input index %d: spell name %q could not be resolved in the effective catalog",
|
Message: fmt.Sprintf("input index %d: spell name %q could not be resolved in the effective catalog",
|
||||||
@@ -141,7 +141,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
|
|||||||
canonicalRefs, orderChanged, duplicateCount := canonicalizeSourceRefs(order, inputCast.SourceRefs)
|
canonicalRefs, orderChanged, duplicateCount := canonicalizeSourceRefs(order, inputCast.SourceRefs)
|
||||||
cast.SourceRefs = canonicalRefs
|
cast.SourceRefs = canonicalRefs
|
||||||
if orderChanged || duplicateCount > 0 {
|
if orderChanged || duplicateCount > 0 {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: spellCastScope(index),
|
Scope: spellCastScope(index),
|
||||||
ReasonCode: ReasonCodeSourceReferencesNormalized,
|
ReasonCode: ReasonCodeSourceReferencesNormalized,
|
||||||
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d, order changed %t, duplicates removed %d)",
|
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d, order changed %t, duplicates removed %d)",
|
||||||
@@ -179,7 +179,7 @@ type duplicateGroup struct {
|
|||||||
removed []int
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (dnd.SpellList, []contracts.Warning) {
|
func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (dnd.SpellList, []diagnostics.Finding) {
|
||||||
if len(input.SpellCasts) == 0 {
|
if len(input.SpellCasts) == 0 {
|
||||||
return input, nil
|
return input, nil
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.Docum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if len(group.removed) == 0 {
|
if len(group.removed) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -264,7 +264,7 @@ func writeKeyInt(builder *strings.Builder, value int) {
|
|||||||
builder.WriteByte(';')
|
builder.WriteByte(';')
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
|
||||||
const maxDisplayedIndices = 20
|
const maxDisplayedIndices = 20
|
||||||
displayed := removed
|
displayed := removed
|
||||||
if len(displayed) > maxDisplayedIndices {
|
if len(displayed) > maxDisplayedIndices {
|
||||||
@@ -279,7 +279,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
|||||||
if omitted := len(removed) - len(displayed); omitted > 0 {
|
if omitted := len(removed) - len(displayed); omitted > 0 {
|
||||||
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
|
||||||
}
|
}
|
||||||
return contracts.Warning{
|
return diagnostics.Finding{
|
||||||
Scope: spellCastScope(retainedIndex),
|
Scope: spellCastScope(retainedIndex),
|
||||||
ReasonCode: ReasonCodeDuplicateSpellCastCollapsed,
|
ReasonCode: ReasonCodeDuplicateSpellCastCollapsed,
|
||||||
Message: message,
|
Message: message,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
||||||
spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
|
spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,13 +150,13 @@ func TestNormalizeCanonicalizesNamesAndReportsUnresolvedNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
|
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
|
||||||
unresolved := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameUnresolved)
|
unresolved := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameUnresolved)
|
||||||
if len(result.Warnings) != 0 || canonicalized == nil || canonicalized.Disposition != contracts.DiagnosticDispositionObservation || canonicalized.OccurrenceCount != 3 || unresolved == nil || unresolved.Disposition != contracts.DiagnosticDispositionAdvisory || unresolved.OccurrenceCount != 1 || unresolved.Samples[0].Scope != "spell_casts[3]" || strings.Contains(unresolved.Samples[0].Message, "Mystery\nSpell") || !strings.Contains(unresolved.Samples[0].Message, `Mystery\nSpell\tName`) {
|
if canonicalized == nil || canonicalized.Disposition != contracts.DiagnosticDispositionObservation || canonicalized.OccurrenceCount != 3 || unresolved == nil || unresolved.Disposition != contracts.DiagnosticDispositionAdvisory || unresolved.OccurrenceCount != 1 || unresolved.Samples[0].Scope != "spell_casts[3]" || strings.Contains(unresolved.Samples[0].Message, "Mystery\nSpell") || !strings.Contains(unresolved.Samples[0].Message, `Mystery\nSpell\tName`) {
|
||||||
t.Fatalf("diagnostics = %#v, want grouped canonicalization and unresolved-quality diagnostics", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v, want grouped canonicalization and unresolved-quality diagnostics", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeLimitsWarningsWithoutChangingSpellValues(t *testing.T) {
|
func TestNormalizeLimitsWarningsWithoutChangingSpellValues(t *testing.T) {
|
||||||
input := dnd.SpellList{SpellCasts: make([]dnd.SpellCast, diagnostics.MaxWarnings+1)}
|
input := dnd.SpellList{SpellCasts: make([]dnd.SpellCast, contracts.MaxDiagnosticSamples+1)}
|
||||||
for index := range input.SpellCasts {
|
for index := range input.SpellCasts {
|
||||||
input.SpellCasts[index].Spell = fmt.Sprintf("Unknown Spell %d", index)
|
input.SpellCasts[index].Spell = fmt.Sprintf("Unknown Spell %d", index)
|
||||||
}
|
}
|
||||||
@@ -168,7 +167,7 @@ func TestNormalizeLimitsWarningsWithoutChangingSpellValues(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(result.Value, input) {
|
if !reflect.DeepEqual(result.Value, input) {
|
||||||
t.Fatalf("normalized value = %#v, want unresolved spell values preserved", result.Value)
|
t.Fatalf("normalized value = %#v, want unresolved spell values preserved", result.Value)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCodeSpellNameUnresolved || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory || result.Diagnostics[0].OccurrenceCount != len(input.SpellCasts) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCodeSpellNameUnresolved || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory || result.Diagnostics[0].OccurrenceCount != len(input.SpellCasts) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,7 +349,7 @@ func TestNormalizeCollapsesDuplicateGroupsAfterCanonicalization(t *testing.T) {
|
|||||||
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
|
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
|
||||||
refsNormalized := diagnosticByReason(result.Diagnostics, ReasonCodeSourceReferencesNormalized)
|
refsNormalized := diagnosticByReason(result.Diagnostics, ReasonCodeSourceReferencesNormalized)
|
||||||
collapsed := diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed)
|
collapsed := diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed)
|
||||||
if len(result.Warnings) != 0 || canonicalized == nil || canonicalized.OccurrenceCount != 3 || refsNormalized == nil || refsNormalized.OccurrenceCount != 1 || collapsed == nil || collapsed.OccurrenceCount != 2 || !strings.Contains(collapsed.Samples[0].Message, "retained input index 0") {
|
if canonicalized == nil || canonicalized.OccurrenceCount != 3 || refsNormalized == nil || refsNormalized.OccurrenceCount != 1 || collapsed == nil || collapsed.OccurrenceCount != 2 || !strings.Contains(collapsed.Samples[0].Message, "retained input index 0") {
|
||||||
t.Fatalf("diagnostics = %#v, want grouped normalization observations", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v, want grouped normalization observations", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,10 +379,8 @@ func TestNormalizeKeepsDistinctAndIneligibleCastsSeparate(t *testing.T) {
|
|||||||
if len(result.Value.SpellCasts) != 2 {
|
if len(result.Value.SpellCasts) != 2 {
|
||||||
t.Fatalf("normalized casts = %#v, want both casts retained", result.Value.SpellCasts)
|
t.Fatalf("normalized casts = %#v, want both casts retained", result.Value.SpellCasts)
|
||||||
}
|
}
|
||||||
for _, warning := range result.Warnings {
|
if diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed) != nil {
|
||||||
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
|
t.Fatalf("diagnostics = %#v, want no duplicate collapse", result.Diagnostics)
|
||||||
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -404,10 +401,8 @@ func TestNormalizeDoesNotCollapseAdjacentOrOverlappingEvidence(t *testing.T) {
|
|||||||
if len(result.Value.SpellCasts) != len(input.SpellCasts) {
|
if len(result.Value.SpellCasts) != len(input.SpellCasts) {
|
||||||
t.Fatalf("normalized casts = %#v, want adjacent and overlapping evidence retained", result.Value.SpellCasts)
|
t.Fatalf("normalized casts = %#v, want adjacent and overlapping evidence retained", result.Value.SpellCasts)
|
||||||
}
|
}
|
||||||
for _, warning := range result.Warnings {
|
if diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed) != nil {
|
||||||
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
|
t.Fatalf("diagnostics = %#v, want no duplicate collapse", result.Diagnostics)
|
||||||
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,8 +443,8 @@ func TestNormalizeIsIdempotentForAlreadyNormalizedInput(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("second Normalize() error = %v, want nil", err)
|
t.Fatalf("second Normalize() error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(second.Value, first.Value) || len(first.Warnings) != 0 || len(second.Warnings) != 0 {
|
if !reflect.DeepEqual(second.Value, first.Value) {
|
||||||
t.Fatalf("first = %#v/%#v, second = %#v/%#v, want identical artifacts without mutation warnings", first.Value, first.Warnings, second.Value, second.Warnings)
|
t.Fatalf("first = %#v, second = %#v, want identical artifacts", first.Value, second.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package diagnostics provides bounded, safe text for deterministic D&D
|
// Package diagnostics provides bounded, safe text for deterministic D&D
|
||||||
// decisions and warnings.
|
// decisions and findings.
|
||||||
package diagnostics
|
package diagnostics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,15 +13,22 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
MaxIssues = 20
|
MaxIssues = 20
|
||||||
MaxWarnings = 20
|
|
||||||
MaxDisplayedRunes = 128
|
MaxDisplayedRunes = 128
|
||||||
MaxMessageBytes = 4096
|
MaxMessageBytes = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Finding is a local, ungrouped D&D diagnostic fact. Producers convert these
|
||||||
|
// to bounded framework diagnostics at their result boundary.
|
||||||
|
type Finding struct {
|
||||||
|
Scope string
|
||||||
|
ReasonCode string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
// DataQualityResult converts accepted source-quality findings into bounded,
|
// DataQualityResult converts accepted source-quality findings into bounded,
|
||||||
// locally grouped advisories. These findings do not indicate process
|
// locally grouped advisories. These findings do not indicate process
|
||||||
// degradation.
|
// degradation.
|
||||||
func DataQualityResult(findings []contracts.Warning) (contracts.ValidationResult, error) {
|
func DataQualityResult(findings []Finding) (contracts.ValidationResult, error) {
|
||||||
diagnostics, err := Collect(findings, contracts.DiagnosticDispositionAdvisory, contracts.DiagnosticCategoryDataQuality)
|
diagnostics, err := Collect(findings, contracts.DiagnosticDispositionAdvisory, contracts.DiagnosticCategoryDataQuality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.ValidationResult{}, err
|
return contracts.ValidationResult{}, err
|
||||||
@@ -31,7 +38,7 @@ func DataQualityResult(findings []contracts.Warning) (contracts.ValidationResult
|
|||||||
|
|
||||||
// Collect converts individual deterministic findings into bounded,
|
// Collect converts individual deterministic findings into bounded,
|
||||||
// occurrence-grouped producer diagnostics with one consistent classification.
|
// occurrence-grouped producer diagnostics with one consistent classification.
|
||||||
func Collect(findings []contracts.Warning, disposition contracts.DiagnosticDisposition, category contracts.DiagnosticCategory) ([]contracts.ProducerDiagnostic, error) {
|
func Collect(findings []Finding, disposition contracts.DiagnosticDisposition, category contracts.DiagnosticCategory) ([]contracts.ProducerDiagnostic, error) {
|
||||||
collector := frameworkdiagnostics.NewCollector()
|
collector := frameworkdiagnostics.NewCollector()
|
||||||
for _, finding := range findings {
|
for _, finding := range findings {
|
||||||
if err := collector.Add(contracts.ProducerDiagnostic{
|
if err := collector.Add(contracts.ProducerDiagnostic{
|
||||||
@@ -49,13 +56,13 @@ func Collect(findings []contracts.Warning, disposition contracts.DiagnosticDispo
|
|||||||
|
|
||||||
// NormalizationDiagnostics classifies deterministic normalization findings as
|
// NormalizationDiagnostics classifies deterministic normalization findings as
|
||||||
// observations, except for explicitly identified unresolved-quality findings.
|
// observations, except for explicitly identified unresolved-quality findings.
|
||||||
func NormalizationDiagnostics(findings []contracts.Warning, advisoryReasonCodes ...string) ([]contracts.ProducerDiagnostic, error) {
|
func NormalizationDiagnostics(findings []Finding, advisoryReasonCodes ...string) ([]contracts.ProducerDiagnostic, error) {
|
||||||
advisoryReasons := make(map[string]struct{}, len(advisoryReasonCodes))
|
advisoryReasons := make(map[string]struct{}, len(advisoryReasonCodes))
|
||||||
for _, reasonCode := range advisoryReasonCodes {
|
for _, reasonCode := range advisoryReasonCodes {
|
||||||
advisoryReasons[reasonCode] = struct{}{}
|
advisoryReasons[reasonCode] = struct{}{}
|
||||||
}
|
}
|
||||||
observations := make([]contracts.Warning, 0, len(findings))
|
observations := make([]Finding, 0, len(findings))
|
||||||
advisories := make([]contracts.Warning, 0)
|
advisories := make([]Finding, 0)
|
||||||
for _, finding := range findings {
|
for _, finding := range findings {
|
||||||
if _, advisory := advisoryReasons[finding.ReasonCode]; advisory {
|
if _, advisory := advisoryReasons[finding.ReasonCode]; advisory {
|
||||||
advisories = append(advisories, finding)
|
advisories = append(advisories, finding)
|
||||||
@@ -97,28 +104,6 @@ func Aggregate(prefix string, issues []string) string {
|
|||||||
return aggregateMessage(prefix, displayed, len(issues)-len(displayed))
|
return aggregateMessage(prefix, displayed, len(issues)-len(displayed))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LimitWarnings returns at most MaxWarnings warnings, reserving the final
|
|
||||||
// position for a deterministic omission summary when truncation is required.
|
|
||||||
func LimitWarnings(warnings []contracts.Warning, scope, reasonCode string) []contracts.Warning {
|
|
||||||
if warnings == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(warnings) <= MaxWarnings {
|
|
||||||
bounded := make([]contracts.Warning, len(warnings))
|
|
||||||
copy(bounded, warnings)
|
|
||||||
return bounded
|
|
||||||
}
|
|
||||||
displayed := MaxWarnings - 1
|
|
||||||
bounded := make([]contracts.Warning, displayed, MaxWarnings)
|
|
||||||
copy(bounded, warnings[:displayed])
|
|
||||||
bounded = append(bounded, contracts.Warning{
|
|
||||||
Scope: scope,
|
|
||||||
ReasonCode: reasonCode,
|
|
||||||
Message: fmt.Sprintf("%d additional warning(s) omitted", len(warnings)-displayed),
|
|
||||||
})
|
|
||||||
return bounded
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregateMessage(prefix string, issues []string, omitted int) string {
|
func aggregateMessage(prefix string, issues []string, omitted int) string {
|
||||||
message := prefix + ": " + strings.Join(issues, ", ")
|
message := prefix + ": " + strings.Join(issues, ", ")
|
||||||
if omitted > 0 {
|
if omitted > 0 {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package diagnostics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -30,30 +29,8 @@ func TestAggregateEnforcesByteBudgetAndReportsOmissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLimitWarningsBoundsOutputAndReportsOmissions(t *testing.T) {
|
|
||||||
warnings := make([]contracts.Warning, MaxWarnings+3)
|
|
||||||
for index := range warnings {
|
|
||||||
warnings[index] = contracts.Warning{ReasonCode: fmt.Sprintf("warning-%d", index)}
|
|
||||||
}
|
|
||||||
before := append([]contracts.Warning(nil), warnings...)
|
|
||||||
|
|
||||||
got := LimitWarnings(warnings, "records", "warnings_omitted")
|
|
||||||
if len(got) != MaxWarnings {
|
|
||||||
t.Fatalf("LimitWarnings() count = %d, want %d", len(got), MaxWarnings)
|
|
||||||
}
|
|
||||||
summary := got[len(got)-1]
|
|
||||||
wantOmitted := len(warnings) - (MaxWarnings - 1)
|
|
||||||
if summary.Scope != "records" || summary.ReasonCode != "warnings_omitted" ||
|
|
||||||
summary.Message != fmt.Sprintf("%d additional warning(s) omitted", wantOmitted) {
|
|
||||||
t.Fatalf("summary = %#v", summary)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(warnings, before) {
|
|
||||||
t.Fatal("LimitWarnings() mutated its input")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
||||||
result, err := DataQualityResult([]contracts.Warning{
|
result, err := DataQualityResult([]Finding{
|
||||||
{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
|
{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
|
||||||
{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
|
{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
|
||||||
{Scope: "records[1]", ReasonCode: "not_near_source", Message: "second"},
|
{Scope: "records[1]", ReasonCode: "not_near_source", Message: "second"},
|
||||||
@@ -61,7 +38,7 @@ func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
if !result.Approved || len(result.Diagnostics) != 1 {
|
||||||
t.Fatalf("result = %#v", result)
|
t.Fatalf("result = %#v", result)
|
||||||
}
|
}
|
||||||
diagnostic := result.Diagnostics[0]
|
diagnostic := result.Diagnostics[0]
|
||||||
@@ -71,9 +48,9 @@ func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectGroupsNormalizationFindingsWithBoundedSamples(t *testing.T) {
|
func TestCollectGroupsNormalizationFindingsWithBoundedSamples(t *testing.T) {
|
||||||
findings := make([]contracts.Warning, 5)
|
findings := make([]Finding, 5)
|
||||||
for index := range findings {
|
for index := range findings {
|
||||||
findings[index] = contracts.Warning{
|
findings[index] = Finding{
|
||||||
Scope: fmt.Sprintf("records[%d]", index),
|
Scope: fmt.Sprintf("records[%d]", index),
|
||||||
ReasonCode: "record_normalized",
|
ReasonCode: "record_normalized",
|
||||||
Message: fmt.Sprintf("record %d normalized", index),
|
Message: fmt.Sprintf("record %d normalized", index),
|
||||||
@@ -94,7 +71,7 @@ func TestCollectGroupsNormalizationFindingsWithBoundedSamples(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizationDiagnosticsCombinesQualityAndCleanupWithoutWarnings(t *testing.T) {
|
func TestNormalizationDiagnosticsCombinesQualityAndCleanupWithoutWarnings(t *testing.T) {
|
||||||
groups, err := NormalizationDiagnostics([]contracts.Warning{
|
groups, err := NormalizationDiagnostics([]Finding{
|
||||||
{Scope: "spell_casts[0]", ReasonCode: "spell_name_canonicalized", Message: "canonicalized spell name"},
|
{Scope: "spell_casts[0]", ReasonCode: "spell_name_canonicalized", Message: "canonicalized spell name"},
|
||||||
{Scope: "spell_casts[1]", ReasonCode: "spell_name_unresolved", Message: "spell was not in the catalog"},
|
{Scope: "spell_casts[1]", ReasonCode: "spell_name_unresolved", Message: "spell was not in the catalog"},
|
||||||
}, "spell_name_unresolved")
|
}, "spell_name_unresolved")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
func TestValidatorApprovesNormalizedCombatTurns(t *testing.T) {
|
func TestValidatorApprovesNormalizedCombatTurns(t *testing.T) {
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: invariantDocument(), Value: normalizedList()})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: invariantDocument(), Value: normalizedList()})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func TestValidatorApprovesWellFormedCombatTurnList(t *testing.T) {
|
func TestValidatorApprovesWellFormedCombatTurnList(t *testing.T) {
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Value: validCombatTurnList()})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Value: validCombatTurnList()})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func TestValidatorEnforcesCurrentChunkEvidenceDuringExtraction(t *testing.T) {
|
|||||||
func TestValidatorDefersMalformedShape(t *testing.T) {
|
func TestValidatorDefersMalformedShape(t *testing.T) {
|
||||||
value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{Actor: "Aria"}}}
|
value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{Actor: "Aria"}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("shape deferral = %#v, %v; want approval without source warning", result, err)
|
t.Fatalf("shape deferral = %#v, %v; want approval without source warning", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
}
|
}
|
||||||
citedTexts[turnIndex] = citedText
|
citedTexts[turnIndex] = citedText
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for turnIndex, turn := range req.Value.CombatTurns {
|
for turnIndex, turn := range req.Value.CombatTurns {
|
||||||
citedText := citedTexts[turnIndex]
|
citedText := citedTexts[turnIndex]
|
||||||
if actorAppearsInCitedText(citedText, turn.Actor) {
|
if actorAppearsInCitedText(citedText, turn.Actor) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: fmt.Sprintf("combat_turns[%d]", turnIndex),
|
Scope: fmt.Sprintf("combat_turns[%d]", turnIndex),
|
||||||
ReasonCode: ReasonCode,
|
ReasonCode: ReasonCode,
|
||||||
Message: diagnostics.Aggregate("combat turn not near source", []string{
|
Message: diagnostics.Aggregate("combat turn not near source", []string{
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestValidatorWarnsOncePerTurnForUnrelatedActor(t *testing.T) {
|
|||||||
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
|
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
|
||||||
}}}
|
}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: relatednessDocument(), Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: relatednessDocument(), Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||||
t.Fatalf("Validate() = %#v, %v; want one advisory for the turn", result, err)
|
t.Fatalf("Validate() = %#v, %v; want one advisory for the turn", result, err)
|
||||||
}
|
}
|
||||||
diagnostic := result.Diagnostics[0]
|
diagnostic := result.Diagnostics[0]
|
||||||
@@ -62,7 +62,7 @@ func TestValidatorLimitsUnrelatedActorWarnings(t *testing.T) {
|
|||||||
if err != nil || !result.Approved {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[0].OmittedSampleCount != len(turns)-contracts.MaxDiagnosticSamples {
|
if len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[0].OmittedSampleCount != len(turns)-contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func TestRepresentativeProductionValidatorResultsSatisfyCorrectionContract(t *te
|
|||||||
name string
|
name string
|
||||||
validate func() (contracts.ValidationResult, error)
|
validate func() (contracts.ValidationResult, error)
|
||||||
wantApproved bool
|
wantApproved bool
|
||||||
wantWarningCount int
|
|
||||||
wantDiagnosticCount int
|
wantDiagnosticCount int
|
||||||
wantDisposition contracts.DiagnosticDisposition
|
wantDisposition contracts.DiagnosticDisposition
|
||||||
}{
|
}{
|
||||||
@@ -123,9 +122,6 @@ func TestRepresentativeProductionValidatorResultsSatisfyCorrectionContract(t *te
|
|||||||
if result.Approved != test.wantApproved {
|
if result.Approved != test.wantApproved {
|
||||||
t.Fatalf("Validate() approved = %t, want %t: %#v", result.Approved, test.wantApproved, result)
|
t.Fatalf("Validate() approved = %t, want %t: %#v", result.Approved, test.wantApproved, result)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != test.wantWarningCount {
|
|
||||||
t.Fatalf("Validate() warnings = %d, want %d: %#v", len(result.Warnings), test.wantWarningCount, result)
|
|
||||||
}
|
|
||||||
if len(result.Diagnostics) != test.wantDiagnosticCount {
|
if len(result.Diagnostics) != test.wantDiagnosticCount {
|
||||||
t.Fatalf("Validate() diagnostics = %d, want %d: %#v", len(result.Diagnostics), test.wantDiagnosticCount, result)
|
t.Fatalf("Validate() diagnostics = %d, want %d: %#v", len(result.Diagnostics), test.wantDiagnosticCount, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
func TestValidatorAcceptsEmptyAndWellFormedEventLists(t *testing.T) {
|
func TestValidatorAcceptsEmptyAndWellFormedEventLists(t *testing.T) {
|
||||||
for _, value := range []dnd.EnemyEventList{{Events: []dnd.EnemyEvent{}}, validEventList()} {
|
for _, value := range []dnd.EnemyEventList{{Events: []dnd.EnemyEvent{}}, validEventList()} {
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.ValidationResult{Approved: true}, nil
|
return contracts.ValidationResult{Approved: true}, nil
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for eventIndex, event := range req.Value.Events {
|
for eventIndex, event := range req.Value.Events {
|
||||||
citedText, err := resolver.CitedText(event.SourceRefs)
|
citedText, err := resolver.CitedText(event.SourceRefs)
|
||||||
if err != nil || shared.ContainsTokenSequence(citedText, event.Name) {
|
if err != nil || shared.ContainsTokenSequence(citedText, event.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: fmt.Sprintf("events[%d]", eventIndex),
|
Scope: fmt.Sprintf("events[%d]", eventIndex),
|
||||||
ReasonCode: ReasonCode,
|
ReasonCode: ReasonCode,
|
||||||
Message: diagnostics.Aggregate("enemy event subject not near source", []string{
|
Message: diagnostics.Aggregate("enemy event subject not near source", []string{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func TestValidatorUsesOnlyCitedTranscriptEvidence(t *testing.T) {
|
|||||||
value := validEventList()
|
value := validEventList()
|
||||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"npc_registry": {Items: []contracts.ReferenceItem{{Content: []byte("Ashfang")}}}}}
|
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"npc_registry": {Items: []contracts.ReferenceItem{{Content: []byte("Ashfang")}}}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("The party waits."), References: references, Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("The party waits."), References: references, Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ func TestValidatorDefersMalformedValuesAndBoundsWarnings(t *testing.T) {
|
|||||||
value.Events[index] = dnd.EnemyEvent{Name: "Missing", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
value.Events[index] = dnd.EnemyEvent{Name: "Missing", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||||
}
|
}
|
||||||
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("none"), Value: value})
|
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("none"), Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(value.Events) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(value.Events) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
registry := pipeline.NewValidatorRegistry()
|
registry := pipeline.NewValidatorRegistry()
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
return contracts.ValidationResult{Approved: true}, nil
|
return contracts.ValidationResult{Approved: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, occurrence := range req.Value.Occurrences {
|
for index, occurrence := range req.Value.Occurrences {
|
||||||
citedText, err := resolver.CitedText(occurrence.SourceRefs)
|
citedText, err := resolver.CitedText(occurrence.SourceRefs)
|
||||||
if err != nil || shared.ContainsTokenSequence(citedText, occurrence.Name) {
|
if err != nil || shared.ContainsTokenSequence(citedText, occurrence.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: fmt.Sprintf("occurrences[%d]", index),
|
Scope: fmt.Sprintf("occurrences[%d]", index),
|
||||||
ReasonCode: ReasonCode,
|
ReasonCode: ReasonCode,
|
||||||
Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestValidatorMatchesTokenSequencesAcrossRanges(t *testing.T) {
|
|||||||
"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("missing-item")}}},
|
"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("missing-item")}}},
|
||||||
}}
|
}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemOccurrenceList]{Source: doc, References: references, Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemOccurrenceList]{Source: doc, References: references, Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
if diagnostic := result.Diagnostics[0]; diagnostic.Samples[0].Scope != "occurrences[1]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(diagnostic.Samples[0].Message, "missing-item") {
|
if diagnostic := result.Diagnostics[0]; diagnostic.Samples[0].Scope != "occurrences[1]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(diagnostic.Samples[0].Message, "missing-item") {
|
||||||
@@ -56,7 +56,7 @@ func TestValidatorBoundsWarningsAndRegistersPolicy(t *testing.T) {
|
|||||||
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "nothing useful"}}},
|
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "nothing useful"}}},
|
||||||
Value: dnd.ItemOccurrenceList{Occurrences: occurrences},
|
Value: dnd.ItemOccurrenceList{Occurrences: occurrences},
|
||||||
})
|
})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
if got := New(Options{}).CheckpointFingerprints(); !reflect.DeepEqual(got, []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}) {
|
if got := New(Options{}).CheckpointFingerprints(); !reflect.DeepEqual(got, []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}) {
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
}
|
}
|
||||||
citedTexts[itemIndex] = citedText
|
citedTexts[itemIndex] = citedText
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for itemIndex, item := range req.Value.Items {
|
for itemIndex, item := range req.Value.Items {
|
||||||
if shared.ContainsTokenSequence(citedTexts[itemIndex], item.Name) {
|
if shared.ContainsTokenSequence(citedTexts[itemIndex], item.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: fmt.Sprintf("items[%d]", itemIndex), ReasonCode: ReasonCode,
|
Scope: fmt.Sprintf("items[%d]", itemIndex), ReasonCode: ReasonCode,
|
||||||
Message: fmt.Sprintf("Item %s was not found in cited source text", diagnostics.Quote(item.Name)),
|
Message: fmt.Sprintf("Item %s was not found in cited source text", diagnostics.Quote(item.Name)),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
|||||||
value.Items[0].Name = "Glossary Relic"
|
value.Items[0].Name = "Glossary Relic"
|
||||||
before := value
|
before := value
|
||||||
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: doc, References: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Relic")}}}}}, Value: value})
|
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: doc, References: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Relic")}}}}}, Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !reflect.DeepEqual(value, before) {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !reflect.DeepEqual(value, before) {
|
||||||
t.Fatalf("reference-only match = %#v, %v; want advisory warning", result, err)
|
t.Fatalf("reference-only match = %#v, %v; want advisory warning", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func TestValidatorBoundsWarningsAndRegisters(t *testing.T) {
|
|||||||
items[index] = dnd.Item{ID: "item", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
items[index] = dnd.Item{ID: "item", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||||
}
|
}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "Nothing here."}}}, Value: dnd.ItemRegistry{Items: items}})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "Nothing here."}}}, Value: dnd.ItemRegistry{Items: items}})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(items) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(items) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
registry := pipeline.NewValidatorRegistry()
|
registry := pipeline.NewValidatorRegistry()
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
}
|
}
|
||||||
citedTexts[index] = citedText
|
citedTexts[index] = citedText
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, occurrence := range req.Value.Occurrences {
|
for index, occurrence := range req.Value.Occurrences {
|
||||||
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{Scope: fmt.Sprintf("occurrences[%d]", index), ReasonCode: ReasonCode, Message: fmt.Sprintf("Location occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name))})
|
warnings = append(warnings, diagnostics.Finding{Scope: fmt.Sprintf("occurrences[%d]", index), ReasonCode: ReasonCode, Message: fmt.Sprintf("Location occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name))})
|
||||||
}
|
}
|
||||||
return diagnostics.DataQualityResult(warnings)
|
return diagnostics.DataQualityResult(warnings)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestValidatorUsesOnlyCitedTranscriptEvidence(t *testing.T) {
|
|||||||
before := cloneList(value)
|
before := cloneList(value)
|
||||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Keep")}}}}}
|
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Keep")}}}}}
|
||||||
result, err = New(Options{}).Validate(context.Background(), request(doc, value, references))
|
result, err = New(Options{}).Validate(context.Background(), request(doc, value, references))
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
||||||
t.Fatalf("reference-only match = %#v, %v", result, err)
|
t.Fatalf("reference-only match = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func TestValidatorDefersUnreadableEvidenceAndBoundsWarnings(t *testing.T) {
|
|||||||
occurrences[index] = occurrenceList("Missing").Occurrences[0]
|
occurrences[index] = occurrenceList("Missing").Occurrences[0]
|
||||||
}
|
}
|
||||||
result, err = New(Options{}).Validate(context.Background(), request(document(), dnd.LocationOccurrenceList{Occurrences: occurrences}, contracts.ReferenceSet{}))
|
result, err = New(Options{}).Validate(context.Background(), request(document(), dnd.LocationOccurrenceList{Occurrences: occurrences}, contracts.ReferenceSet{}))
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(occurrences) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(occurrences) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
}
|
}
|
||||||
citedTexts[locationIndex] = citedText
|
citedTexts[locationIndex] = citedText
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for locationIndex, location := range req.Value.Locations {
|
for locationIndex, location := range req.Value.Locations {
|
||||||
if shared.ContainsTokenSequence(citedTexts[locationIndex], location.Name) {
|
if shared.ContainsTokenSequence(citedTexts[locationIndex], location.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: fmt.Sprintf("locations[%d]", locationIndex), ReasonCode: ReasonCode,
|
Scope: fmt.Sprintf("locations[%d]", locationIndex), ReasonCode: ReasonCode,
|
||||||
Message: fmt.Sprintf("Location %s was not found in cited source text", diagnostics.Quote(location.Name)),
|
Message: fmt.Sprintf("Location %s was not found in cited source text", diagnostics.Quote(location.Name)),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
|||||||
before := value
|
before := value
|
||||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Keep")}}}}}
|
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Glossary Keep")}}}}}
|
||||||
result, err = New(Options{}).Validate(context.Background(), request(doc, value, references))
|
result, err = New(Options{}).Validate(context.Background(), request(doc, value, references))
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].ReasonCode != ReasonCode || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "Glossary Keep") || !reflect.DeepEqual(value, before) {
|
||||||
t.Fatalf("reference-only match = %#v, %v; want advisory warning", result, err)
|
t.Fatalf("reference-only match = %#v, %v; want advisory warning", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func TestValidatorBoundsWarnings(t *testing.T) {
|
|||||||
locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||||
}
|
}
|
||||||
result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationRegistry{Locations: locations}, contracts.ReferenceSet{}))
|
result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationRegistry{Locations: locations}, contracts.ReferenceSet{}))
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(locations) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(locations) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
t.Fatalf("bounded advisories = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
}
|
}
|
||||||
citedTexts[index] = citedText
|
citedTexts[index] = citedText
|
||||||
}
|
}
|
||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]diagnostics.Finding, 0)
|
||||||
for index, occurrence := range req.Value.Occurrences {
|
for index, occurrence := range req.Value.Occurrences {
|
||||||
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, diagnostics.Finding{
|
||||||
Scope: fmt.Sprintf("occurrences[%d]", index),
|
Scope: fmt.Sprintf("occurrences[%d]", index),
|
||||||
ReasonCode: ReasonCode,
|
ReasonCode: ReasonCode,
|
||||||
Message: fmt.Sprintf("NPC occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
Message: fmt.Sprintf("NPC occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestValidatorUsesOnlyCurrentTranscriptAndWarnsOncePerOccurrence(t *testing.
|
|||||||
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "O’Rin Thorn speaks."}, {ID: 2, Text: "The party waits."}}}
|
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "O’Rin Thorn speaks."}, {ID: 2, Text: "The party waits."}}}
|
||||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Missing NPC")}}}}}
|
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Missing NPC")}}}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{Source: doc, References: references, Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{Source: doc, References: references, Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
|
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
if diagnostic := result.Diagnostics[0]; diagnostic.Samples[0].Scope != "occurrences[1]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(diagnostic.Samples[0].Message, `Missing\nNPC`) {
|
if diagnostic := result.Diagnostics[0]; diagnostic.Samples[0].Scope != "occurrences[1]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(diagnostic.Samples[0].Message, `Missing\nNPC`) {
|
||||||
@@ -68,7 +68,7 @@ func TestValidatorBoundsWarnings(t *testing.T) {
|
|||||||
if err != nil || !result.Approved {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("Validate() = %#v, %v", result, err)
|
t.Fatalf("Validate() = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
if len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != count || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples {
|
||||||
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
t.Fatalf("diagnostics = %#v", result.Diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user