Finalize structured diagnostic aggregation

This commit is contained in:
2026-08-27 16:40:17 +00:00
parent 480680b257
commit 4dbbf68051
112 changed files with 656 additions and 893 deletions

View File

@@ -110,8 +110,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
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) {
var warnings []contracts.Warning
func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatalog, order shared.SourceRefOrder) (dnd.SpellList, []diagnostics.Finding) {
var warnings []diagnostics.Finding
if input.SpellCasts == nil {
return dnd.SpellList{}, nil
}
@@ -121,7 +121,7 @@ func normalizeSpellList(input dnd.SpellList, catalog spellcatalog.EffectiveCatal
cast := cloneSpellCast(inputCast)
if canonicalName, ok := catalog.Lookup(inputCast.Spell); ok {
if inputCast.Spell != canonicalName {
warnings = append(warnings, contracts.Warning{
warnings = append(warnings, diagnostics.Finding{
Scope: spellCastScope(index),
ReasonCode: ReasonCodeSpellNameCanonicalized,
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
} else {
warnings = append(warnings, contracts.Warning{
warnings = append(warnings, diagnostics.Finding{
Scope: spellCastScope(index),
ReasonCode: ReasonCodeSpellNameUnresolved,
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)
cast.SourceRefs = canonicalRefs
if orderChanged || duplicateCount > 0 {
warnings = append(warnings, contracts.Warning{
warnings = append(warnings, diagnostics.Finding{
Scope: spellCastScope(index),
ReasonCode: ReasonCodeSourceReferencesNormalized,
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
}
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 {
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 {
if len(group.removed) == 0 {
continue
@@ -264,7 +264,7 @@ func writeKeyInt(builder *strings.Builder, value int) {
builder.WriteByte(';')
}
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
func duplicateWarning(retainedIndex int, removed []int) diagnostics.Finding {
const maxDisplayedIndices = 20
displayed := removed
if len(displayed) > maxDisplayedIndices {
@@ -279,7 +279,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
if omitted := len(removed) - len(displayed); omitted > 0 {
message += fmt.Sprintf("; %d additional removed input indices omitted", omitted)
}
return contracts.Warning{
return diagnostics.Finding{
Scope: spellCastScope(retainedIndex),
ReasonCode: ReasonCodeDuplicateSpellCastCollapsed,
Message: message,

View File

@@ -12,7 +12,6 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"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"
)
@@ -151,13 +150,13 @@ func TestNormalizeCanonicalizesNamesAndReportsUnresolvedNames(t *testing.T) {
}
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
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)
}
}
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 {
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) {
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)
}
}
@@ -350,7 +349,7 @@ func TestNormalizeCollapsesDuplicateGroupsAfterCanonicalization(t *testing.T) {
canonicalized := diagnosticByReason(result.Diagnostics, ReasonCodeSpellNameCanonicalized)
refsNormalized := diagnosticByReason(result.Diagnostics, ReasonCodeSourceReferencesNormalized)
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)
}
}
@@ -380,10 +379,8 @@ func TestNormalizeKeepsDistinctAndIneligibleCastsSeparate(t *testing.T) {
if len(result.Value.SpellCasts) != 2 {
t.Fatalf("normalized casts = %#v, want both casts retained", result.Value.SpellCasts)
}
for _, warning := range result.Warnings {
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
}
if diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed) != nil {
t.Fatalf("diagnostics = %#v, want no duplicate collapse", result.Diagnostics)
}
})
}
@@ -404,10 +401,8 @@ func TestNormalizeDoesNotCollapseAdjacentOrOverlappingEvidence(t *testing.T) {
if len(result.Value.SpellCasts) != len(input.SpellCasts) {
t.Fatalf("normalized casts = %#v, want adjacent and overlapping evidence retained", result.Value.SpellCasts)
}
for _, warning := range result.Warnings {
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
}
if diagnosticByReason(result.Diagnostics, ReasonCodeDuplicateSpellCastCollapsed) != nil {
t.Fatalf("diagnostics = %#v, want no duplicate collapse", result.Diagnostics)
}
}
@@ -448,8 +443,8 @@ func TestNormalizeIsIdempotentForAlreadyNormalizedInput(t *testing.T) {
if err != nil {
t.Fatalf("second Normalize() error = %v, want nil", err)
}
if !reflect.DeepEqual(second.Value, first.Value) || len(first.Warnings) != 0 || len(second.Warnings) != 0 {
t.Fatalf("first = %#v/%#v, second = %#v/%#v, want identical artifacts without mutation warnings", first.Value, first.Warnings, second.Value, second.Warnings)
if !reflect.DeepEqual(second.Value, first.Value) {
t.Fatalf("first = %#v, second = %#v, want identical artifacts", first.Value, second.Value)
}
}