Classify source relatedness as data-quality advisories

This commit is contained in:
2026-08-27 15:49:53 +00:00
parent 1f1967c8d2
commit ccba2ce3f9
25 changed files with 232 additions and 189 deletions

View File

@@ -9,13 +9,14 @@ import (
"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"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape"
)
const (
Key = "extract/dnd/spells/source_relatedness"
WarningReasonCode = "spell_not_near_source"
policy = "dnd.spells.validator.source_relatedness.v1"
Key = "extract/dnd/spells/source_relatedness"
ReasonCode = "spell_not_near_source"
policy = "dnd.spells.validator.source_relatedness.v1"
)
type Options struct{}
@@ -51,10 +52,10 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
var warnings []contracts.Warning
for spellIndex, spell := range req.Value.SpellCasts {
if !spellAppearsInCitedText(citedTexts[spellIndex], spell) {
warnings = append(warnings, contracts.Warning{Scope: fmt.Sprintf("spell_casts[%d]", spellIndex), ReasonCode: WarningReasonCode, Message: fmt.Sprintf("spell %q was not found in cited source text", strings.TrimSpace(spell.Spell))})
warnings = append(warnings, contracts.Warning{Scope: fmt.Sprintf("spell_casts[%d]", spellIndex), ReasonCode: ReasonCode, Message: fmt.Sprintf("spell %s was not found in cited source text", diagnostics.Quote(strings.TrimSpace(spell.Spell)))})
}
}
return contracts.ValidationResult{Approved: true, Warnings: warnings}, nil
return diagnostics.DataQualityResult(warnings)
}
func spellAppearsInCitedText(citedText string, spell dnd.SpellCast) bool {
name := strings.TrimSpace(spell.Spell)

View File

@@ -18,8 +18,8 @@ func TestValidatorApprovesWithoutWarningWhenSpellAppearsInCitedText(t *testing.T
if !result.Approved {
t.Fatalf("Approved = false, want true")
}
if len(result.Warnings) != 0 {
t.Fatalf("Warnings = %#v, want none", result.Warnings)
if len(result.Diagnostics) != 0 {
t.Fatalf("Diagnostics = %#v, want none", result.Diagnostics)
}
}
@@ -31,11 +31,11 @@ func TestValidatorWarnsWhenSpellDoesNotAppearInCitedText(t *testing.T) {
if !result.Approved {
t.Fatalf("Approved = false, want true")
}
if len(result.Warnings) != 1 {
t.Fatalf("Warnings = %#v, want one warning", result.Warnings)
if len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
t.Fatalf("Diagnostics = %#v, want one advisory", result.Diagnostics)
}
if result.Warnings[0].ReasonCode != WarningReasonCode {
t.Fatalf("ReasonCode = %q, want %q", result.Warnings[0].ReasonCode, WarningReasonCode)
if result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
t.Fatalf("diagnostic = %#v", result.Diagnostics[0])
}
}
@@ -56,7 +56,7 @@ func TestValidatorMatchesCaseInsensitiveUnicodeMultiwordSpellAcrossCitations(t *
},
}
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{Source: doc, Value: value})
if err != nil || !result.Approved || len(result.Warnings) != 0 {
if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
t.Fatalf("Validate() = %#v, %v; want normalized multiword spell approval", result, err)
}
}
@@ -66,8 +66,27 @@ func TestValidatorDoesNotMatchShortSpellNameSubstring(t *testing.T) {
ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session",
Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party said nothing."}},
}, "Aid", 1))
if err != nil || !result.Approved || len(result.Warnings) != 1 {
t.Fatalf("Validate() = %#v, %v; want short-name boundary warning", result, err)
if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
t.Fatalf("Validate() = %#v, %v; want short-name boundary advisory", result, err)
}
}
func TestValidatorBoundsHighCardinalitySpellFindings(t *testing.T) {
count := contracts.MaxDiagnosticSamples + 4
casts := make([]dnd.SpellCast, count)
for index := range casts {
casts[index] = dnd.SpellCast{Caster: "Aria", Spell: "Missing Spell", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
}
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "The party waits."}}},
Value: dnd.SpellList{SpellCasts: casts},
})
if err != nil || !result.Approved || len(result.Warnings) != 0 || len(result.Diagnostics) != 1 {
t.Fatalf("Validate() = %#v, %v", result, err)
}
diagnostic := result.Diagnostics[0]
if diagnostic.OccurrenceCount != count || len(diagnostic.Samples) != contracts.MaxDiagnosticSamples || diagnostic.OmittedSampleCount != count-contracts.MaxDiagnosticSamples {
t.Fatalf("diagnostic = %#v", diagnostic)
}
}
@@ -75,8 +94,8 @@ func TestValidatorIgnoresInvalidCitations(t *testing.T) {
request := requestWithSpell(validDocument(), "Cure Wounds", 2)
request.Value.SpellCasts[0].SourceRefs[0].StartUnitID = 99
result, err := New(Options{}).Validate(context.Background(), request)
if err != nil || !result.Approved || len(result.Warnings) != 0 {
t.Fatalf("Validate() = %#v, %v; want approval without relatedness warning", result, err)
if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
t.Fatalf("Validate() = %#v, %v; want approval without relatedness advisory", result, err)
}
}