Narrow location occurrence normalizer references

This commit is contained in:
2026-08-04 13:12:46 +00:00
parent 55b188fd84
commit 5002864e88
4 changed files with 39 additions and 22 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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 {