Minimize D&D spell extraction contracts

This commit is contained in:
2026-07-22 19:01:58 +00:00
parent a263a0840c
commit 2cbaf20e55
31 changed files with 169 additions and 283 deletions

View File

@@ -13,8 +13,6 @@
"required": [
"caster",
"spell",
"effect",
"narrative_description",
"source_refs"
],
"properties": {
@@ -26,14 +24,6 @@
"type": "string",
"minLength": 1
},
"effect": {
"type": "string",
"minLength": 1
},
"narrative_description": {
"type": "string",
"minLength": 1
},
"source_refs": {
"type": "array",
"minItems": 1,

View File

@@ -99,12 +99,6 @@ func validate(value dnd.SpellList) error {
if strings.TrimSpace(spell.Spell) == "" {
return fmt.Errorf("spell_casts[%d].spell must not be empty", index)
}
if strings.TrimSpace(spell.Effect) == "" {
return fmt.Errorf("spell_casts[%d].effect must not be empty", index)
}
if strings.TrimSpace(spell.NarrativeDescription) == "" {
return fmt.Errorf("spell_casts[%d].narrative_description must not be empty", index)
}
if len(spell.SourceRefs) == 0 {
return fmt.Errorf("spell_casts[%d].source_refs must not be empty", index)
}

View File

@@ -27,8 +27,8 @@ func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
t.Fatalf("Decode() error = %v, want nil", err)
}
want := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Caster: "Aria", Spell: "Cure Wounds", Effect: "Heals an injured ally.", NarrativeDescription: "Aria restores the fighter after the fight.", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}}},
{Caster: "Borin", Spell: "Fire Bolt", Effect: "Scorches the wight.", NarrativeDescription: "Borin hurls fire at the wight.", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 3}}},
{Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}}},
{Caster: "Borin", Spell: "Fire Bolt", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 3}}},
}}
if !reflect.DeepEqual(value, want) {
t.Fatalf("Decode() = %#v, want %#v", value, want)
@@ -82,9 +82,10 @@ func TestCodecStrictlyRejectsInvalidRepresentations(t *testing.T) {
want string
}{
{name: "unknown", raw: `{"spell_casts":[],"unexpected":true}`, want: "unknown field"},
{name: "unknown record field", raw: `{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[],"unexpected":true}]}`, want: "unknown field"},
{name: "trailing", raw: `{"spell_casts":[]} {}`, want: "multiple JSON values"},
{name: "missing", raw: `{}`, want: "spell_casts must be present"},
{name: "invalid evidence", raw: `{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals","narrative_description":"Aria heals","source_refs":[{"source_id":"session","start_unit_id":0,"end_unit_id":1}]}]}`, want: "start_unit_id"},
{name: "invalid evidence", raw: `{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[{"source_id":"session","start_unit_id":0,"end_unit_id":1}]}]}`, want: "start_unit_id"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View File

@@ -3,8 +3,6 @@
{
"caster": "Aria",
"spell": "Cure Wounds",
"effect": "Heals an injured ally.",
"narrative_description": "Aria restores the fighter after the fight.",
"source_refs": [
{
"source_id": "session-alpha",
@@ -16,8 +14,6 @@
{
"caster": "Borin",
"spell": "Fire Bolt",
"effect": "Scorches the wight.",
"narrative_description": "Borin hurls fire at the wight.",
"source_refs": [
{
"source_id": "session-alpha",

View File

@@ -33,8 +33,6 @@ messages:
content_file: ./sharedassets/common-dnd-references.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-immediate-resolution.md
- role: user
content_file: ./sharedassets/common-dnd-npcs.md
cache_control:

View File

@@ -1,14 +1,8 @@
For each spell cast, source references must collectively support the caster,
spell, effect, and narrative_description. If a detail is not supported by the
cited transcript units, omit that detail or describe only the supported attempt
or declaration.
For each spell cast, source references must collectively support the in-world
caster, spell name, and the fact that the cast or declared attempt occurred.
For spells, do not follow summoned creatures, persistent effects, or other
downstream consequences through the rest of the scene.
Return only D&D spell-cast artifacts. For each spell cast, identify the
in-world caster, spell name, effect, narrative description, and source
references.
Return only D&D spell-cast artifacts. For each record, identify the in-world
caster, canonical spell name, and source references.
Use the player and party references together with transcript context to map
first-person player speech to the associated player character and use the
@@ -20,10 +14,3 @@ transcript; do not invent a name.
Use the canonical spell-name catalog to select spell names. Do not return a
spell name absent from that catalog, even when it is suggested by general D&D
knowledge or reference material.
Effects and narrative descriptions are session records, not rules summaries.
Report only mechanics, explanations, and outcomes established by the cited
transcript units. Preserve the table's observed resolution without silently
correcting it from general D&D knowledge. If the transcript gives a possibly
nonstandard rationale, use wording such as "the GM rules" or "the table
resolves" rather than asserting that rationale as a universal rule.

View File

@@ -1,15 +1,9 @@
Extract Dungeons & Dragons spell-cast artifacts from the provided transcript.
Do not infer a spell cast from general D&D knowledge or from table chatter that
does not identify a spell being cast.
Describe the session as it was played and adjudicated. The transcript is
authoritative for what happened in this session, even when a table ruling may
differ from published D&D rules. Do not correct the transcript or fill in
unstated mechanics from general D&D knowledge. When a ruling or mechanical
explanation matters, attribute it to the GM or table instead of presenting it
as a universal game rule.
Include an actual casting event or an unambiguous declared casting attempt.
Exclude spell mentions, hypothetical plans, rules discussion, and catalog
matches that do not establish a casting event in the transcript.
Use the provided canonical spell-name catalog when naming each extracted spell.
Return the canonical catalog spelling exactly. The catalog is a recognition
aid; it does not establish that a spell was cast or how the spell works.
aid and never evidence that a spell was cast.

View File

@@ -13,8 +13,6 @@
"required": [
"caster",
"spell",
"effect",
"narrative_description",
"source_refs"
],
"properties": {
@@ -26,17 +24,9 @@
"type": "string",
"description": "Canonical spell name from the provided spell-name catalog."
},
"effect": {
"type": "string",
"description": "Concise immediate effect or resolution established by the cited transcript units; do not infer mechanics from general D&D rules knowledge or follow persistent downstream consequences."
},
"narrative_description": {
"type": "string",
"description": "Short session-grounded description of the casting declaration and immediate resolution, containing only details established by the cited transcript units."
},
"source_refs": {
"type": "array",
"description": "Transcript ranges offered as evidence for factual claims about the casting declaration and immediate resolution in this spell-cast object.",
"description": "Transcript ranges offered as evidence for the caster, spell name, and casting event in this spell-cast object.",
"items": {
"type": "object",
"additionalProperties": false,

View File

@@ -88,11 +88,9 @@ func canonicalSpellList(response extractionResponse, sourceID string) dnd.SpellL
}
}
spellCasts[index] = dnd.SpellCast{
Caster: spell.Caster,
Spell: spell.Spell,
Effect: spell.Effect,
NarrativeDescription: spell.NarrativeDescription,
SourceRefs: refs,
Caster: spell.Caster,
Spell: spell.Spell,
SourceRefs: refs,
}
}
if response.SpellCasts == nil {

View File

@@ -19,11 +19,9 @@ import (
func TestExtractReturnsCanonicalSpellListFromPrivateResponse(t *testing.T) {
client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{
{
Caster: " Aria ",
Spell: " Cure Wounds ",
Effect: " Heals an injured ally. ",
NarrativeDescription: " Aria restores the fighter after the fight. ",
SourceRefs: responseSourceRefs(1, 2),
Caster: " Aria ",
Spell: " Cure Wounds ",
SourceRefs: responseSourceRefs(1, 2),
},
}}}
req := extractionRequest()
@@ -34,11 +32,9 @@ func TestExtractReturnsCanonicalSpellListFromPrivateResponse(t *testing.T) {
}
want := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{
Caster: " Aria ",
Spell: " Cure Wounds ",
Effect: " Heals an injured ally. ",
NarrativeDescription: " Aria restores the fighter after the fight. ",
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}},
Caster: " Aria ",
Spell: " Cure Wounds ",
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}},
},
}}
if !reflect.DeepEqual(result.Value, want) {
@@ -268,9 +264,9 @@ func TestExtractRejectsInvalidRequests(t *testing.T) {
func TestExtractOrdersAndDeduplicatesEvidence(t *testing.T) {
client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{
{Caster: "Borin", Spell: "Fire Bolt", Effect: "Burns.", NarrativeDescription: "Second.", SourceRefs: responseSourceRefs(2, 2)},
{Caster: "Aria", Spell: "Cure Wounds", Effect: "Heals.", NarrativeDescription: "First.", SourceRefs: []spellSourceRefResponse{{StartUnitID: 1, EndUnitID: 2}, {StartUnitID: 1, EndUnitID: 2}}},
{Caster: "Narrator", Spell: "Unknown", Effect: "Unknown.", NarrativeDescription: "Uncited."},
{Caster: "Borin", Spell: "Fire Bolt", SourceRefs: responseSourceRefs(2, 2)},
{Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []spellSourceRefResponse{{StartUnitID: 1, EndUnitID: 2}, {StartUnitID: 1, EndUnitID: 2}}},
{Caster: "Narrator", Spell: "Unknown"},
}}}
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
if err != nil {
@@ -286,7 +282,7 @@ func TestExtractOrdersAndDeduplicatesEvidence(t *testing.T) {
func TestExtractPreservesInvalidEvidenceForValidators(t *testing.T) {
client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{{
Caster: "Aria", Spell: "Cure Wounds", Effect: "Heals.", NarrativeDescription: "Aria heals.",
Caster: "Aria", Spell: "Cure Wounds",
SourceRefs: []spellSourceRefResponse{{StartUnitID: 99, EndUnitID: 0}},
}}}}
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
@@ -300,13 +296,13 @@ func TestExtractPreservesInvalidEvidenceForValidators(t *testing.T) {
}
func TestExtractMapsRawSemanticCandidatesWithoutRepair(t *testing.T) {
client := &fakeSpellsLLMClient{content: []byte(`{"spell_casts":[{"caster":"","spell":"Cure Wounds","effect":"","narrative_description":"","source_refs":[{"start_unit_id":0,"end_unit_id":-1}]}]}`)}
client := &fakeSpellsLLMClient{content: []byte(`{"spell_casts":[{"caster":"","spell":"Cure Wounds","source_refs":[{"start_unit_id":0,"end_unit_id":-1}]}]}`)}
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
if err != nil {
t.Fatalf("Extract() error = %v, want nil", err)
}
spell := result.Value.SpellCasts[0]
if spell.Caster != "" || spell.Effect != "" || spell.NarrativeDescription != "" {
if spell.Caster != "" {
t.Fatalf("spell = %#v, want blank semantic values preserved", spell)
}
if refs := spell.SourceRefs; len(refs) != 1 || refs[0] != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 0, EndUnitID: -1}) {

View File

@@ -5,11 +5,9 @@ type extractionResponse struct {
}
type spellCastResponse struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []spellSourceRefResponse `json:"source_refs"`
Caster string `json:"caster"`
Spell string `json:"spell"`
SourceRefs []spellSourceRefResponse `json:"source_refs"`
}
type spellSourceRefResponse struct {

View File

@@ -37,14 +37,14 @@ func TestLoadResponseSchemaUsesExtractorOwnedLLMSchema(t *testing.T) {
{
name: "semantic blanks and empty evidence",
response: map[string]any{"spell_casts": []any{map[string]any{
"caster": "", "spell": "", "effect": "", "narrative_description": "", "source_refs": []any{},
"caster": "", "spell": "", "source_refs": []any{},
}}},
valid: true,
},
{
name: "nonpositive unit candidates",
response: map[string]any{"spell_casts": []any{map[string]any{
"caster": "Aria", "spell": "Cure Wounds", "effect": "Heals.", "narrative_description": "Aria heals.",
"caster": "Aria", "spell": "Cure Wounds",
"source_refs": []any{map[string]any{"start_unit_id": 0, "end_unit_id": -1}},
}}},
valid: true,
@@ -52,25 +52,25 @@ func TestLoadResponseSchemaUsesExtractorOwnedLLMSchema(t *testing.T) {
{
name: "missing required field",
response: map[string]any{"spell_casts": []any{map[string]any{
"spell": "Cure Wounds", "effect": "Heals.", "narrative_description": "Aria heals.", "source_refs": []any{},
"spell": "Cure Wounds", "source_refs": []any{},
}}},
},
{
name: "unknown field",
response: map[string]any{"spell_casts": []any{map[string]any{
"caster": "Aria", "spell": "Cure Wounds", "effect": "Heals.", "narrative_description": "Aria heals.", "source_refs": []any{}, "id": "assigned later",
"caster": "Aria", "spell": "Cure Wounds", "source_refs": []any{}, "id": "assigned later",
}}},
},
{
name: "wrong field type",
response: map[string]any{"spell_casts": []any{map[string]any{
"caster": 7, "spell": "Cure Wounds", "effect": "Heals.", "narrative_description": "Aria heals.", "source_refs": []any{},
"caster": 7, "spell": "Cure Wounds", "source_refs": []any{},
}}},
},
{
name: "noninteger source identifier",
response: map[string]any{"spell_casts": []any{map[string]any{
"caster": "Aria", "spell": "Cure Wounds", "effect": "Heals.", "narrative_description": "Aria heals.",
"caster": "Aria", "spell": "Cure Wounds",
"source_refs": []any{map[string]any{"start_unit_id": 1.5, "end_unit_id": 2}},
}}},
},
@@ -131,10 +131,8 @@ func validSpellsResponse() map[string]any {
return map[string]any{
"spell_casts": []any{
map[string]any{
"caster": "Aria",
"spell": "Cure Wounds",
"effect": "The wounds close.",
"narrative_description": "Aria casts Cure Wounds.",
"caster": "Aria",
"spell": "Cure Wounds",
"source_refs": []any{
map[string]any{"start_unit_id": 1, "end_unit_id": 2},
},

View File

@@ -25,7 +25,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
"common-dnd-identity.md",
"common-dnd-transcript.md",
"common-dnd-references.md",
"common-dnd-immediate-resolution.md",
"common-dnd-npcs.md",
},
}

View File

@@ -266,12 +266,12 @@ func TestNormalizeCollapsesDuplicateGroupsAfterCanonicalization(t *testing.T) {
firstEvidence := source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 2}
secondEvidence := source.SourceRef{SourceID: "source", StartUnitID: 3, EndUnitID: 4}
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Caster: " Aria \t", Spell: " cure wounds ", Effect: "first effect", NarrativeDescription: "first narrative", SourceRefs: []source.SourceRef{secondEvidence, firstEvidence, firstEvidence}},
{Caster: "Borin", Spell: "Healing Word", Effect: "distinct effect", NarrativeDescription: "distinct narrative", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " yle ", Spell: "Cure Wounds", Effect: "kept effect", NarrativeDescription: "kept narrative", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: "aria", Spell: " cure wounds ", Effect: "removed effect", NarrativeDescription: "removed narrative", SourceRefs: []source.SourceRef{firstEvidence, secondEvidence}},
{Caster: "KYLE", Spell: " cure wounds ", Effect: "removed effect two", NarrativeDescription: "removed narrative two", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " kyle ", Spell: "Cure Wounds", Effect: "removed effect three", NarrativeDescription: "removed narrative three", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " Aria \t", Spell: " cure wounds ", SourceRefs: []source.SourceRef{secondEvidence, firstEvidence, firstEvidence}},
{Caster: "Borin", Spell: "Healing Word", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " yle ", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: "aria", Spell: " cure wounds ", SourceRefs: []source.SourceRef{firstEvidence, secondEvidence}},
{Caster: "KYLE", Spell: " cure wounds ", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " kyle ", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{firstEvidence}},
}}
result, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
@@ -281,8 +281,8 @@ func TestNormalizeCollapsesDuplicateGroupsAfterCanonicalization(t *testing.T) {
if len(result.Value.SpellCasts) != 3 {
t.Fatalf("normalized casts = %#v, want first occurrences plus distinct cast", result.Value.SpellCasts)
}
if got := result.Value.SpellCasts[0]; got.Caster != " Aria \t" || got.Effect != "first effect" || got.NarrativeDescription != "first narrative" {
t.Fatalf("retained first cast = %#v, want first occurrence fields unchanged", got)
if got := result.Value.SpellCasts[0]; got.Caster != " Aria \t" {
t.Fatalf("retained first cast = %#v, want first occurrence caster unchanged", got)
}
if got := result.Value.SpellCasts[0].SourceRefs; !reflect.DeepEqual(got, []source.SourceRef{firstEvidence, secondEvidence}) {
t.Fatalf("retained first evidence = %#v, want canonical first evidence only", got)
@@ -393,11 +393,9 @@ func TestNormalizeBoundsDuplicateWarningIndices(t *testing.T) {
func TestNormalizeIsIdempotentForAlreadyNormalizedInput(t *testing.T) {
doc := sourceDocument(3)
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{{
Spell: "Cure Wounds",
Caster: "Aria",
Effect: "effect",
NarrativeDescription: "narrative",
SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 1, EndUnitID: 2}},
Spell: "Cure Wounds",
Caster: "Aria",
SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 1, EndUnitID: 2}},
}}}
normalizer := newNormalizer(t)
first, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))

View File

@@ -7,8 +7,6 @@
{
"caster": " Aria ",
"spell": " cure wounds ",
"effect": "first effect",
"narrative_description": "first narrative",
"source_refs": [
{"source_id": "source", "start_unit_id": 2, "end_unit_id": 2},
{"source_id": "source", "start_unit_id": 1, "end_unit_id": 1},
@@ -18,8 +16,6 @@
{
"caster": "aria",
"spell": "Cure Wounds",
"effect": "duplicate effect",
"narrative_description": "duplicate narrative",
"source_refs": [
{"source_id": "source", "start_unit_id": 1, "end_unit_id": 1},
{"source_id": "source", "start_unit_id": 2, "end_unit_id": 2}
@@ -28,8 +24,6 @@
{
"caster": "aria",
"spell": "Cure Wounds",
"effect": "distinct effect",
"narrative_description": "distinct narrative",
"source_refs": [
{"source_id": "source", "start_unit_id": 2, "end_unit_id": 2}
]
@@ -41,8 +35,6 @@
{
"caster": " Aria ",
"spell": "Cure Wounds",
"effect": "first effect",
"narrative_description": "first narrative",
"source_refs": [
{"source_id": "source", "start_unit_id": 1, "end_unit_id": 1},
{"source_id": "source", "start_unit_id": 2, "end_unit_id": 2}
@@ -51,8 +43,6 @@
{
"caster": "aria",
"spell": "Cure Wounds",
"effect": "distinct effect",
"narrative_description": "distinct narrative",
"source_refs": [
{"source_id": "source", "start_unit_id": 2, "end_unit_id": 2}
]
@@ -72,8 +62,6 @@
{
"caster": "Aria",
"spell": "Cure Wounds",
"effect": "heals an ally",
"narrative_description": "Aria restores an ally's wounds.",
"source_refs": [
{"source_id": "source", "start_unit_id": 1, "end_unit_id": 1}
]
@@ -85,8 +73,6 @@
{
"caster": "Aria",
"spell": "Cure Wounds",
"effect": "heals an ally",
"narrative_description": "Aria restores an ally's wounds.",
"source_refs": [
{"source_id": "source", "start_unit_id": 1, "end_unit_id": 1}
]

View File

@@ -17,11 +17,9 @@ type SpellList struct {
}
type SpellCast struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []source.SourceRef `json:"source_refs"`
Caster string `json:"caster"`
Spell string `json:"spell"`
SourceRefs []source.SourceRef `json:"source_refs"`
}
type NPCList struct {

View File

@@ -201,11 +201,9 @@ func spellList(names ...string) dnd.SpellList {
func validCast(name string) dnd.SpellCast {
return dnd.SpellCast{
Caster: "Aria",
Spell: name,
Effect: "heals an ally",
NarrativeDescription: "Aria restores Borin.",
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
Caster: "Aria",
Spell: name,
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
}
}

View File

@@ -48,12 +48,6 @@ func Validate(value dnd.SpellList) error {
if strings.TrimSpace(spell.Spell) == "" {
return fmt.Errorf("spell_casts[%d].spell must not be empty", index)
}
if strings.TrimSpace(spell.Effect) == "" {
return fmt.Errorf("spell_casts[%d].effect must not be empty", index)
}
if strings.TrimSpace(spell.NarrativeDescription) == "" {
return fmt.Errorf("spell_casts[%d].narrative_description must not be empty", index)
}
if len(spell.SourceRefs) == 0 {
return fmt.Errorf("spell_casts[%d].source_refs must not be empty", index)
}

View File

@@ -34,17 +34,23 @@ func TestValidatorRejectsMissingSpellList(t *testing.T) {
}
func TestValidatorRejectsMissingRequiredSpellFields(t *testing.T) {
value := validSpellList()
value.SpellCasts[0].Spell = ""
result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
tests := []struct {
name string
mutate func(*dnd.SpellCast)
}{
{name: "blank caster", mutate: func(cast *dnd.SpellCast) { cast.Caster = " " }},
{name: "blank spell", mutate: func(cast *dnd.SpellCast) { cast.Spell = "" }},
{name: "empty evidence", mutate: func(cast *dnd.SpellCast) { cast.SourceRefs = nil }},
}
if result.Approved {
t.Fatalf("Approved = true, want false")
}
if result.ReasonCode != ReasonCode {
t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCode)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
value := validSpellList()
test.mutate(&value.SpellCasts[0])
result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
if err != nil || result.Approved || result.ReasonCode != ReasonCode {
t.Fatalf("Validate() = %#v, %v; want shape rejection", result, err)
}
})
}
}
@@ -69,5 +75,5 @@ func requestWithValue(value dnd.SpellList) contracts.TypedValidationRequest[dnd.
}
func validSpellList() dnd.SpellList {
return dnd.SpellList{SpellCasts: []dnd.SpellCast{{Caster: "Aria", Spell: "Cure Wounds", Effect: "heals", NarrativeDescription: "Aria heals Borin.", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
return dnd.SpellList{SpellCasts: []dnd.SpellCast{{Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
}

View File

@@ -90,7 +90,7 @@ func TestValidatorSpecCheckpointAndRegistration(t *testing.T) {
func requestWithValue(doc *source.SourceDocument, ref source.SourceRef) contracts.TypedValidationRequest[dnd.SpellList] {
return contracts.TypedValidationRequest[dnd.SpellList]{Source: doc, Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{
Caster: "Aria", Spell: "Cure Wounds", Effect: "heals", NarrativeDescription: "Aria casts Cure Wounds.", SourceRefs: []source.SourceRef{ref},
Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{ref},
}}}}
}

View File

@@ -41,7 +41,7 @@ func TestValidatorWarnsWhenSpellDoesNotAppearInCitedText(t *testing.T) {
func TestValidatorMatchesCaseInsensitiveUnicodeMultiwordSpellAcrossCitations(t *testing.T) {
value := dnd.SpellList{SpellCasts: []dnd.SpellCast{{
Caster: "Aria", Spell: "Tasha's Hideous Laughter", Effect: "effect", NarrativeDescription: "description",
Caster: "Aria", Spell: "Tasha's Hideous Laughter",
SourceRefs: []source.SourceRef{
{SourceID: "session", StartUnitID: 2, EndUnitID: 2},
{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
@@ -108,7 +108,7 @@ func TestValidatorSpecCheckpointAndRegistration(t *testing.T) {
func requestWithSpell(doc *source.SourceDocument, name string, unitID int) contracts.TypedValidationRequest[dnd.SpellList] {
return contracts.TypedValidationRequest[dnd.SpellList]{Source: doc, Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{
Caster: "Aria", Spell: name, Effect: "effect", NarrativeDescription: "description",
Caster: "Aria", Spell: name,
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: unitID, EndUnitID: unitID}},
}}}}
}