Compose D&D location tracking modules

This commit is contained in:
2026-08-04 00:39:00 +00:00
parent a168c13b85
commit 811d5b8bd9
8 changed files with 282 additions and 9 deletions

View File

@@ -334,7 +334,7 @@ func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", i
func referenceSlots() []contracts.ReferenceSlot {
slots := shared.ReferenceSlots(referenceSlotDescriptions)
slots = append(slots, contracts.ReferenceSlot{
Name: LocationRegistryReferenceSlot, Description: "Required normalized location registry used only for occurrence identity grounding, never as occurrence evidence.",
Name: LocationRegistryReferenceSlot, Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.",
Required: true, AcceptedMediaTypes: []string{"application/json"}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, MaxBytes: LocationRegistryMaxBytes,
})
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })

View File

@@ -5,6 +5,8 @@ import (
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"
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"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
@@ -12,6 +14,8 @@ import (
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"
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"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions"
@@ -29,6 +33,15 @@ import (
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"
itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness"
occurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants"
occurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry"
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape"
occurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs"
occurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_relatedness"
locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/identity"
locationshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/shape"
locationrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_refs"
locationrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_relatedness"
interactioninvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/invariants"
interactionregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/registry"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape"
@@ -245,5 +258,42 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
},
})
}},
{name: "locations validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageExtract, Module: locationextract.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(locationshape.Key), pipeline.Binding(locationrefs.Key),
pipeline.Binding(validjsonschema.Key), pipeline.Binding(locationrelatedness.Key),
},
})
}},
{name: "locations normalize validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageNormalize, Module: locationnormalize.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(locationshape.Key), pipeline.Binding(locationidentity.Key),
pipeline.Binding(locationrefs.Key), pipeline.Binding(validjsonschema.Key), pipeline.Binding(locationrelatedness.Key),
},
})
}},
{name: "location occurrences validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageExtract, Module: locationoccurrenceextract.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(occurrenceshape.Key), pipeline.Binding(occurrenceregistry.Key),
pipeline.Binding(occurrencerefs.Key), pipeline.Binding(validjsonschema.Key), pipeline.Binding(occurrencerelatedness.Key),
},
})
}},
{name: "location occurrences normalize validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageNormalize, Module: locationoccurrencenormalize.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(occurrenceshape.Key), pipeline.Binding(occurrenceregistry.Key),
pipeline.Binding(occurrenceinvariants.Key), pipeline.Binding(occurrencerefs.Key), pipeline.Binding(validjsonschema.Key),
pipeline.Binding(occurrencerelatedness.Key),
},
})
}},
})
}

View File

@@ -25,6 +25,12 @@ func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error {
{name: "scene descriptions evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence)
}},
{name: "locations evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.LocationListKind, locationEvidence)
}},
{name: "location occurrences evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.LocationOccurrenceListKind, locationOccurrenceEvidence)
}},
})
}
@@ -83,3 +89,19 @@ func sceneDescriptionEvidence(value dnd.SceneDescriptionList) []source.SourceRef
}
return refs
}
func locationEvidence(value dnd.LocationList) []source.SourceRef {
var refs []source.SourceRef
for _, location := range value.Locations {
refs = append(refs, location.SourceRefs...)
}
return append([]source.SourceRef(nil), refs...)
}
func locationOccurrenceEvidence(value dnd.LocationOccurrenceList) []source.SourceRef {
var refs []source.SourceRef
for _, occurrence := range value.Occurrences {
refs = append(refs, occurrence.SourceRefs...)
}
return append([]source.SourceRef(nil), refs...)
}

View File

@@ -152,6 +152,48 @@ func appendSceneDescriptionLists(values []dnd.SceneDescriptionList) (dnd.SceneDe
return combined, nil
}
func appendLocationLists(values []dnd.LocationList) (dnd.LocationList, error) {
count := 0
present := false
for _, value := range values {
if value.Locations != nil {
present = true
}
count += len(value.Locations)
}
if !present {
return dnd.LocationList{}, nil
}
combined := dnd.LocationList{Locations: make([]dnd.Location, 0, count)}
for _, value := range values {
for _, location := range value.Locations {
combined.Locations = append(combined.Locations, cloneLocation(location))
}
}
return combined, nil
}
func appendLocationOccurrenceLists(values []dnd.LocationOccurrenceList) (dnd.LocationOccurrenceList, error) {
count := 0
present := false
for _, value := range values {
if value.Occurrences != nil {
present = true
}
count += len(value.Occurrences)
}
if !present {
return dnd.LocationOccurrenceList{}, nil
}
combined := dnd.LocationOccurrenceList{Occurrences: make([]dnd.LocationOccurrence, 0, count)}
for _, value := range values {
for _, occurrence := range value.Occurrences {
combined.Occurrences = append(combined.Occurrences, cloneLocationOccurrence(occurrence))
}
}
return combined, nil
}
func cloneCombatTurn(value dnd.CombatTurn) dnd.CombatTurn {
clone := value
clone.SourceRefs = cloneSourceRefs(value.SourceRefs)
@@ -192,6 +234,18 @@ func cloneNPCInteraction(value dnd.NPCInteraction) dnd.NPCInteraction {
return clone
}
func cloneLocation(value dnd.Location) dnd.Location {
clone := value
clone.SourceRefs = cloneSourceRefs(value.SourceRefs)
return clone
}
func cloneLocationOccurrence(value dnd.LocationOccurrence) dnd.LocationOccurrence {
clone := value
clone.SourceRefs = cloneSourceRefs(value.SourceRefs)
return clone
}
func cloneSourceRefs(refs []source.SourceRef) []source.SourceRef {
return slices.Clone(refs)
}

View File

@@ -8,6 +8,8 @@ import (
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"
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locations"
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"
@@ -15,6 +17,8 @@ import (
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"
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"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
@@ -22,6 +26,8 @@ import (
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"
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"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions"
@@ -43,6 +49,10 @@ func registerModules(registries pipeline.Registries) error {
{name: "scene descriptions codec", register: func() error {
return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, scenedescriptioncodec.New())
}},
{name: "locations codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, locationcodec.New()) }},
{name: "location occurrences codec", register: func() error {
return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, locationoccurrencecodec.New())
}},
{name: "scenes chunker", register: func() error { return scenes.Register(registries.Chunkers) }},
{name: "spells extractor", register: func() error { return spellextract.Register(registries.Extractors) }},
{name: "npcs extractor", register: func() error { return npcextract.Register(registries.Extractors) }},
@@ -51,6 +61,8 @@ func registerModules(registries pipeline.Registries) error {
{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) }},
{name: "locations extractor", register: func() error { return locationextract.Register(registries.Extractors) }},
{name: "location occurrences extractor", register: func() error { return locationoccurrenceextract.Register(registries.Extractors) }},
{name: "spell-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.SpellListKind, appendSpellLists)
}},
@@ -72,6 +84,12 @@ func registerModules(registries pipeline.Registries) error {
{name: "scene-description-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.SceneDescriptionListKind, appendSceneDescriptionLists)
}},
{name: "location-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.LocationListKind, appendLocationLists)
}},
{name: "location-occurrence-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.LocationOccurrenceListKind, appendLocationOccurrenceLists)
}},
{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) }},
@@ -79,6 +97,8 @@ func registerModules(registries pipeline.Registries) error {
{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) }},
{name: "locations normalizer", register: func() error { return locationnormalize.Register(registries.Normalizers) }},
{name: "location occurrences normalizer", register: func() error { return locationoccurrencenormalize.Register(registries.Normalizers) }},
{name: "spell-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.SpellList](registries.Normalizers, dnd.SpellListKind)
}},
@@ -100,6 +120,12 @@ func registerModules(registries pipeline.Registries) error {
{name: "scene-description-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.SceneDescriptionList](registries.Normalizers, dnd.SceneDescriptionListKind)
}},
{name: "location-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.LocationList](registries.Normalizers, dnd.LocationListKind)
}},
{name: "location-occurrence-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.LocationOccurrenceList](registries.Normalizers, dnd.LocationOccurrenceListKind)
}},
})
}
@@ -115,5 +141,8 @@ func registerPromptAssets(assets *llm.AssetRegistry) error {
{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) }},
{name: "locations prompt assets", register: func() error { return locationextract.RegisterPromptAssets(assets) }},
{name: "location normalization prompt assets", register: func() error { return locationnormalize.RegisterPromptAssets(assets) }},
{name: "location occurrences prompt assets", register: func() error { return locationoccurrenceextract.RegisterPromptAssets(assets) }},
})
}

View File

@@ -15,6 +15,8 @@ import (
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"
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"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
@@ -22,6 +24,8 @@ import (
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"
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"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions"
@@ -51,6 +55,9 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"dnd.npc_interactions/dnd.npc_interactions.yaml",
"dnd.scene_descriptions/dnd.scene_descriptions.yaml",
"dnd.npcs.normalize/dnd.npcs.normalize.yaml",
"dnd.locations/dnd.locations.yaml",
"dnd.locations.normalize/dnd.locations.normalize.yaml",
"dnd.location_occurrences/dnd.location_occurrences.yaml",
} {
content, err := fs.ReadFile(promptFS, name)
if err != nil {
@@ -75,12 +82,12 @@ 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})
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})
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})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
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})
@@ -88,6 +95,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(interactionnormalize.Key), []contracts.ArtifactKind{dnd.NPCInteractionListKind})
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",
"extract/dnd/location-occurrences/shape", "extract/dnd/location-occurrences/registry", "normalize/dnd/location-occurrences/invariants", "extract/dnd/location-occurrences/source_refs", "extract/dnd/location-occurrences/source_relatedness",
"extract/dnd/npcs/shape",
"extract/dnd/npcs/source_refs",
"extract/dnd/npcs/source_relatedness",
@@ -121,6 +130,22 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"generic/always_accept",
"generic/always_reject",
})
locationExtractChain := []pipeline.ModuleBinding{pipeline.Binding("generic/valid_json"), pipeline.Binding("extract/dnd/locations/shape"), pipeline.Binding("extract/dnd/locations/source_refs"), pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("extract/dnd/locations/source_relatedness")}
locationNormalizeChain := []pipeline.ModuleBinding{pipeline.Binding("generic/valid_json"), pipeline.Binding("extract/dnd/locations/shape"), pipeline.Binding("normalize/dnd/locations/identity"), pipeline.Binding("extract/dnd/locations/source_refs"), pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("extract/dnd/locations/source_relatedness")}
occurrenceExtractChain := []pipeline.ModuleBinding{pipeline.Binding("generic/valid_json"), pipeline.Binding("extract/dnd/location-occurrences/shape"), pipeline.Binding("extract/dnd/location-occurrences/registry"), pipeline.Binding("extract/dnd/location-occurrences/source_refs"), pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("extract/dnd/location-occurrences/source_relatedness")}
occurrenceNormalizeChain := []pipeline.ModuleBinding{pipeline.Binding("generic/valid_json"), pipeline.Binding("extract/dnd/location-occurrences/shape"), pipeline.Binding("extract/dnd/location-occurrences/registry"), pipeline.Binding("normalize/dnd/location-occurrences/invariants"), pipeline.Binding("extract/dnd/location-occurrences/source_refs"), pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("extract/dnd/location-occurrences/source_relatedness")}
for _, test := range []struct {
stage pipeline.ModuleStage
key string
want []pipeline.ModuleBinding
}{
{pipeline.StageExtract, locationextract.Key, locationExtractChain}, {pipeline.StageNormalize, locationnormalize.Key, locationNormalizeChain},
{pipeline.StageExtract, locationoccurrenceextract.Key, occurrenceExtractChain}, {pipeline.StageNormalize, locationoccurrencenormalize.Key, occurrenceNormalizeChain},
} {
if got := registries.ValidatorChains.Validators(test.stage, test.key); !reflect.DeepEqual(got, test.want) {
t.Fatalf("validator chain for %s/%s = %#v, want %#v", test.stage, test.key, got, test.want)
}
}
wantChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/spells/shape"),
@@ -334,6 +359,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"dnd_item_events_llm.v1.json",
"dnd_npc_interactions_llm.v1.json",
"dnd_scene_descriptions_llm.v1.json",
"dnd_locations_llm.v1.json",
"dnd_location_occurrences_llm.v1.json",
})
if spec, ok := registries.Chunkers.Spec("dnd/scenes"); !ok || spec.Key != "dnd/scenes" {
t.Fatalf("scene chunker spec = %#v, present = %t; want family-owned spec", spec, ok)
@@ -384,6 +411,21 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if !extractOK || interactionExtractSpec.ArtifactKind != dnd.NPCInteractionListKind || !normalizeOK || interactionNormalizeSpec.ArtifactKind != dnd.NPCInteractionListKind || interactionNormalizeSpec.Stage != pipeline.StageNormalize {
t.Fatalf("NPC interaction specs = %#v / %#v, present = %t / %t", interactionExtractSpec, interactionNormalizeSpec, 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)
}
locationRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "locations")
occurrenceNormalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "locations")
if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || !reflect.DeepEqual(locationRegistrySlot, occurrenceNormalizeRegistrySlot) {
t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
}
sceneExtractSpec, sceneExtractOK := registries.Extractors.Spec(scenedescriptionextract.Key)
sceneNormalizeSpec, sceneNormalizeOK := registries.Normalizers.Spec(scenedescriptionnormalize.Key)
if !sceneExtractOK || sceneExtractSpec.ArtifactKind != dnd.SceneDescriptionListKind || !sceneNormalizeOK || sceneNormalizeSpec.ArtifactKind != dnd.SceneDescriptionListKind || sceneNormalizeSpec.Stage != pipeline.StageNormalize {
@@ -428,6 +470,12 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes
{name: "scene descriptions", project: func() []source.SourceRef {
return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}})
}, want: []source.SourceRef{first, second}},
{name: "locations", project: func() []source.SourceRef {
return locationEvidence(dnd.LocationList{Locations: []dnd.Location{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}},
{name: "location occurrences", project: func() []source.SourceRef {
return locationOccurrenceEvidence(dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}},
} {
t.Run(test.name, func(t *testing.T) {
got := test.project()
@@ -482,6 +530,44 @@ func TestAppendNPCListsPreservesOrderAndArrayPresence(t *testing.T) {
}
}
func TestAppendLocationListsPreserveOrderPresenceAndOwnership(t *testing.T) {
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}
input := []dnd.LocationList{{Locations: []dnd.Location{{ID: "one", Name: "First", SourceRefs: refs}}}, {Locations: []dnd.Location{{ID: "two", Name: "Second", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}}
got, err := appendLocationLists(input)
if err != nil || !reflect.DeepEqual([]string{got.Locations[0].Name, got.Locations[1].Name}, []string{"First", "Second"}) {
t.Fatalf("appendLocationLists() = %#v, %v", got, err)
}
got.Locations[0].SourceRefs[0].StartUnitID = 99
if input[0].Locations[0].SourceRefs[0].StartUnitID != 1 {
t.Fatal("merged locations share source-reference storage")
}
for _, values := range [][]dnd.LocationList{nil, {{}, {}}} {
result, err := appendLocationLists(values)
if err != nil || result.Locations != nil {
t.Fatalf("nil-only merge = %#v, %v", result, err)
}
}
}
func TestAppendLocationOccurrenceListsPreserveOrderPresenceAndOwnership(t *testing.T) {
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}
input := []dnd.LocationOccurrenceList{{Occurrences: []dnd.LocationOccurrence{{LocationID: "one", Name: "First", Kind: dnd.LocationOccurrenceKindVisited, SourceRefs: refs}}}, {Occurrences: []dnd.LocationOccurrence{{LocationID: "two", Name: "Second", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}}
got, err := appendLocationOccurrenceLists(input)
if err != nil || !reflect.DeepEqual([]string{got.Occurrences[0].Name, got.Occurrences[1].Name}, []string{"First", "Second"}) {
t.Fatalf("appendLocationOccurrenceLists() = %#v, %v", got, err)
}
got.Occurrences[0].SourceRefs[0].StartUnitID = 99
if input[0].Occurrences[0].SourceRefs[0].StartUnitID != 1 {
t.Fatal("merged location occurrences share source-reference storage")
}
for _, values := range [][]dnd.LocationOccurrenceList{nil, {{}, {}}} {
result, err := appendLocationOccurrenceLists(values)
if err != nil || result.Occurrences != nil {
t.Fatalf("nil-only merge = %#v, %v", result, err)
}
}
}
func TestAppendSpellListsPreservesOrderPresenceAndOwnership(t *testing.T) {
tests := []struct {
name string

View File

@@ -16,6 +16,15 @@ import (
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"
itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness"
occurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants"
occurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry"
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape"
occurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs"
occurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_relatedness"
locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/identity"
locationshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/shape"
locationrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_refs"
locationrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_relatedness"
interactioninvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/invariants"
interactionregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/registry"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape"
@@ -69,6 +78,15 @@ func registerValidators(registries pipeline.Registries) error {
{name: "scene description source references validator", register: func() error { return scenerefs.Register(registries.Validators) }},
{name: "scene description source relatedness validator", register: func() error { return scenerelatedness.Register(registries.Validators) }},
{name: "scene description normalized invariants validator", register: func() error { return sceneinvariants.Register(registries.Validators) }},
{name: "location shape validator", register: func() error { return locationshape.Register(registries.Validators) }},
{name: "location identity validator", register: func() error { return locationidentity.Register(registries.Validators) }},
{name: "location source references validator", register: func() error { return locationrefs.Register(registries.Validators) }},
{name: "location source relatedness validator", register: func() error { return locationrelatedness.Register(registries.Validators) }},
{name: "location occurrence shape validator", register: func() error { return occurrenceshape.Register(registries.Validators) }},
{name: "location occurrence registry validator", register: func() error { return occurrenceregistry.Register(registries.Validators) }},
{name: "location occurrence normalized invariants validator", register: func() error { return occurrenceinvariants.Register(registries.Validators) }},
{name: "location occurrence source references validator", register: func() error { return occurrencerefs.Register(registries.Validators) }},
{name: "location occurrence source relatedness validator", register: func() error { return occurrencerelatedness.Register(registries.Validators) }},
{name: "spell-list always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.SpellList](registries.Validators, dnd.SpellListKind)
}},
@@ -111,5 +129,17 @@ func registerValidators(registries pipeline.Registries) error {
{name: "scene-description-list always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.SceneDescriptionList](registries.Validators, dnd.SceneDescriptionListKind)
}},
{name: "location-list always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.LocationList](registries.Validators, dnd.LocationListKind)
}},
{name: "location-list always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.LocationList](registries.Validators, dnd.LocationListKind)
}},
{name: "location-occurrence-list always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.LocationOccurrenceList](registries.Validators, dnd.LocationOccurrenceListKind)
}},
{name: "location-occurrence-list always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.LocationOccurrenceList](registries.Validators, dnd.LocationOccurrenceListKind)
}},
})
}

View File

@@ -27,7 +27,7 @@ func TestValidatorDistinguishesSameNameLocationsByID(t *testing.T) {
if err != nil || !result.Approved || !reflect.DeepEqual(value, before) {
t.Fatalf("matching IDs = %#v, %v", result, err)
}
value.Occurrences[0].Name = "Other Gate"
value.Occurrences[0].Name = "Sun Gate"
result, err = validator.Validate(context.Background(), request(references, value))
if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "does not match registry location") {
t.Fatalf("mismatched name = %#v, %v", result, err)
@@ -101,7 +101,9 @@ func registryReferences(t *testing.T) (contracts.ReferenceSet, dnd.Location, dnd
secondRefs := []source.SourceRef{{SourceID: "source", StartUnitID: 2, EndUnitID: 2}}
first := dnd.Location{ID: identity.DeriveID("Moon Gate", firstRefs), Name: "Moon Gate", SourceRefs: firstRefs}
second := dnd.Location{ID: identity.DeriveID("Moon Gate", secondRefs), Name: "Moon Gate", SourceRefs: secondRefs}
content, err := locationcodec.New().Encode(dnd.LocationList{Locations: []dnd.Location{first, second}})
thirdRefs := []source.SourceRef{{SourceID: "source", StartUnitID: 3, EndUnitID: 3}}
third := dnd.Location{ID: identity.DeriveID("Sun Gate", thirdRefs), Name: "Sun Gate", SourceRefs: thirdRefs}
content, err := locationcodec.New().Encode(dnd.LocationList{Locations: []dnd.Location{first, second, third}})
if err != nil {
t.Fatal(err)
}