From 5002864e88e6a42307b52f75fb08fdabe82167ba Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 4 Aug 2026 13:12:46 +0000 Subject: [PATCH] Narrow location occurrence normalizer references --- internal/cli/example_contract_test.go | 8 ++++++ .../locationoccurrences/normalizer.go | 14 ++-------- .../locationoccurrences/normalizer_test.go | 28 +++++++++++++------ .../modules/dnd/register/register_test.go | 11 +++++++- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/internal/cli/example_contract_test.go b/internal/cli/example_contract_test.go index a95866b..aa0bf22 100644 --- a/internal/cli/example_contract_test.go +++ b/internal/cli/example_contract_test.go @@ -75,6 +75,14 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) { t.Fatalf("location occurrence %s reference = %#v, want generated location registry", target.Stage, binding) } } + for _, slot := range []string{"party", "glossary"} { + if len(occurrenceLane.ExtractReferences.ReferenceSet.Slots[slot].Items) != 1 { + t.Fatalf("location occurrence extractor %s reference was not materialized: %#v", slot, occurrenceLane.ExtractReferences) + } + if _, found := occurrenceLane.NormalizeReferences.ReferenceSet.Slots[slot]; found { + t.Fatalf("location occurrence normalizer unexpectedly consumes %s: %#v", slot, occurrenceLane.NormalizeReferences) + } + } spellLane := referenceContractLane(t, materialized, "spells") if len(spellLane.ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 || len(spellLane.NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 { diff --git a/internal/modules/dnd/normalize/locationoccurrences/normalizer.go b/internal/modules/dnd/normalize/locationoccurrences/normalizer.go index 12be344..d6a3e0f 100644 --- a/internal/modules/dnd/normalize/locationoccurrences/normalizer.go +++ b/internal/modules/dnd/normalize/locationoccurrences/normalizer.go @@ -38,13 +38,6 @@ const ( var requiredCapabilities = []string{"merged"} var providedCapabilities = []string{"normalized"} -var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ - Glossary: "Optional campaign glossary reference material used only for location-occurrence disambiguation.", - Party: "Optional party roster reference material used only for location-occurrence disambiguation.", - Players: "Optional player list reference material used only for location-occurrence disambiguation.", - Roster: "Deprecated alias for party roster reference material used only for location-occurrence disambiguation.", -} - var _ contracts.Normalizer[dnd.LocationOccurrenceList] = (*Normalizer)(nil) var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil) @@ -332,13 +325,10 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning { func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) } func referenceSlots() []contracts.ReferenceSlot { - slots := shared.ReferenceSlots(referenceSlotDescriptions) - slots = append(slots, contracts.ReferenceSlot{ + return []contracts.ReferenceSlot{{ 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 }) - return slots + }} } func ModuleSpec() pipeline.ModuleSpec { diff --git a/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go b/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go index 271068e..f41951d 100644 --- a/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go +++ b/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go @@ -97,25 +97,35 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) { t.Fatalf("New() error = %v", err) } normalizer := newNormalizer(t, registryReferences(t, registryLocations("The Mill"))) - if spec := ModuleSpec(); spec.Key != Key || spec.Stage != pipeline.StageNormalize || spec.ExecutionClass != contracts.ExecutionClassDeterministic || spec.ArtifactKind != dnd.LocationOccurrenceListKind { + spec := ModuleSpec() + if spec.Key != Key || spec.Stage != pipeline.StageNormalize || spec.ExecutionClass != contracts.ExecutionClassDeterministic || spec.ArtifactKind != dnd.LocationOccurrenceListKind { t.Fatalf("ModuleSpec() = %#v", spec) } - var locationSlot contracts.ReferenceSlot - for _, slot := range ModuleSpec().ReferenceSlots { - if slot.Name == LocationRegistryReferenceSlot { - locationSlot = slot - } + wantSlots := []contracts.ReferenceSlot{{ + 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, + }} + if !reflect.DeepEqual(spec.ReferenceSlots, wantSlots) { + t.Fatalf("ModuleSpec().ReferenceSlots = %#v, want %#v", spec.ReferenceSlots, wantSlots) } - if !locationSlot.Required || !reflect.DeepEqual(locationSlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationSlot.MaxBytes != LocationRegistryMaxBytes { - t.Fatalf("location slot = %#v", locationSlot) + if got := normalizer.ReferenceSlots(); !reflect.DeepEqual(got, wantSlots) { + t.Fatalf("ReferenceSlots() = %#v, want %#v", got, wantSlots) } registry := pipeline.NewNormalizerRegistry() if err := Register(registry); err != nil { t.Fatal(err) } - if _, ok := registry.Spec(Key); !ok { + registeredSpec, ok := registry.Spec(Key) + if !ok { t.Fatalf("registry missing %q", Key) } + if !reflect.DeepEqual(registeredSpec.ReferenceSlots, wantSlots) { + t.Fatalf("registered reference slots = %#v, want %#v", registeredSpec.ReferenceSlots, wantSlots) + } if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { t.Fatal("DecodeOptions() accepted unknown options") } diff --git a/internal/modules/dnd/register/register_test.go b/internal/modules/dnd/register/register_test.go index 805cb03..480570c 100644 --- a/internal/modules/dnd/register/register_test.go +++ b/internal/modules/dnd/register/register_test.go @@ -423,9 +423,18 @@ func TestRegisterAddsDNDFamily(t *testing.T) { } 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) { + 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) + } + if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationRegistrySlot.MaxBytes != 1048576 || !reflect.DeepEqual(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) + 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) + } + } 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 {