diff --git a/internal/modules/dnd/register/chains.go b/internal/modules/dnd/register/chains.go index 3628a19..eec4a97 100644 --- a/internal/modules/dnd/register/chains.go +++ b/internal/modules/dnd/register/chains.go @@ -3,12 +3,14 @@ package register import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" + enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" + enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs" @@ -18,6 +20,10 @@ import ( combatshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/shape" combatsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_refs" combatrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_relatedness" + enemyeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/invariants" + enemyeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/shape" + enemyeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_refs" + enemyeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_relatedness" itemeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/invariants" itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs" @@ -127,6 +133,33 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error { }, }) }}, + {name: "enemy events validator chain", register: func() error { + return registry.Register(pipeline.ValidatorChainMapping{ + Stage: pipeline.StageExtract, + Module: enemyeventextract.Key, + Validators: []pipeline.ModuleBinding{ + pipeline.Binding(validjson.Key), + pipeline.Binding(enemyeventshape.Key), + pipeline.Binding(enemyeventrefs.Key), + pipeline.Binding(validjsonschema.Key), + pipeline.Binding(enemyeventrelatedness.Key), + }, + }) + }}, + {name: "enemy events normalize validator chain", register: func() error { + return registry.Register(pipeline.ValidatorChainMapping{ + Stage: pipeline.StageNormalize, + Module: enemyeventnormalize.Key, + Validators: []pipeline.ModuleBinding{ + pipeline.Binding(validjson.Key), + pipeline.Binding(enemyeventshape.Key), + pipeline.Binding(enemyeventinvariants.Key), + pipeline.Binding(enemyeventrefs.Key), + pipeline.Binding(validjsonschema.Key), + pipeline.Binding(enemyeventrelatedness.Key), + }, + }) + }}, {name: "item events validator chain", register: func() error { return registry.Register(pipeline.ValidatorChainMapping{ Stage: pipeline.StageExtract, diff --git a/internal/modules/dnd/register/evidence.go b/internal/modules/dnd/register/evidence.go index 6867438..9dd6474 100644 --- a/internal/modules/dnd/register/evidence.go +++ b/internal/modules/dnd/register/evidence.go @@ -13,6 +13,9 @@ func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error { {name: "combat turns evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.CombatTurnListKind, combatTurnEvidence) }}, + {name: "enemy events evidence", register: func() error { + return pipeline.RegisterArtifactEvidence(registry, dnd.EnemyEventListKind, enemyEventEvidence) + }}, {name: "item events evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.ItemEventListKind, itemEventEvidence) }}, @@ -49,6 +52,14 @@ func combatTurnEvidence(value dnd.CombatTurnList) []source.SourceRef { return append([]source.SourceRef(nil), refs...) } +func enemyEventEvidence(value dnd.EnemyEventList) []source.SourceRef { + var refs []source.SourceRef + for _, record := range value.Events { + refs = append(refs, record.SourceRefs...) + } + return append([]source.SourceRef(nil), refs...) +} + func itemEventEvidence(value dnd.ItemEventList) []source.SourceRef { var refs []source.SourceRef for _, record := range value.Events { diff --git a/internal/modules/dnd/register/merge.go b/internal/modules/dnd/register/merge.go index e7abc0c..af475ae 100644 --- a/internal/modules/dnd/register/merge.go +++ b/internal/modules/dnd/register/merge.go @@ -70,6 +70,27 @@ func appendCombatTurnLists(values []dnd.CombatTurnList) (dnd.CombatTurnList, err return combined, nil } +func appendEnemyEventLists(values []dnd.EnemyEventList) (dnd.EnemyEventList, error) { + count := 0 + present := false + for _, value := range values { + if value.Events != nil { + present = true + } + count += len(value.Events) + } + if !present { + return dnd.EnemyEventList{}, nil + } + combined := dnd.EnemyEventList{Events: make([]dnd.EnemyEvent, 0, count)} + for _, value := range values { + for _, event := range value.Events { + combined.Events = append(combined.Events, cloneEnemyEvent(event)) + } + } + return combined, nil +} + func appendItemEventLists(values []dnd.ItemEventList) (dnd.ItemEventList, error) { count := 0 present := false @@ -137,6 +158,12 @@ func cloneCombatTurn(value dnd.CombatTurn) dnd.CombatTurn { return clone } +func cloneEnemyEvent(value dnd.EnemyEvent) dnd.EnemyEvent { + clone := value + clone.SourceRefs = cloneSourceRefs(value.SourceRefs) + return clone +} + func cloneItemEvent(value dnd.ItemEvent) dnd.ItemEvent { clone := value if value.Quantity != nil { diff --git a/internal/modules/dnd/register/modules.go b/internal/modules/dnd/register/modules.go index 960a802..47ee441 100644 --- a/internal/modules/dnd/register/modules.go +++ b/internal/modules/dnd/register/modules.go @@ -6,18 +6,21 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns" + enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents" itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents" interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs" scenedescriptioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" + enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" + enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs" @@ -33,6 +36,7 @@ func registerModules(registries pipeline.Registries) error { {name: "spells codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, codec) }}, {name: "npcs codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, npccodec.New()) }}, {name: "combat turns codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, combatcodec.New()) }}, + {name: "enemy events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, enemyeventcodec.New()) }}, {name: "item events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemeventcodec.New()) }}, {name: "npc interactions codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, interactioncodec.New()) }}, {name: "scene descriptions codec", register: func() error { @@ -42,6 +46,7 @@ func registerModules(registries pipeline.Registries) error { {name: "spells extractor", register: func() error { return spellextract.Register(registries.Extractors) }}, {name: "npcs extractor", register: func() error { return npcextract.Register(registries.Extractors) }}, {name: "combat turns extractor", register: func() error { return combatextract.Register(registries.Extractors) }}, + {name: "enemy events extractor", register: func() error { return enemyeventextract.Register(registries.Extractors) }}, {name: "item events extractor", register: func() error { return itemeventextract.Register(registries.Extractors) }}, {name: "npc interactions extractor", register: func() error { return interactionextract.Register(registries.Extractors) }}, {name: "scene descriptions extractor", register: func() error { return scenedescriptionextract.Register(registries.Extractors) }}, @@ -54,6 +59,9 @@ func registerModules(registries pipeline.Registries) error { {name: "combat-turn-list appendorder merger", register: func() error { return appendorder.RegisterTyped(registries.Mergers, dnd.CombatTurnListKind, appendCombatTurnLists) }}, + {name: "enemy-event-list appendorder merger", register: func() error { + return appendorder.RegisterTyped(registries.Mergers, dnd.EnemyEventListKind, appendEnemyEventLists) + }}, {name: "item-event-list appendorder merger", register: func() error { return appendorder.RegisterTyped(registries.Mergers, dnd.ItemEventListKind, appendItemEventLists) }}, @@ -66,6 +74,7 @@ func registerModules(registries pipeline.Registries) error { {name: "spells normalizer", register: func() error { return spellnormalize.Register(registries.Normalizers) }}, {name: "npcs normalizer", register: func() error { return npcnormalize.Register(registries.Normalizers) }}, {name: "combat turns normalizer", register: func() error { return combatnormalize.Register(registries.Normalizers) }}, + {name: "enemy events normalizer", register: func() error { return enemyeventnormalize.Register(registries.Normalizers) }}, {name: "item events normalizer", register: func() error { return itemeventnormalize.Register(registries.Normalizers) }}, {name: "npc interactions normalizer", register: func() error { return interactionnormalize.Register(registries.Normalizers) }}, {name: "scene descriptions normalizer", register: func() error { return scenedescriptionnormalize.Register(registries.Normalizers) }}, @@ -78,6 +87,9 @@ func registerModules(registries pipeline.Registries) error { {name: "combat-turn-list noop normalizer", register: func() error { return noop.RegisterTyped[dnd.CombatTurnList](registries.Normalizers, dnd.CombatTurnListKind) }}, + {name: "enemy-event-list noop normalizer", register: func() error { + return noop.RegisterTyped[dnd.EnemyEventList](registries.Normalizers, dnd.EnemyEventListKind) + }}, {name: "item-event-list noop normalizer", register: func() error { return noop.RegisterTyped[dnd.ItemEventList](registries.Normalizers, dnd.ItemEventListKind) }}, @@ -97,6 +109,7 @@ func registerPromptAssets(assets *llm.AssetRegistry) error { {name: "npcs prompt assets", register: func() error { return npcextract.RegisterPromptAssets(assets) }}, {name: "npc normalization prompt assets", register: func() error { return npcnormalize.RegisterPromptAssets(assets) }}, {name: "combat turns prompt assets", register: func() error { return combatextract.RegisterPromptAssets(assets) }}, + {name: "enemy events prompt assets", register: func() error { return enemyeventextract.RegisterPromptAssets(assets) }}, {name: "item events prompt assets", register: func() error { return itemeventextract.RegisterPromptAssets(assets) }}, {name: "npc interactions prompt assets", register: func() error { return interactionextract.RegisterPromptAssets(assets) }}, {name: "scene descriptions prompt assets", register: func() error { return scenedescriptionextract.RegisterPromptAssets(assets) }}, diff --git a/internal/modules/dnd/register/prompt_cache_test.go b/internal/modules/dnd/register/prompt_cache_test.go index 3376223..bc69b39 100644 --- a/internal/modules/dnd/register/prompt_cache_test.go +++ b/internal/modules/dnd/register/prompt_cache_test.go @@ -9,6 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" + enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs" @@ -57,6 +58,17 @@ func TestExtractionPromptsShareRenderedPrefix(t *testing.T) { }), npcInput: true, }, + { + name: "enemy events", + promptID: enemyeventextract.PromptID, + promptVersion: enemyeventextract.SchemaVersion, + inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ + "npcs": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), + "combat_turns": promptkit.Inline(`{"sentinel":"combat-turns-sentinel"}`), + "npc_interactions": promptkit.Inline(`{"sentinel":"npc-interactions-sentinel"}`), + }), + npcInput: true, + }, { name: "npc interactions", promptID: interactionextract.PromptID, diff --git a/internal/modules/dnd/register/register_test.go b/internal/modules/dnd/register/register_test.go index 82a9c8a..e633cce 100644 --- a/internal/modules/dnd/register/register_test.go +++ b/internal/modules/dnd/register/register_test.go @@ -13,12 +13,14 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" + enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" + enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs" @@ -44,6 +46,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "dnd.spells/dnd.spells.yaml", "dnd.npcs/dnd.npcs.yaml", "dnd.combat_turns/dnd.combat_turns.yaml", + "dnd.enemy_events/dnd.enemy_events.yaml", "dnd.item_events/dnd.item_events.yaml", "dnd.npc_interactions/dnd.npc_interactions.yaml", "dnd.scene_descriptions/dnd.scene_descriptions.yaml", @@ -72,14 +75,15 @@ func TestRegisterAddsDNDFamily(t *testing.T) { t.Fatalf("normalization schema asset = %v, want registered private schema", err) } assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"}) - assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, itemeventextract.Key, interactionextract.Key, scenedescriptionextract.Key}) - assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, itemeventnormalize.Key, interactionnormalize.Key, scenedescriptionnormalize.Key, pipeline.DefaultNormalizeModule}) - assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) - assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) - assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) - assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) + assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, interactionextract.Key, scenedescriptionextract.Key}) + assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, interactionnormalize.Key, scenedescriptionnormalize.Key, pipeline.DefaultNormalizeModule}) + assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) + assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) + assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) + assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind}) + assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(interactionnormalize.Key), []contracts.ArtifactKind{dnd.NPCInteractionListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(scenedescriptionnormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind}) @@ -96,6 +100,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "extract/dnd/combat-turns/source_refs", "extract/dnd/combat-turns/source_relatedness", "normalize/dnd/combat-turns/invariants", + "extract/dnd/enemy-events/shape", + "extract/dnd/enemy-events/source_refs", + "extract/dnd/enemy-events/source_relatedness", + "normalize/dnd/enemy-events/invariants", "extract/dnd/item-events/shape", "extract/dnd/item-events/source_refs", "extract/dnd/item-events/source_relatedness", @@ -168,6 +176,27 @@ func TestRegisterAddsDNDFamily(t *testing.T) { if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, combatNormalizeChain) { t.Fatalf("combat normalize validator chain = %#v, want %#v", got, combatNormalizeChain) } + enemyEventExtractChain := []pipeline.ModuleBinding{ + pipeline.Binding("generic/valid_json"), + pipeline.Binding("extract/dnd/enemy-events/shape"), + pipeline.Binding("extract/dnd/enemy-events/source_refs"), + pipeline.Binding("generic/valid_json_schema"), + pipeline.Binding("extract/dnd/enemy-events/source_relatedness"), + } + enemyEventNormalizeChain := []pipeline.ModuleBinding{ + pipeline.Binding("generic/valid_json"), + pipeline.Binding("extract/dnd/enemy-events/shape"), + pipeline.Binding("normalize/dnd/enemy-events/invariants"), + pipeline.Binding("extract/dnd/enemy-events/source_refs"), + pipeline.Binding("generic/valid_json_schema"), + pipeline.Binding("extract/dnd/enemy-events/source_relatedness"), + } + if got := registries.ValidatorChains.Validators(pipeline.StageExtract, enemyeventextract.Key); !reflect.DeepEqual(got, enemyEventExtractChain) { + t.Fatalf("enemy event extract validator chain = %#v, want %#v", got, enemyEventExtractChain) + } + if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, enemyeventnormalize.Key); !reflect.DeepEqual(got, enemyEventNormalizeChain) { + t.Fatalf("enemy event normalize validator chain = %#v, want %#v", got, enemyEventNormalizeChain) + } itemEventExtractChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), pipeline.Binding("extract/dnd/item-events/shape"), @@ -264,6 +293,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "dnd.combat_turns/sharedassets/common-dnd-system.md", "dnd.combat_turns/sharedassets/common-dnd-transcript.md", "dnd.combat_turns/task.md", + "dnd.enemy_events/dnd.enemy_events.yaml", + "dnd.enemy_events/grounding.md", + "dnd.enemy_events/instructions.md", + "dnd.enemy_events/task.md", "dnd.item_events/dnd.item_events.yaml", "dnd.item_events/instructions.md", "dnd.item_events/sharedassets/common-dnd-extraction-evidence.md", @@ -289,6 +322,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "dnd_spells_llm.v1.json", "dnd_npcs_llm.v1.json", "dnd_combat_turns_llm.v1.json", + "dnd_enemy_events_llm.v1.json", "dnd_item_events_llm.v1.json", "dnd_npc_interactions_llm.v1.json", "dnd_scene_descriptions_llm.v1.json", @@ -314,6 +348,19 @@ func TestRegisterAddsDNDFamily(t *testing.T) { if spec, ok := registries.Normalizers.Spec(combatnormalize.Key); !ok || spec.ArtifactKind != dnd.CombatTurnListKind || spec.Stage != pipeline.StageNormalize { t.Fatalf("combat normalizer spec = %#v, present = %t; want dnd combat-turn-list artifact", spec, ok) } + enemyEventExtractSpec, enemyEventExtractOK := registries.Extractors.Spec(enemyeventextract.Key) + enemyEventNormalizeSpec, enemyEventNormalizeOK := registries.Normalizers.Spec(enemyeventnormalize.Key) + if !enemyEventExtractOK || enemyEventExtractSpec.ArtifactKind != dnd.EnemyEventListKind || !enemyEventNormalizeOK || enemyEventNormalizeSpec.ArtifactKind != dnd.EnemyEventListKind || enemyEventNormalizeSpec.Stage != pipeline.StageNormalize { + t.Fatalf("enemy event specs = %#v / %#v, present = %t / %t", enemyEventExtractSpec, enemyEventNormalizeSpec, enemyEventExtractOK, enemyEventNormalizeOK) + } + if len(enemyEventExtractSpec.ReferenceSlots) != 8 || len(enemyEventNormalizeSpec.ReferenceSlots) != 1 { + t.Fatalf("enemy event reference slots = %#v / %#v", enemyEventExtractSpec.ReferenceSlots, enemyEventNormalizeSpec.ReferenceSlots) + } + npcRegistrySlot := referenceSlot(enemyEventExtractSpec.ReferenceSlots, "npcs") + enemyNormalizeRegistrySlot := referenceSlot(enemyEventNormalizeSpec.ReferenceSlots, "npcs") + if !npcRegistrySlot.Required || !enemyNormalizeRegistrySlot.Required || npcRegistrySlot.MaxBytes != enemyNormalizeRegistrySlot.MaxBytes || !reflect.DeepEqual(npcRegistrySlot.AcceptedMediaTypes, enemyNormalizeRegistrySlot.AcceptedMediaTypes) || !reflect.DeepEqual(npcRegistrySlot.AcceptedArtifactKinds, enemyNormalizeRegistrySlot.AcceptedArtifactKinds) { + t.Fatalf("enemy-event NPC registry slots disagree: %#v / %#v", enemyEventExtractSpec.ReferenceSlots, enemyEventNormalizeSpec.ReferenceSlots) + } itemEventExtractSpec, itemEventExtractOK := registries.Extractors.Spec(itemeventextract.Key) itemEventNormalizeSpec, itemEventNormalizeOK := registries.Normalizers.Spec(itemeventnormalize.Key) if !itemEventExtractOK || itemEventExtractSpec.ArtifactKind != dnd.ItemEventListKind || !itemEventNormalizeOK || itemEventNormalizeSpec.ArtifactKind != dnd.ItemEventListKind || itemEventNormalizeSpec.Stage != pipeline.StageNormalize || len(itemEventNormalizeSpec.ReferenceSlots) != 0 { @@ -361,6 +408,9 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes {name: "combat turns", project: func() []source.SourceRef { return combatTurnEvidence(dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{SourceRefs: []source.SourceRef{first, second}}}}) }, want: []source.SourceRef{first, second}}, + {name: "enemy events", project: func() []source.SourceRef { + return enemyEventEvidence(dnd.EnemyEventList{Events: []dnd.EnemyEvent{{SourceRefs: []source.SourceRef{first, second}}}}) + }, want: []source.SourceRef{first, second}}, {name: "item events", project: func() []source.SourceRef { return itemEventEvidence(dnd.ItemEventList{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{first, second}}}}) }, want: []source.SourceRef{first, second}}, @@ -536,6 +586,30 @@ func TestAppendCombatTurnListsPreservesOrderPresenceAndOwnership(t *testing.T) { } } +func TestAppendEnemyEventListsPreservesOrderPresenceAndOwnership(t *testing.T) { + refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} + input := []dnd.EnemyEventList{ + {}, + {Events: []dnd.EnemyEvent{}}, + {Events: []dnd.EnemyEvent{{Name: "first", Kind: dnd.EnemyEventKindEngaged, SourceRefs: refs}}}, + {Events: []dnd.EnemyEvent{{Name: "second", Kind: dnd.EnemyEventKindFled, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}, + } + got, err := appendEnemyEventLists(input) + if err != nil || got.Events == nil || !reflect.DeepEqual([]string{got.Events[0].Name, got.Events[1].Name}, []string{"first", "second"}) { + t.Fatalf("appendEnemyEventLists() = %#v, error = %v", got, err) + } + got.Events[0].SourceRefs[0].StartUnitID = 999 + if input[2].Events[0].SourceRefs[0].StartUnitID != 1 { + t.Fatal("merged enemy events share source-reference storage") + } + for _, values := range [][]dnd.EnemyEventList{nil, []dnd.EnemyEventList{{}, {}}} { + result, err := appendEnemyEventLists(values) + if err != nil || result.Events != nil { + t.Fatalf("nil-only merge = %#v, %v; want nil events", result, err) + } + } +} + func TestAppendItemEventListsPreservesOrderPresenceAndOwnership(t *testing.T) { quantity := 3 refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} @@ -578,6 +652,11 @@ func TestAppendListsPreserveNestedSourceReferencePresence(t *testing.T) { t.Fatalf("appendCombatTurnLists() = %#v, %v; want present-empty source refs", turns, err) } + enemyEvents, err := appendEnemyEventLists([]dnd.EnemyEventList{{Events: []dnd.EnemyEvent{{SourceRefs: []source.SourceRef{}}}}}) + if err != nil || enemyEvents.Events[0].SourceRefs == nil { + t.Fatalf("appendEnemyEventLists() = %#v, %v; want present-empty source refs", enemyEvents, err) + } + interactions, err := appendNPCInteractionLists([]dnd.NPCInteractionList{{Interactions: []dnd.NPCInteraction{{SourceRefs: []source.SourceRef{}}}}}) if err != nil || interactions.Interactions[0].SourceRefs == nil { t.Fatalf("appendNPCInteractionLists() = %#v, %v; want present-empty source refs", interactions, err) diff --git a/internal/modules/dnd/register/validators.go b/internal/modules/dnd/register/validators.go index 75c9de8..9ce94fb 100644 --- a/internal/modules/dnd/register/validators.go +++ b/internal/modules/dnd/register/validators.go @@ -7,6 +7,10 @@ import ( combatshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/shape" combatsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_refs" combatrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_relatedness" + enemyeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/invariants" + enemyeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/shape" + enemyeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_refs" + enemyeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_relatedness" itemeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/invariants" itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs" @@ -46,6 +50,10 @@ func registerValidators(registries pipeline.Registries) error { {name: "combat source references validator", register: func() error { return combatsourcerefs.Register(registries.Validators) }}, {name: "combat source relatedness validator", register: func() error { return combatrelatedness.Register(registries.Validators) }}, {name: "combat normalized invariants validator", register: func() error { return combatinvariants.Register(registries.Validators) }}, + {name: "enemy event shape validator", register: func() error { return enemyeventshape.Register(registries.Validators) }}, + {name: "enemy event source references validator", register: func() error { return enemyeventrefs.Register(registries.Validators) }}, + {name: "enemy event source relatedness validator", register: func() error { return enemyeventrelatedness.Register(registries.Validators) }}, + {name: "enemy event normalized invariants validator", register: func() error { return enemyeventinvariants.Register(registries.Validators) }}, {name: "item event shape validator", register: func() error { return itemeventshape.Register(registries.Validators) }}, {name: "item event source references validator", register: func() error { return itemeventrefs.Register(registries.Validators) }}, {name: "item event source relatedness validator", register: func() error { return itemeventrelatedness.Register(registries.Validators) }}, @@ -77,6 +85,12 @@ func registerValidators(registries pipeline.Registries) error { {name: "combat-turn-list always reject validator", register: func() error { return alwaysreject.RegisterTyped[dnd.CombatTurnList](registries.Validators, dnd.CombatTurnListKind) }}, + {name: "enemy-event-list always accept validator", register: func() error { + return alwaysaccept.RegisterTyped[dnd.EnemyEventList](registries.Validators, dnd.EnemyEventListKind) + }}, + {name: "enemy-event-list always reject validator", register: func() error { + return alwaysreject.RegisterTyped[dnd.EnemyEventList](registries.Validators, dnd.EnemyEventListKind) + }}, {name: "item-event-list always accept validator", register: func() error { return alwaysaccept.RegisterTyped[dnd.ItemEventList](registries.Validators, dnd.ItemEventListKind) }},