Verify complete D&D entity handoffs

This commit is contained in:
2026-08-05 20:07:31 +00:00
parent a6e176e160
commit 6de470d541
5 changed files with 260 additions and 36 deletions

View File

@@ -37,9 +37,9 @@ pipelines:
lanes:
- item-occurrences
- item-registry
- locations
- location-registry
- location-occurrences
- npc_registry
- npc-registry
- spells
- combat-turns
- npc-occurrences
@@ -54,7 +54,7 @@ pipelines:
retries: 2
merge: appendorder
normalize: dnd/item-registry
npc_registry:
npc-registry:
extract:
module: dnd/npc-registry
retries: 2
@@ -62,7 +62,7 @@ pipelines:
normalize:
module: dnd/npc-registry
retries: 2
locations:
location-registry:
extract:
module: dnd/location-registry
retries: 2
@@ -83,11 +83,11 @@ pipelines:
location_registry:
artifact:
step: describe-session
lane: locations
lane: location-registry
npc_registry:
artifact:
step: describe-session
lane: npc_registry
lane: npc-registry
scene_descriptions:
artifact:
step: describe-session
@@ -138,7 +138,7 @@ pipelines:
npc_registry:
artifact:
step: describe-session
lane: npc_registry
lane: npc-registry
scene_descriptions:
artifact:
step: describe-session

View File

@@ -20,8 +20,12 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences"
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
npcoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
combat "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
enemyevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
itemoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
@@ -57,7 +61,7 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
t.Fatalf("enemy event lane = %#v, want typed production composition", lane)
}
for slot, want := range map[string]struct{ step, lane string }{
"npc_registry": {step: "describe-session", lane: "npc_registry"},
"npc_registry": {step: "describe-session", lane: "npc-registry"},
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
"combat_turns": {step: "extract-events", lane: "combat-turns"},
"npc_occurrences": {step: "extract-events", lane: "npc-occurrences"},
@@ -67,7 +71,7 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
t.Fatalf("enemy event %s reference = %#v, want generated %s/%s artifact", slot, binding, want.step, want.lane)
}
}
if binding, found := generatedReferenceBinding(lane.NormalizeReferences.Bindings, "npc_registry"); !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "npc_registry" {
if binding, found := generatedReferenceBinding(lane.NormalizeReferences.Bindings, "npc_registry"); !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "npc-registry" {
t.Fatalf("enemy event normalizer NPC reference = %#v, want generated NPC artifact", binding)
}
@@ -97,7 +101,7 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
}
}
func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t *testing.T) {
func TestMaintainedCompleteExamplePublishesRegistryBackedEntityOccurrences(t *testing.T) {
t.Chdir(repositoryPath())
outputRoot := filepath.Join(t.TempDir(), "output")
configPath := completeExampleConfigWithTemporaryCache(t)
@@ -123,18 +127,17 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
runRoot := filepath.Join(outputRoot, productionRunID)
index := readProductionJSON[exampleOutputIndex](t, filepath.Join(runRoot, "index.json"))
var enemyOutput exampleOutputIndexEntry
var locationOutput, occurrenceOutput exampleOutputIndexEntry
outputs := make(map[string]exampleOutputIndexEntry)
for _, entry := range index.OutputFiles {
switch entry.LaneID {
case "enemy-events":
enemyOutput = entry
case "locations":
locationOutput = entry
case "location-occurrences":
occurrenceOutput = entry
}
outputs[entry.LaneID] = entry
}
enemyOutput := outputs["enemy-events"]
locationOutput := outputs["location-registry"]
locationOccurrenceOutput := outputs["location-occurrences"]
npcRegistryOutput := outputs["npc-registry"]
npcOccurrenceOutput := outputs["npc-occurrences"]
itemRegistryOutput := outputs["item-registry"]
itemOccurrenceOutput := outputs["item-occurrences"]
if enemyOutput.File != "lanes/enemy-events.json" || enemyOutput.SchemaID != "notarius.dnd.enemy_events" || enemyOutput.SchemaVersion != "v1" {
t.Fatalf("enemy event output = %#v, want typed enemy-event JSON", enemyOutput)
}
@@ -142,23 +145,51 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
if len(value.Events) != 1 || value.Events[0].Name != "Kesh" || value.Events[0].Kind != dnd.EnemyEventKindFled || len(value.Events[0].SourceRefs) != 1 || value.Events[0].SourceRefs[0].SourceID != "session-ravenfall" || value.Events[0].SourceRefs[0].StartUnitID != 10 {
t.Fatalf("enemy event artifact = %#v, want source-linked Kesh fleeing event", value)
}
if locationOutput.File != "lanes/locations.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion {
if locationOutput.File != "lanes/location-registry.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion {
t.Fatalf("location output = %#v, want typed location registry JSON", locationOutput)
}
locationsValue := readProductionJSON[dnd.LocationRegistry](t, filepath.Join(runRoot, locationOutput.File))
if len(locationsValue.Locations) != 2 || locationsValue.Locations[0].Name != "Moon Gate" || locationsValue.Locations[1].Name != "Moon Gate" || locationsValue.Locations[0].ID == locationsValue.Locations[1].ID {
t.Fatalf("location registry = %#v, want distinct source-grounded identities for same-name locations", locationsValue)
}
if occurrenceOutput.File != "lanes/location-occurrences.json" || occurrenceOutput.SchemaID != locationoccurrencecodec.SchemaID || occurrenceOutput.SchemaVersion != locationoccurrencecodec.SchemaVersion {
t.Fatalf("location occurrence output = %#v, want typed occurrence JSON", occurrenceOutput)
if locationOccurrenceOutput.File != "lanes/location-occurrences.json" || locationOccurrenceOutput.SchemaID != locationoccurrencecodec.SchemaID || locationOccurrenceOutput.SchemaVersion != locationoccurrencecodec.SchemaVersion {
t.Fatalf("location occurrence output = %#v, want typed occurrence JSON", locationOccurrenceOutput)
}
occurrencesValue := readProductionJSON[dnd.LocationOccurrenceList](t, filepath.Join(runRoot, occurrenceOutput.File))
occurrencesValue := readProductionJSON[dnd.LocationOccurrenceList](t, filepath.Join(runRoot, locationOccurrenceOutput.File))
if len(occurrencesValue.Occurrences) != 2 || occurrencesValue.Occurrences[0].LocationID == occurrencesValue.Occurrences[1].LocationID || occurrencesValue.Occurrences[0].Name != "Moon Gate" || occurrencesValue.Occurrences[1].Name != "Moon Gate" {
t.Fatalf("location occurrences = %#v, want source-grounded references to distinct registry identities", occurrencesValue)
}
if npcRegistryOutput.File != "lanes/npc-registry.json" || npcRegistryOutput.SchemaID != npccodec.SchemaID || npcRegistryOutput.SchemaVersion != npccodec.SchemaVersion {
t.Fatalf("NPC registry output = %#v, want typed registry JSON", npcRegistryOutput)
}
npcRegistry := readProductionJSON[dnd.NPCRegistry](t, filepath.Join(runRoot, npcRegistryOutput.File))
if len(npcRegistry.NPCs) != 1 || npcRegistry.NPCs[0].Name != "Kesh" {
t.Fatalf("NPC registry = %#v, want Kesh identity", npcRegistry)
}
if npcOccurrenceOutput.File != "lanes/npc-occurrences.json" || npcOccurrenceOutput.SchemaID != npcoccurrencecodec.SchemaID || npcOccurrenceOutput.SchemaVersion != npcoccurrencecodec.SchemaVersion {
t.Fatalf("NPC occurrence output = %#v, want typed occurrence JSON", npcOccurrenceOutput)
}
npcOccurrences := readProductionJSON[dnd.NPCOccurrenceList](t, filepath.Join(runRoot, npcOccurrenceOutput.File))
if len(npcOccurrences.Occurrences) != 1 || npcOccurrences.Occurrences[0].NPCID != npcRegistry.NPCs[0].ID || npcOccurrences.Occurrences[0].Name != "Kesh" || len(npcOccurrences.Occurrences[0].SourceRefs) != 1 || npcOccurrences.Occurrences[0].SourceRefs[0].StartUnitID != 7 {
t.Fatalf("NPC occurrences = %#v, want independently evidenced Kesh registry grounding", npcOccurrences)
}
if itemRegistryOutput.File != "lanes/item-registry.json" || itemRegistryOutput.SchemaID != itemregistrycodec.SchemaID || itemRegistryOutput.SchemaVersion != itemregistrycodec.SchemaVersion {
t.Fatalf("item registry output = %#v, want typed registry JSON", itemRegistryOutput)
}
itemRegistry := readProductionJSON[dnd.ItemRegistry](t, filepath.Join(runRoot, itemRegistryOutput.File))
if len(itemRegistry.Items) != 1 || itemRegistry.Items[0].Name != "Moonblade" {
t.Fatalf("item registry = %#v, want Moonblade identity", itemRegistry)
}
if itemOccurrenceOutput.File != "lanes/item-occurrences.json" || itemOccurrenceOutput.SchemaID != itemoccurrencecodec.SchemaID || itemOccurrenceOutput.SchemaVersion != itemoccurrencecodec.SchemaVersion {
t.Fatalf("item occurrence output = %#v, want typed occurrence JSON", itemOccurrenceOutput)
}
itemOccurrences := readProductionJSON[dnd.ItemOccurrenceList](t, filepath.Join(runRoot, itemOccurrenceOutput.File))
if len(itemOccurrences.Occurrences) != 1 || itemOccurrences.Occurrences[0].ItemID != itemRegistry.Items[0].ID || itemOccurrences.Occurrences[0].Name != "Moonblade" || len(itemOccurrences.Occurrences[0].SourceRefs) != 1 || itemOccurrences.Occurrences[0].SourceRefs[0].StartUnitID != 5 {
t.Fatalf("item occurrences = %#v, want independently evidenced Moonblade registry grounding", itemOccurrences)
}
evidence := readProductionJSON[evidencecontext.Document](t, filepath.Join(runRoot, "evidence-context.json"))
for _, laneID := range []string{"enemy-events", "locations", "location-occurrences"} {
for _, laneID := range []string{"enemy-events", "npc-registry", "npc-occurrences", "item-registry", "item-occurrences", "location-registry", "location-occurrences"} {
if !containsString(evidence.SelectedLanes, laneID) || !evidenceHasLane(evidence, laneID) {
t.Fatalf("evidence context = %#v, want direct %s evidence", evidence, laneID)
}
@@ -192,6 +223,25 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
t.Fatalf("location occurrence registry input = %q, want source-free ID grounding", registryInput.Content)
}
}
for _, test := range []struct {
promptID string
slot string
name string
}{
{promptID: npcoccurrences.PromptID, slot: "npc_registry", name: "Kesh"},
{promptID: itemoccurrences.PromptID, slot: "item_registry", name: "Moonblade"},
} {
requests := client.requestsFor(test.promptID)
if len(requests) != 2 {
t.Fatalf("%s requests = %#v, want one request per scene", test.promptID, requests)
}
for _, request := range requests {
registryInput := request.Inputs[test.slot]
if !strings.Contains(string(registryInput.Content), test.name) || !strings.Contains(string(registryInput.Content), `"id"`) || strings.Contains(string(registryInput.Content), "source_refs") {
t.Fatalf("%s registry input = %q, want source-free ID grounding", test.promptID, registryInput.Content)
}
}
}
}
func completeExampleConfigWithTemporaryCache(t *testing.T) string {
@@ -256,11 +306,30 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
case spells.PromptID:
content = []byte(`{"spell_casts":[]}`)
case itemregistry.PromptID:
content = []byte(`{"items":[]}`)
if combatScene {
content = []byte(`{"items":[]}`)
} else {
content = []byte(`{"items":[{"name":"Moonblade","source_refs":[{"start_unit_id":5,"end_unit_id":5}]}]}`)
}
case itemregistrynormalize.PromptID:
content = []byte(`{"duplicate_groups":[]}`)
case itemoccurrences.PromptID:
content = []byte(`{"occurrences":[]}`)
if combatScene {
content = []byte(`{"occurrences":[]}`)
} else {
var registry struct {
Items []struct {
ID string `json:"id"`
} `json:"items"`
}
if err := json.Unmarshal(request.Inputs["item_registry"].Content, &registry); err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("decode generated item registry: %w", err)
}
if len(registry.Items) != 1 {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated item registry has %d items, want 1", len(registry.Items))
}
content = []byte(fmt.Sprintf(`{"occurrences":[{"item_id":%q,"name":"Moonblade","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[{"start_segment":5,"end_segment":5}]}]}`, registry.Items[0].ID))
}
case combat.PromptID:
content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`)
case npcoccurrences.PromptID:

View File

@@ -6,13 +6,24 @@ import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
locationregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npcregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
)
func TestProductionNPCOccurrencePipelineResolvesAndPrepares(t *testing.T) {
@@ -116,6 +127,77 @@ func TestProductionNPCOccurrenceReferencesRejectIncompatibleExternalRegistries(t
}
}
func TestProductionOccurrencePipelinesAcceptCompatibleExternalRegistries(t *testing.T) {
components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries)
reference := source.SourceRef{SourceID: "external-registry", StartUnitID: 1, EndUnitID: 1}
npcContent, err := npcregistrycodec.New().Encode(dnd.NPCRegistry{NPCs: []dnd.NPC{{
ID: npcidentity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{reference},
}}})
if err != nil {
t.Fatal(err)
}
locationContent, err := locationregistrycodec.New().Encode(dnd.LocationRegistry{Locations: []dnd.Location{{
ID: locationidentity.DeriveID("Moon Gate", []source.SourceRef{reference}), Name: "Moon Gate", SourceRefs: []source.SourceRef{reference},
}}})
if err != nil {
t.Fatal(err)
}
itemContent, err := itemregistrycodec.New().Encode(dnd.ItemRegistry{Items: []dnd.Item{{
ID: itemidentity.DeriveID("Moonblade"), Name: "Moonblade", SourceRefs: []source.SourceRef{reference},
}}})
if err != nil {
t.Fatal(err)
}
root := t.TempDir()
for _, test := range []struct {
name string
slot string
extractor string
normalizer string
content []byte
}{
{name: "NPC", slot: "npc_registry", extractor: occurrenceextract.Key, normalizer: occurrencenormalize.Key, content: npcContent},
{name: "location", slot: "location_registry", extractor: locationoccurrenceextract.Key, normalizer: locationoccurrencenormalize.Key, content: locationContent},
{name: "item", slot: "item_registry", extractor: itemoccurrenceextract.Key, normalizer: itemoccurrencenormalize.Key, content: itemContent},
} {
t.Run(test.name, func(t *testing.T) {
path := filepath.Join(root, strings.ToLower(test.name)+"-registry.json")
if err := os.WriteFile(path, test.content, 0o600); err != nil {
t.Fatal(err)
}
resolved, err := pipeline.ResolvePipeline(externalOccurrenceProfile(test.slot, test.extractor, test.normalizer, pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog)
if err != nil {
t.Fatalf("ResolvePipeline() error = %v", err)
}
materialized, warnings, err := pipeline.MaterializeReferences(resolved, catalog, pipeline.ReferenceMaterializationOptions{})
if err != nil || len(warnings) != 0 {
t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings)
}
if _, err := pipeline.Prepare(materialized, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
})
}
}
func externalOccurrenceProfile(slot, extractor, normalizer string, reference pipeline.ReferenceSource) pipeline.PipelineProfile {
return pipeline.PipelineProfile{
ID: "external-registry-occurrences",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
Output: pipeline.Binding("json"),
Steps: []pipeline.PipelineStepProfile{{
ID: "occurrences",
References: map[string]pipeline.ReferenceSource{slot: reference},
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"occurrences": {Extract: pipeline.Binding(extractor), Normalize: pipeline.Binding(normalizer)},
},
}},
}
}
func npcOccurrenceProfile(reference pipeline.ReferenceSource) pipeline.PipelineProfile {
profile := pipeline.PipelineProfile{
ID: "dnd-npc-occurrences",

View File

@@ -58,10 +58,10 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err)
}
if example.name == "complete" {
if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-registry,locations,npc_registry,scene-descriptions|extract-events:combat-turns,item-occurrences,location-occurrences,npc-occurrences,spells|track-enemies:enemy-events" {
if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-registry,location-registry,npc-registry,scene-descriptions|extract-events:combat-turns,item-occurrences,location-occurrences,npc-occurrences,spells|track-enemies:enemy-events" {
t.Fatalf("complete example steps and lanes = %v, want the documented D&D extractor composition", got)
}
locationLane := referenceContractLane(t, materialized, "locations")
locationLane := referenceContractLane(t, materialized, "location-registry")
if locationLane.ArtifactKind != dnd.LocationRegistryKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 {
t.Fatalf("location lane = %#v, want typed registry composition", locationLane)
}
@@ -71,7 +71,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
}
for _, target := range []pipeline.ResolvedReferenceTarget{occurrenceLane.ExtractReferences, occurrenceLane.NormalizeReferences} {
binding, found := generatedReferenceBinding(target.Bindings, "location_registry")
if !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "locations" {
if !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "location-registry" {
t.Fatalf("location occurrence %s reference = %#v, want generated location registry", target.Stage, binding)
}
}
@@ -99,7 +99,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
}
enemyEventLane := referenceContractLane(t, materialized, "enemy-events")
for slot, want := range map[string]struct{ step, lane string }{
"npc_registry": {step: "describe-session", lane: "npc_registry"},
"npc_registry": {step: "describe-session", lane: "npc-registry"},
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
"combat_turns": {step: "extract-events", lane: "combat-turns"},
"npc_occurrences": {step: "extract-events", lane: "npc-occurrences"},

View File

@@ -30,16 +30,32 @@ import (
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents"
itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences"
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
npcoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
scenecodec "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"
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
npcoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
sceneextract "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"
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
npcoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
scenenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
"gitea.maximumdirect.net/eric/promptkit"
@@ -51,9 +67,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
assertProductionContains(t, "inputs", registries.Inputs.RegisteredKeys(), []string{"seriatim"})
assertProductionContains(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes", "generic"})
assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", "dnd/npc-registry", combatextract.Key, itemoccurrenceextract.Key, enemyeventextract.Key})
assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{spells.Key, npcextract.Key, combatextract.Key, itemoccurrenceextract.Key, itemregistryextract.Key, npcoccurrenceextract.Key, sceneextract.Key, locationextract.Key, locationoccurrenceextract.Key, enemyeventextract.Key})
assertProductionContains(t, "mergers", registries.Mergers.RegisteredKeys(), []string{"appendorder"})
assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{"noop", spellnormalize.Key, "dnd/npc-registry", combatnormalize.Key, itemoccurrencenormalize.Key, enemyeventnormalize.Key})
assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{pipeline.DefaultNormalizeModule, spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, itemoccurrencenormalize.Key, itemregistrynormalize.Key, npcoccurrencenormalize.Key, scenenormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, enemyeventnormalize.Key})
assertProductionContains(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"})
assertProductionContains(t, "validators", registries.Validators.RegisteredKeys(), []string{
"extract/dnd/spells/catalog",
@@ -65,9 +81,36 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
"extract/dnd/combat-turns/source_relatedness",
"normalize/dnd/combat-turns/invariants",
"extract/dnd/item-occurrences/shape",
"extract/dnd/item-occurrences/registry",
"extract/dnd/item-occurrences/source_refs",
"extract/dnd/item-occurrences/source_relatedness",
"normalize/dnd/item-occurrences/invariants",
"extract/dnd/item-registry/shape",
"extract/dnd/item-registry/source_refs",
"extract/dnd/item-registry/source_relatedness",
"normalize/dnd/item-registry/identity",
"extract/dnd/npc-registry/shape",
"extract/dnd/npc-registry/source_refs",
"extract/dnd/npc-registry/source_relatedness",
"normalize/dnd/npc-registry/identity",
"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",
"normalize/dnd/scene-descriptions/invariants",
"extract/dnd/location-registry/shape",
"extract/dnd/location-registry/source_refs",
"extract/dnd/location-registry/source_relatedness",
"normalize/dnd/location-registry/identity",
"extract/dnd/location-occurrences/shape",
"extract/dnd/location-occurrences/registry",
"extract/dnd/location-occurrences/source_refs",
"extract/dnd/location-occurrences/source_relatedness",
"normalize/dnd/location-occurrences/invariants",
"extract/dnd/enemy-events/shape",
"extract/dnd/enemy-events/engagements",
"extract/dnd/enemy-events/source_refs",
@@ -78,12 +121,19 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
"generic/valid_json",
"generic/valid_json_schema",
})
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemOccurrenceListKind, dnd.EnemyEventListKind})
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemOccurrenceListKind, dnd.EnemyEventListKind})
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemOccurrenceListKind, dnd.EnemyEventListKind})
allArtifactKinds := []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.NPCOccurrenceListKind, dnd.CombatTurnListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind, dnd.ItemRegistryKind, dnd.ItemOccurrenceListKind, dnd.EnemyEventListKind}
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), allArtifactKinds)
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), allArtifactKinds)
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), allArtifactKinds)
assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind})
assertProductionContains(t, "combat normalizer variants", registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
assertProductionContains(t, "item occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind})
assertProductionContains(t, "item registry normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemregistrynormalize.Key), []contracts.ArtifactKind{dnd.ItemRegistryKind})
assertProductionContains(t, "NPC registry normalizer variants", registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind})
assertProductionContains(t, "NPC occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(npcoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind})
assertProductionContains(t, "scene description normalizer variants", registries.Normalizers.RegisteredArtifactKinds(scenenormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind})
assertProductionContains(t, "location registry normalizer variants", registries.Normalizers.RegisteredArtifactKinds(locationnormalize.Key), []contracts.ArtifactKind{dnd.LocationRegistryKind})
assertProductionContains(t, "location occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(locationoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.LocationOccurrenceListKind})
assertProductionContains(t, "enemy event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
wantChain := []pipeline.ModuleBinding{
@@ -167,18 +217,24 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
{stage: pipeline.StageChunk, key: "dnd/scenes", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/spells", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/location-registry", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/item-registry", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/combat-turns", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/item-occurrences", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/npc-occurrences", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/location-occurrences", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: "dnd/scene-descriptions", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageExtract, key: enemyeventextract.Key, want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageMerge, key: "appendorder", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "noop", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "dnd/spells", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageNormalize, key: "dnd/location-registry", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageNormalize, key: "dnd/item-registry", want: contracts.ExecutionClassLLMBacked},
{stage: pipeline.StageNormalize, key: "dnd/combat-turns", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "dnd/item-occurrences", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "dnd/npc-occurrences", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "dnd/location-occurrences", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: "dnd/scene-descriptions", want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageNormalize, key: enemyeventnormalize.Key, want: contracts.ExecutionClassDeterministic},
{stage: pipeline.StageOutput, key: "json", want: contracts.ExecutionClassDeterministic},
@@ -204,6 +260,23 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
if !ok || itemOccurrenceCodecSpec.Kind != dnd.ItemOccurrenceListKind || itemOccurrenceCodecSpec.Schema.ID != itemoccurrencecodec.SchemaID {
t.Fatalf("item occurrence codec spec = %#v, ok=%t, want typed D&D item-occurrence codec", itemOccurrenceCodecSpec, ok)
}
for _, test := range []struct {
name string
kind contracts.ArtifactKind
id string
}{
{name: "NPC registry", kind: dnd.NPCRegistryKind, id: npccodec.SchemaID},
{name: "NPC occurrences", kind: dnd.NPCOccurrenceListKind, id: npcoccurrencecodec.SchemaID},
{name: "item registry", kind: dnd.ItemRegistryKind, id: itemregistrycodec.SchemaID},
{name: "scene descriptions", kind: dnd.SceneDescriptionListKind, id: scenecodec.SchemaID},
{name: "location registry", kind: dnd.LocationRegistryKind, id: locationcodec.SchemaID},
{name: "location occurrences", kind: dnd.LocationOccurrenceListKind, id: locationoccurrencecodec.SchemaID},
} {
spec, found := catalog.ArtifactCodecs.Spec(test.kind)
if !found || spec.Kind != test.kind || spec.Schema.ID != test.id {
t.Fatalf("%s codec spec = %#v, found=%t, want typed D&D codec", test.name, spec, found)
}
}
enemyEventCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.EnemyEventListKind)
if !ok || enemyEventCodecSpec.Kind != dnd.EnemyEventListKind || enemyEventCodecSpec.Schema.ID != enemyeventcodec.SchemaID {
t.Fatalf("enemy event codec spec = %#v, ok=%t, want typed D&D enemy-event codec", enemyEventCodecSpec, ok)