Harden diagnostic handling and warning presentation
This commit is contained in:
@@ -16,7 +16,7 @@ import (
|
||||
func TestValidatorApprovesNormalizedCombatTurns(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: invariantDocument(), Value: normalizedList()})
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
||||
t.Fatalf("Validate() = %#v, %v; want approval without diagnostics", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func TestValidatorApprovesWellFormedCombatTurnList(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Value: validCombatTurnList()})
|
||||
if err != nil || !result.Approved {
|
||||
t.Fatalf("Validate() = %#v, %v; want approval without warnings", result, err)
|
||||
t.Fatalf("Validate() = %#v, %v; want approval without diagnostics", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestValidatorDefersMalformedShape(t *testing.T) {
|
||||
value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{Actor: "Aria"}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value})
|
||||
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 diagnostics", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[turnIndex] = citedText
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for turnIndex, turn := range req.Value.CombatTurns {
|
||||
citedText := citedTexts[turnIndex]
|
||||
if actorAppearsInCitedText(citedText, turn.Actor) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("combat_turns[%d]", turnIndex),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: diagnostics.Aggregate("combat turn not near source", []string{
|
||||
@@ -64,7 +64,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
func actorAppearsInCitedText(citedText string, actor string) bool {
|
||||
return shared.ContainsTokenSequence(citedText, actor)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestValidatorWarnsOncePerTurnForUnrelatedActor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorLimitsUnrelatedActorWarnings(t *testing.T) {
|
||||
func TestValidatorLimitsUnrelatedActorAdvisories(t *testing.T) {
|
||||
turns := make([]dnd.CombatTurn, contracts.MaxDiagnosticSamples+2)
|
||||
for index := range turns {
|
||||
turns[index] = dnd.CombatTurn{
|
||||
|
||||
@@ -42,13 +42,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
if err != nil {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for eventIndex, event := range req.Value.Events {
|
||||
citedText, err := resolver.CitedText(event.SourceRefs)
|
||||
if err != nil || shared.ContainsTokenSequence(citedText, event.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("events[%d]", eventIndex),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: diagnostics.Aggregate("enemy event subject not near source", []string{
|
||||
@@ -56,7 +56,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestValidatorAcceptsUnicodeSubjectInCitedEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorDefersMalformedValuesAndBoundsWarnings(t *testing.T) {
|
||||
func TestValidatorDefersMalformedValuesAndBoundsAdvisories(t *testing.T) {
|
||||
malformed := dnd.EnemyEventList{Events: []dnd.EnemyEvent{{Name: "Ashfang"}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document("none"), Value: malformed})
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
|
||||
|
||||
@@ -45,19 +45,19 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
citedText, err := resolver.CitedText(occurrence.SourceRefs)
|
||||
if err != nil || shared.ContainsTokenSequence(citedText, occurrence.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("occurrences[%d]", index),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestValidatorDefersMalformedAndUnreadableEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorBoundsWarningsAndRegistersPolicy(t *testing.T) {
|
||||
func TestValidatorBoundsAdvisoriesAndRegistersPolicy(t *testing.T) {
|
||||
count := contracts.MaxDiagnosticSamples + 5
|
||||
occurrences := make([]dnd.ItemOccurrence, count)
|
||||
for index := range occurrences {
|
||||
|
||||
@@ -50,17 +50,17 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[itemIndex] = citedText
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for itemIndex, item := range req.Value.Items {
|
||||
if shared.ContainsTokenSequence(citedTexts[itemIndex], item.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("items[%d]", itemIndex), ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("Item %s was not found in cited source text", diagnostics.Quote(item.Name)),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
|
||||
@@ -16,17 +16,17 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
||||
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "They recover the star compass."}, {ID: 2, Text: "Unrelated text."}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Source: doc, Value: value})
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
|
||||
t.Fatalf("cited match = %#v, %v; want approval without warnings", result, err)
|
||||
t.Fatalf("cited match = %#v, %v; want approval without advisories", result, err)
|
||||
}
|
||||
value.Items[0].Name = "Glossary Relic"
|
||||
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})
|
||||
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", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorBoundsWarningsAndRegisters(t *testing.T) {
|
||||
func TestValidatorBoundsAdvisoriesAndRegisters(t *testing.T) {
|
||||
items := make([]dnd.Item, contracts.MaxDiagnosticSamples+2)
|
||||
for index := range items {
|
||||
items[index] = dnd.Item{ID: "item", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||
|
||||
@@ -50,14 +50,14 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[index] = citedText
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
||||
continue
|
||||
}
|
||||
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))})
|
||||
findings = append(findings, 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(findings)
|
||||
}
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
return pipeline.ValidatorSpec{Key: Key, ExecutionClass: contracts.ExecutionClassDeterministic}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestValidatorUsesOnlyCitedTranscriptEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorDefersUnreadableEvidenceAndBoundsWarnings(t *testing.T) {
|
||||
func TestValidatorDefersUnreadableEvidenceAndBoundsAdvisories(t *testing.T) {
|
||||
invalid := occurrenceList("Missing")
|
||||
invalid.Occurrences[0].SourceRefs[0].StartUnitID = 99
|
||||
invalid.Occurrences[0].SourceRefs[0].EndUnitID = 99
|
||||
|
||||
@@ -50,17 +50,17 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[locationIndex] = citedText
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for locationIndex, location := range req.Value.Locations {
|
||||
if shared.ContainsTokenSequence(citedTexts[locationIndex], location.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("locations[%d]", locationIndex), ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("Location %s was not found in cited source text", diagnostics.Quote(location.Name)),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
|
||||
@@ -17,14 +17,14 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
||||
doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party enters o’rin’s gate."}, {ID: 2, Kind: "message", Text: "Unrelated location."}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), request(doc, value, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
|
||||
t.Fatalf("cited match = %#v, %v; want approval without warnings", result, err)
|
||||
t.Fatalf("cited match = %#v, %v; want approval without advisories", result, err)
|
||||
}
|
||||
value.Locations[0].Name = "Glossary Keep"
|
||||
before := value
|
||||
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))
|
||||
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", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestValidatorRegisters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorBoundsWarnings(t *testing.T) {
|
||||
func TestValidatorBoundsAdvisories(t *testing.T) {
|
||||
locations := make([]dnd.Location, contracts.MaxDiagnosticSamples+2)
|
||||
for index := range locations {
|
||||
locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||
|
||||
@@ -50,18 +50,18 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[index] = citedText
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
if shared.ContainsTokenSequence(citedTexts[index], occurrence.Name) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("occurrences[%d]", index),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("NPC occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestValidatorDefersMalformedShapeAndInvalidRanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorBoundsWarnings(t *testing.T) {
|
||||
func TestValidatorBoundsAdvisories(t *testing.T) {
|
||||
count := contracts.MaxDiagnosticSamples + 5
|
||||
occurrences := make([]dnd.NPCOccurrence, count)
|
||||
for index := range occurrences {
|
||||
|
||||
@@ -49,18 +49,18 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[npcIndex] = citedText
|
||||
}
|
||||
var warnings []diagnostics.Finding
|
||||
var findings []diagnostics.Finding
|
||||
for npcIndex, npc := range req.Value.NPCs {
|
||||
if npcAppearsInCitedText(citedTexts[npcIndex], npc) {
|
||||
continue
|
||||
}
|
||||
warnings = append(warnings, diagnostics.Finding{
|
||||
findings = append(findings, diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("npcs[%d]", npcIndex),
|
||||
ReasonCode: ReasonCode,
|
||||
Message: fmt.Sprintf("NPC %s was not found in cited source text", diagnostics.Quote(npc.Name)),
|
||||
})
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func npcAppearsInCitedText(citedText string, npc dnd.NPC) bool {
|
||||
|
||||
@@ -51,7 +51,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
if err != nil {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
warnings := make([]diagnostics.Finding, 0)
|
||||
findings := make([]diagnostics.Finding, 0)
|
||||
for index, scene := range req.Value.Scenes {
|
||||
citedText, err := resolver.CitedText([]source.SourceRef{scene.SourceRef})
|
||||
if err != nil {
|
||||
@@ -59,13 +59,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTokens := tokenSet(citedText)
|
||||
if !hasGroundedToken(citedTokens, scene.Title) {
|
||||
warnings = append(warnings, warning(index, "title"))
|
||||
findings = append(findings, finding(index, "title"))
|
||||
}
|
||||
if !hasGroundedToken(citedTokens, scene.Summary) {
|
||||
warnings = append(warnings, warning(index, "summary"))
|
||||
findings = append(findings, finding(index, "summary"))
|
||||
}
|
||||
}
|
||||
return diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
|
||||
func tokenSet(value string) map[string]struct{} {
|
||||
@@ -98,7 +98,7 @@ func significant(token string) bool {
|
||||
return !ignored
|
||||
}
|
||||
|
||||
func warning(index int, field string) diagnostics.Finding {
|
||||
func finding(index int, field string) diagnostics.Finding {
|
||||
return diagnostics.Finding{
|
||||
Scope: fmt.Sprintf("scenes[%d].%s", index, field),
|
||||
ReasonCode: ReasonCode,
|
||||
|
||||
@@ -49,13 +49,13 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}
|
||||
citedTexts[spellIndex] = citedText
|
||||
}
|
||||
var warnings []diagnostics.Finding
|
||||
var findings []diagnostics.Finding
|
||||
for spellIndex, spell := range req.Value.SpellCasts {
|
||||
if !spellAppearsInCitedText(citedTexts[spellIndex], spell) {
|
||||
warnings = append(warnings, diagnostics.Finding{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)))})
|
||||
findings = append(findings, diagnostics.Finding{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 diagnostics.DataQualityResult(warnings)
|
||||
return diagnostics.DataQualityResult(findings)
|
||||
}
|
||||
func spellAppearsInCitedText(citedText string, spell dnd.SpellCast) bool {
|
||||
name := strings.TrimSpace(spell.Spell)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
)
|
||||
|
||||
func TestValidatorApprovesWithoutWarningWhenSpellAppearsInCitedText(t *testing.T) {
|
||||
func TestValidatorApprovesWithoutAdvisoryWhenSpellAppearsInCitedText(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), requestWithSpell(validDocument(), "Cure Wounds", 2))
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v, want nil", err)
|
||||
@@ -99,7 +99,7 @@ func TestValidatorIgnoresInvalidCitations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorApprovesEmptySpellListWithoutWarning(t *testing.T) {
|
||||
func TestValidatorApprovesEmptySpellListWithoutAdvisory(t *testing.T) {
|
||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{Source: validDocument(), Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{}}})
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v, want nil", err)
|
||||
|
||||
Reference in New Issue
Block a user