From e15007fffb556e817de4ad0286c46787fb9530af Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 3 Aug 2026 22:54:11 +0000 Subject: [PATCH] Simplify enemy event grounding ownership --- .../dnd/extract/enemyevents/grounding.go | 31 ++++++---------- .../dnd/extract/enemyevents/grounding_test.go | 37 +++++++++++++++---- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/internal/modules/dnd/extract/enemyevents/grounding.go b/internal/modules/dnd/extract/enemyevents/grounding.go index 247da7c..dcdac9a 100644 --- a/internal/modules/dnd/extract/enemyevents/grounding.go +++ b/internal/modules/dnd/extract/enemyevents/grounding.go @@ -16,6 +16,8 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns" interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" + npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs" + scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" sceneregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/scenedescriptions/registry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" @@ -27,6 +29,7 @@ const ( CombatTurnReferenceSlot = "combat_turns" NPCInteractionReferenceSlot = "npc_interactions" ReferenceMaxBytes = 1048576 + promptProjectionMediaType = "application/json" ) var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ @@ -43,7 +46,7 @@ func referenceSlots() []contracts.ReferenceSlot { Name: NPCRegistryReferenceSlot, Description: "Required normalized NPC registry used only for enemy-subject grounding, never as event evidence.", Required: true, - AcceptedMediaTypes: []string{combatturncodec.MediaType}, + AcceptedMediaTypes: []string{npccodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind}, MaxBytes: ReferenceMaxBytes, }, @@ -51,7 +54,7 @@ func referenceSlots() []contracts.ReferenceSlot { Name: SceneDescriptionReferenceSlot, Description: "Required scene descriptions used only to determine exact combat eligibility, never as event evidence.", Required: true, - AcceptedMediaTypes: []string{combatturncodec.MediaType}, + AcceptedMediaTypes: []string{scenecodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.SceneDescriptionListKind}, MaxBytes: ReferenceMaxBytes, }, @@ -88,10 +91,9 @@ type groundingResolver struct { } type grounding struct { - npcInput contracts.LLMInputMaterial - combatTurnInput contracts.LLMInputMaterial - npcInteractionInput contracts.LLMInputMaterial - sceneEligibilityView *sceneregistry.Registry + npcInput contracts.LLMInputMaterial + combatTurnInput contracts.LLMInputMaterial + npcInteractionInput contracts.LLMInputMaterial } func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver, error) { @@ -130,10 +132,6 @@ func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (groundin if !npcs.Bound() { return grounding{}, fmt.Errorf("NPC registry reference is required") } - scenes, err := r.resolveScenes(references) - if err != nil { - return grounding{}, err - } combatTurns, err := resolveInput(references, CombatTurnReferenceSlot, r.combatTurns, prepareCombatTurnInput) if err != nil { return grounding{}, err @@ -143,10 +141,9 @@ func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (groundin return grounding{}, err } return grounding{ - npcInput: npcs.PromptInput(), - combatTurnInput: combatTurns, - npcInteractionInput: npcInteractions, - sceneEligibilityView: scenes, + npcInput: npcs.PromptInput(), + combatTurnInput: combatTurns, + npcInteractionInput: npcInteractions, }, nil } @@ -197,10 +194,6 @@ func (g grounding) PromptInputs() contracts.LLMInputSet { } } -func (g grounding) SceneMatch(chunk *source.Chunk) sceneregistry.ChunkMatch { - return g.sceneEligibilityView.Match(chunk) -} - func prepareCombatTurnInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) { item, ok, err := referenceItem(references, CombatTurnReferenceSlot, combatturncodec.MediaType) if err != nil || !ok { @@ -295,6 +288,6 @@ func projectNPCInteractions(interactions []dnd.NPCInteraction) []npcInteractionP func newPromptInput(name string, content []byte) *contracts.LLMInputMaterial { sum := sha256.Sum256(content) - material := contracts.NewLLMInputMaterial(name, combatturncodec.MediaType, content, "sha256:"+hex.EncodeToString(sum[:]), "") + material := contracts.NewLLMInputMaterial(name, promptProjectionMediaType, content, "sha256:"+hex.EncodeToString(sum[:]), "") return &material } diff --git a/internal/modules/dnd/extract/enemyevents/grounding_test.go b/internal/modules/dnd/extract/enemyevents/grounding_test.go index 53b5473..b240e7e 100644 --- a/internal/modules/dnd/extract/enemyevents/grounding_test.go +++ b/internal/modules/dnd/extract/enemyevents/grounding_test.go @@ -104,15 +104,27 @@ func TestGroundingResolvesGeneratedReferencesAndSceneEligibility(t *testing.T) { if got := string(resolved.PromptInputs()[NPCRegistryReferenceSlot].Content); got != `{"npcs":[{"name":"Grimjaw"}]}` { t.Fatalf("generated NPC projection = %s", got) } - if resolved.SceneMatch(combatChunk()) != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindNarrative}) { - t.Fatalf("generated scene match = %#v", resolved.SceneMatch(combatChunk())) + match, err := resolver.SceneMatch(generated, combatChunk()) + if err != nil { + t.Fatal(err) } - if resolved.SceneMatch(&source.Chunk{ID: "other", Ref: combatChunk().Ref}).State != sceneregistry.MatchMissing { + if match != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindNarrative}) { + t.Fatalf("generated scene match = %#v", match) + } + match, err = resolver.SceneMatch(generated, &source.Chunk{ID: "other", Ref: combatChunk().Ref}) + if err != nil { + t.Fatal(err) + } + if match.State != sceneregistry.MatchMissing { t.Fatal("missing scene was not reported") } mismatched := combatChunk() mismatched.Ref.EndUnitID++ - if resolved.SceneMatch(mismatched).State != sceneregistry.MatchMismatched { + match, err = resolver.SceneMatch(generated, mismatched) + if err != nil { + t.Fatal(err) + } + if match.State != sceneregistry.MatchMismatched { t.Fatal("mismatched scene was not reported") } @@ -123,8 +135,12 @@ func TestGroundingResolvesGeneratedReferencesAndSceneEligibility(t *testing.T) { if got := string(static.PromptInputs()[NPCRegistryReferenceSlot].Content); got != `{"npcs":[{"name":"Ashfang"}]}` { t.Fatalf("static NPC projection changed after generated resolution: %s", got) } - if static.SceneMatch(combatChunk()) != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindCombat}) { - t.Fatalf("static scene match = %#v", static.SceneMatch(combatChunk())) + match, err = resolver.SceneMatch(contracts.ReferenceSet{}, combatChunk()) + if err != nil { + t.Fatal(err) + } + if match != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindCombat}) { + t.Fatalf("static scene match = %#v", match) } } @@ -144,8 +160,13 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := resolver.Resolve(contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), test.want) { - t.Fatalf("Resolve() error = %v, want missing %q reference", err, test.slot) + if test.slot == SceneDescriptionReferenceSlot { + _, err = resolver.SceneMatch(contracts.ReferenceSet{}, combatChunk()) + } else { + _, err = resolver.Resolve(contracts.ReferenceSet{}) + } + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("grounding error = %v, want missing %q reference", err, test.slot) } }) }