Move NPC occurrences to their canonical namespace

This commit is contained in:
2026-08-05 19:00:55 +00:00
parent 5e2ccffc0f
commit 2f61118e78
59 changed files with 518 additions and 518 deletions

View File

@@ -17,7 +17,7 @@ import (
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locations"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions"
occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
@@ -26,7 +26,7 @@ import (
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locations"
interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions"
occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
@@ -52,7 +52,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"dnd.combat_turns/prompt.yaml",
"dnd.enemy_events/prompt.yaml",
"dnd.item_events/prompt.yaml",
"dnd.npc_interactions/prompt.yaml",
"dnd.npc_occurrences/prompt.yaml",
"dnd.scene_descriptions/prompt.yaml",
"dnd.npc_registry.normalize/prompt.yaml",
"dnd.locations/prompt.yaml",
@@ -82,8 +82,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
t.Fatalf("entity reconciliation schema asset = %v, want registered shared schema", err)
}
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, interactionextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, interactionnormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
@@ -92,7 +92,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
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.NPCOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(occurrencenormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(scenedescriptionnormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind})
assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
"extract/dnd/locations/shape", "normalize/dnd/locations/identity", "extract/dnd/locations/source_refs", "extract/dnd/locations/source_relatedness",
@@ -118,11 +118,11 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"extract/dnd/item-events/source_refs",
"extract/dnd/item-events/source_relatedness",
"normalize/dnd/item-events/invariants",
"extract/dnd/npc-interactions/shape",
"extract/dnd/npc-interactions/registry",
"extract/dnd/npc-interactions/source_refs",
"extract/dnd/npc-interactions/source_relatedness",
"normalize/dnd/npc-interactions/invariants",
"extract/dnd/npc-occurrences/shape",
"extract/dnd/npc-occurrences/registry",
"extract/dnd/npc-occurrences/source_refs",
"extract/dnd/npc-occurrences/source_relatedness",
"normalize/dnd/npc-occurrences/invariants",
"extract/dnd/scene-descriptions/shape",
"extract/dnd/scene-descriptions/source_refs",
"extract/dnd/scene-descriptions/source_relatedness",
@@ -245,28 +245,28 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemeventnormalize.Key); !reflect.DeepEqual(got, itemEventNormalizeChain) {
t.Fatalf("item event normalize validator chain = %#v, want %#v", got, itemEventNormalizeChain)
}
interactionExtractChain := []pipeline.ModuleBinding{
npcOccurrenceExtractChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/npc-interactions/shape"),
pipeline.Binding("extract/dnd/npc-interactions/registry"),
pipeline.Binding("extract/dnd/npc-interactions/source_refs"),
pipeline.Binding("extract/dnd/npc-occurrences/shape"),
pipeline.Binding("extract/dnd/npc-occurrences/registry"),
pipeline.Binding("extract/dnd/npc-occurrences/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npc-interactions/source_relatedness"),
pipeline.Binding("extract/dnd/npc-occurrences/source_relatedness"),
}
interactionNormalizeChain := []pipeline.ModuleBinding{
npcOccurrenceNormalizeChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/npc-interactions/shape"),
pipeline.Binding("extract/dnd/npc-interactions/registry"),
pipeline.Binding("normalize/dnd/npc-interactions/invariants"),
pipeline.Binding("extract/dnd/npc-interactions/source_refs"),
pipeline.Binding("extract/dnd/npc-occurrences/shape"),
pipeline.Binding("extract/dnd/npc-occurrences/registry"),
pipeline.Binding("normalize/dnd/npc-occurrences/invariants"),
pipeline.Binding("extract/dnd/npc-occurrences/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npc-interactions/source_relatedness"),
pipeline.Binding("extract/dnd/npc-occurrences/source_relatedness"),
}
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, interactionextract.Key); !reflect.DeepEqual(got, interactionExtractChain) {
t.Fatalf("NPC interaction extract validator chain = %#v, want %#v", got, interactionExtractChain)
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, occurrenceextract.Key); !reflect.DeepEqual(got, npcOccurrenceExtractChain) {
t.Fatalf("NPC occurrence extract validator chain = %#v, want %#v", got, npcOccurrenceExtractChain)
}
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, interactionnormalize.Key); !reflect.DeepEqual(got, interactionNormalizeChain) {
t.Fatalf("NPC interaction normalize validator chain = %#v, want %#v", got, interactionNormalizeChain)
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, occurrencenormalize.Key); !reflect.DeepEqual(got, npcOccurrenceNormalizeChain) {
t.Fatalf("NPC occurrence normalize validator chain = %#v, want %#v", got, npcOccurrenceNormalizeChain)
}
sceneExtractChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
@@ -292,8 +292,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, npcextract.Key); got != nil {
t.Fatalf("NPC merge validator chain = %#v, want absent", got)
}
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, interactionextract.Key); got != nil {
t.Fatalf("NPC interaction merge validator chain = %#v, want absent", got)
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, occurrenceextract.Key); got != nil {
t.Fatalf("NPC occurrence merge validator chain = %#v, want absent", got)
}
assertAssetNamesContain(t, assets.SchemaFS, []string{
"dnd_scenes_llm.v1.json",
@@ -351,31 +351,31 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
t.Fatalf("item event extractor has a generated-reference dependency: %#v", slot)
}
}
interactionExtractSpec, extractOK := registries.Extractors.Spec(interactionextract.Key)
interactionNormalizeSpec, normalizeOK := registries.Normalizers.Spec(interactionnormalize.Key)
if !extractOK || interactionExtractSpec.ArtifactKind != dnd.NPCOccurrenceListKind || !normalizeOK || interactionNormalizeSpec.ArtifactKind != dnd.NPCOccurrenceListKind || interactionNormalizeSpec.Stage != pipeline.StageNormalize {
t.Fatalf("NPC interaction specs = %#v / %#v, present = %t / %t", interactionExtractSpec, interactionNormalizeSpec, extractOK, normalizeOK)
occurrenceExtractSpec, extractOK := registries.Extractors.Spec(occurrenceextract.Key)
occurrenceNormalizeSpec, normalizeOK := registries.Normalizers.Spec(occurrencenormalize.Key)
if !extractOK || occurrenceExtractSpec.ArtifactKind != dnd.NPCOccurrenceListKind || !normalizeOK || occurrenceNormalizeSpec.ArtifactKind != dnd.NPCOccurrenceListKind || occurrenceNormalizeSpec.Stage != pipeline.StageNormalize {
t.Fatalf("NPC occurrence specs = %#v / %#v, present = %t / %t", occurrenceExtractSpec, occurrenceNormalizeSpec, extractOK, normalizeOK)
}
locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.Key)
locationNormalizeSpec, locationNormalizeOK := registries.Normalizers.Spec(locationnormalize.Key)
if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationListKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationListKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked {
t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec)
}
occurrenceExtractSpec, occurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key)
occurrenceNormalizeSpec, occurrenceNormalizeOK := registries.Normalizers.Spec(locationoccurrencenormalize.Key)
if !occurrenceExtractOK || occurrenceExtractSpec.ArtifactKind != dnd.LocationOccurrenceListKind || occurrenceExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !occurrenceNormalizeOK || occurrenceNormalizeSpec.ArtifactKind != dnd.LocationOccurrenceListKind || occurrenceNormalizeSpec.ExecutionClass != contracts.ExecutionClassDeterministic {
t.Fatalf("location occurrence specs = %#v / %#v", occurrenceExtractSpec, occurrenceNormalizeSpec)
locationOccurrenceExtractSpec, locationOccurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key)
locationOccurrenceNormalizeSpec, locationOccurrenceNormalizeOK := registries.Normalizers.Spec(locationoccurrencenormalize.Key)
if !locationOccurrenceExtractOK || locationOccurrenceExtractSpec.ArtifactKind != dnd.LocationOccurrenceListKind || locationOccurrenceExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationOccurrenceNormalizeOK || locationOccurrenceNormalizeSpec.ArtifactKind != dnd.LocationOccurrenceListKind || locationOccurrenceNormalizeSpec.ExecutionClass != contracts.ExecutionClassDeterministic {
t.Fatalf("location occurrence specs = %#v / %#v", locationOccurrenceExtractSpec, locationOccurrenceNormalizeSpec)
}
locationRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "locations")
occurrenceNormalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "locations")
if len(occurrenceExtractSpec.ReferenceSlots) != 5 || len(occurrenceNormalizeSpec.ReferenceSlots) != 1 {
t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
locationRegistrySlot := referenceSlot(locationOccurrenceExtractSpec.ReferenceSlots, "locations")
occurrenceNormalizeRegistrySlot := referenceSlot(locationOccurrenceNormalizeSpec.ReferenceSlots, "locations")
if len(locationOccurrenceExtractSpec.ReferenceSlots) != 5 || len(locationOccurrenceNormalizeSpec.ReferenceSlots) != 1 {
t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", locationOccurrenceExtractSpec.ReferenceSlots, locationOccurrenceNormalizeSpec.ReferenceSlots)
}
if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) {
t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
}
for _, name := range []string{"party", "roster", "players", "glossary"} {
slot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, name)
slot := referenceSlot(locationOccurrenceExtractSpec.ReferenceSlots, name)
if slot.Name != name || slot.Required || len(slot.AcceptedArtifactKinds) != 0 {
t.Fatalf("location occurrence extractor campaign slot %q = %#v, want optional text context", name, slot)
}
@@ -388,10 +388,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if len(sceneExtractSpec.ReferenceSlots) != 3 || len(sceneNormalizeSpec.ReferenceSlots) != 0 {
t.Fatalf("scene description reference slots = %#v / %#v, want extractor campaign slots only", sceneExtractSpec.ReferenceSlots, sceneNormalizeSpec.ReferenceSlots)
}
extractRegistrySlot := referenceSlot(interactionExtractSpec.ReferenceSlots, "npc_registry")
normalizeRegistrySlot := referenceSlot(interactionNormalizeSpec.ReferenceSlots, "npc_registry")
extractRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "npc_registry")
normalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "npc_registry")
if !extractRegistrySlot.Required || !reflect.DeepEqual(extractRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCRegistryKind}) || !sameReferenceSlotContract(extractRegistrySlot, normalizeRegistrySlot) {
t.Fatalf("NPC interaction registry slots disagree: %#v / %#v", interactionExtractSpec.ReferenceSlots, interactionNormalizeSpec.ReferenceSlots)
t.Fatalf("NPC occurrence registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
}
}
@@ -418,8 +418,8 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes
{name: "item events", project: func() []source.SourceRef {
return itemEventEvidence(dnd.ItemEventList{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}},
{name: "npc interactions", project: func() []source.SourceRef {
return npcInteractionEvidence(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{first, second}}}})
{name: "npc occurrences", project: func() []source.SourceRef {
return npcOccurrenceEvidence(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}},
{name: "scene descriptions", project: func() []source.SourceRef {
return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}})
@@ -559,7 +559,7 @@ func TestAppendSpellListsPreservesOrderPresenceAndOwnership(t *testing.T) {
}
}
func TestAppendNPCInteractionListsPreservesOrderPresenceAndOwnership(t *testing.T) {
func TestAppendNPCOccurrenceListsPreservesOrderPresenceAndOwnership(t *testing.T) {
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}
input := []dnd.NPCOccurrenceList{
{},
@@ -567,21 +567,21 @@ func TestAppendNPCInteractionListsPreservesOrderPresenceAndOwnership(t *testing.
{Occurrences: []dnd.NPCOccurrence{{Name: "Aria", Kind: dnd.NPCOccurrenceKindDialogue, SourceRefs: refs}}},
{Occurrences: []dnd.NPCOccurrence{{Name: "Borin", Kind: dnd.NPCOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 20, EndUnitID: 20}}}}},
}
got, err := appendNPCInteractionLists(input)
got, err := appendNPCOccurrenceLists(input)
if err != nil {
t.Fatalf("appendNPCInteractionLists() error = %v", err)
t.Fatalf("appendNPCOccurrenceLists() error = %v", err)
}
if got.Occurrences == nil || !reflect.DeepEqual([]string{got.Occurrences[0].Name, got.Occurrences[1].Name}, []string{"Aria", "Borin"}) {
t.Fatalf("combined interactions = %#v", got)
t.Fatalf("combined occurrences = %#v", got)
}
got.Occurrences[0].SourceRefs[0].StartUnitID = 999
if input[2].Occurrences[0].SourceRefs[0].StartUnitID == 999 {
t.Fatal("merged interactions share source reference storage")
t.Fatal("merged occurrences share source reference storage")
}
for _, values := range [][]dnd.NPCOccurrenceList{nil, []dnd.NPCOccurrenceList{{}, {}}} {
result, err := appendNPCInteractionLists(values)
result, err := appendNPCOccurrenceLists(values)
if err != nil || result.Occurrences != nil {
t.Fatalf("nil-only merge = %#v, %v; want nil interactions", result, err)
t.Fatalf("nil-only merge = %#v, %v; want nil occurrences", result, err)
}
}
}
@@ -711,9 +711,9 @@ func TestAppendListsPreserveNestedSourceReferencePresence(t *testing.T) {
t.Fatalf("appendEnemyEventLists() = %#v, %v; want present-empty source refs", enemyEvents, err)
}
interactions, err := appendNPCInteractionLists([]dnd.NPCOccurrenceList{{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{}}}}})
if err != nil || interactions.Occurrences[0].SourceRefs == nil {
t.Fatalf("appendNPCInteractionLists() = %#v, %v; want present-empty source refs", interactions, err)
occurrences, err := appendNPCOccurrenceLists([]dnd.NPCOccurrenceList{{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{}}}}})
if err != nil || occurrences.Occurrences[0].SourceRefs == nil {
t.Fatalf("appendNPCOccurrenceLists() = %#v, %v; want present-empty source refs", occurrences, err)
}
events, err := appendItemEventLists([]dnd.ItemEventList{{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{}}}}})