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

@@ -18,7 +18,6 @@ import (
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
)
func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T) {
@@ -112,7 +111,7 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
if len(spellValue.SpellCasts) != 1 || spellValue.SpellCasts[0].Caster != "Mira Thorn" {
t.Fatalf("spell output = %#v, want one registry-grounded-context spell", spellValue)
}
assertSpellEvidence(t, spellValue.SpellCasts[0].SourceRefs)
assertCurrentEvidence(t, spellValue.SpellCasts[0].SourceRefs)
case "combat":
decoded, decodeErr := combatcodec.New().Decode(serialized.Artifact.Content)
if decodeErr != nil {
@@ -127,15 +126,6 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
assertCurrentEvidence(t, combatValue.CombatTurns[0].SourceRefs)
}
func assertSpellEvidence(t *testing.T, references []shared.SourceRefResponse) {
t.Helper()
for _, reference := range references {
if reference.SourceID != "npc-session" {
t.Fatalf("spell evidence reference = %#v, want current source only", reference)
}
}
}
func assertCurrentEvidence(t *testing.T, references []source.SourceRef) {
t.Helper()
for _, reference := range references {
@@ -184,9 +174,8 @@ func (client *groundedDNDLLMClient) CompleteStructured(ctx context.Context, requ
}}
case spells.PromptID:
payload = map[string]any{"spell_casts": []any{map[string]any{
"caster": "Mira Thorn", "spell": "Cure Wounds", "effect": "Restores an ally.",
"narrative_description": "Mira Thorn restores an ally.",
"source_refs": []any{map[string]int{"start_unit_id": 1, "end_unit_id": 1}},
"caster": "Mira Thorn", "spell": "Cure Wounds",
"source_refs": []any{map[string]int{"start_unit_id": 1, "end_unit_id": 1}},
}}}
case combatextract.PromptID:
payload = map[string]any{"combat_turns": []any{map[string]any{

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
)
type extractionResponse struct {
@@ -14,11 +13,14 @@ type extractionResponse struct {
}
type spellCastResponse struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []shared.SourceRefResponse `json:"source_refs"`
Caster string `json:"caster"`
Spell string `json:"spell"`
SourceRefs []spellSourceRefResponse `json:"source_refs"`
}
type spellSourceRefResponse struct {
StartUnitID int `json:"start_unit_id"`
EndUnitID int `json:"end_unit_id"`
}
type fakeSpellsLLMClient struct {
@@ -58,12 +60,11 @@ func (client *fakeSpellsLLMClient) CompleteStructured(_ context.Context, req con
return contracts.StructuredCompletionResponse{Content: content}, nil
}
func responseSourceRefs(sourceID string, startUnitID int, endUnitID int) []shared.SourceRefResponse {
return []shared.SourceRefResponse{
func responseSourceRefs(startUnitID int, endUnitID int) []spellSourceRefResponse {
return []spellSourceRefResponse{
{
SourceID: sourceID,
StartUnitID: shared.UnitRefFromInt(startUnitID),
EndUnitID: shared.UnitRefFromInt(endUnitID),
StartUnitID: startUnitID,
EndUnitID: endUnitID,
},
}
}

View File

@@ -2,7 +2,6 @@ package integration_test
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
@@ -11,6 +10,8 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape"
@@ -36,18 +37,14 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) {
response: extractionResponse{
SpellCasts: []spellCastResponse{
{
Caster: "Aria",
Spell: "Cure Wounds",
Effect: "Heals an injured ally.",
NarrativeDescription: "Aria restores the fighter after the fight.",
SourceRefs: responseSourceRefs(expectedDoc.ID, 1, 1),
Caster: "Aria",
Spell: "Cure Wounds",
SourceRefs: responseSourceRefs(1, 1),
},
{
Caster: "Borin",
Spell: "Fire Bolt",
Effect: "Scorches the wight.",
NarrativeDescription: "Borin hurls fire at the wight.",
SourceRefs: responseSourceRefs(expectedDoc.ID, 3, 3),
Caster: "Borin",
Spell: "Fire Bolt",
SourceRefs: responseSourceRefs(3, 3),
},
},
},
@@ -119,7 +116,6 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) {
func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) {
raw := readDNDSpellsFixture(t)
expectedDoc := parseDNDSpellsFixture(t, raw)
resolved := resolveDNDSpellsPipeline(t)
resolved.ResolvedPipeline.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet = dndSpellsReferenceSet(
"Aria: party cleric\nBorin: fighter",
@@ -129,11 +125,9 @@ func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) {
response: extractionResponse{
SpellCasts: []spellCastResponse{
{
Caster: "Borin",
Spell: "Fire Bolt",
Effect: "Scorches the wight.",
NarrativeDescription: "Borin hurls fire at the wight.",
SourceRefs: responseSourceRefs(expectedDoc.ID, 3, 3),
Caster: "Borin",
Spell: "Fire Bolt",
SourceRefs: responseSourceRefs(3, 3),
},
},
},
@@ -195,25 +189,25 @@ func TestProductionSpellPipelineRoutesSemanticCandidatesToDeterministicValidator
}{
{
name: "blank string",
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"","narrative_description":"Aria casts the spell.","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`),
response: []byte(`{"spell_casts":[{"caster":"","spell":"Cure Wounds","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`),
reasonCode: spellshape.ReasonCode,
validatorName: spellshape.Key,
},
{
name: "empty evidence",
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals.","narrative_description":"Aria casts the spell.","source_refs":[]}]}`),
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[]}]}`),
reasonCode: spellshape.ReasonCode,
validatorName: spellshape.Key,
},
{
name: "nonpositive unit candidate",
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals.","narrative_description":"Aria casts the spell.","source_refs":[{"start_unit_id":0,"end_unit_id":1}]}]}`),
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[{"start_unit_id":0,"end_unit_id":1}]}]}`),
reasonCode: spellsourcerefs.ReasonCode,
validatorName: spellsourcerefs.Key,
},
{
name: "unknown unit candidate",
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals.","narrative_description":"Aria casts the spell.","source_refs":[{"start_unit_id":99,"end_unit_id":99}]}]}`),
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[{"start_unit_id":99,"end_unit_id":99}]}]}`),
reasonCode: spellsourcerefs.ReasonCode,
validatorName: spellsourcerefs.Key,
},
@@ -320,12 +314,12 @@ func parseDNDSpellsFixture(t *testing.T, raw []byte) *source.SourceDocument {
return doc
}
func decodeRunnerSpellResponse(t *testing.T, raw []byte) extractionResponse {
func decodeRunnerSpellResponse(t *testing.T, raw []byte) dnd.SpellList {
t.Helper()
var response extractionResponse
if err := json.Unmarshal(raw, &response); err != nil {
t.Fatalf("Unmarshal(raw output) error = %v, want nil", err)
response, err := spellcodec.New().Decode(raw)
if err != nil {
t.Fatalf("decode durable spell output: %v", err)
}
return response
}