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

@@ -1,5 +1,5 @@
// Package npcinteractions normalizes merged D&D NPC interaction candidates.
package npcinteractions
// Package npcoccurrences normalizes merged D&D NPC occurrence candidates.
package npcoccurrences
import (
"context"
@@ -10,21 +10,21 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcinteractions"
occurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcoccurrences"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
)
const (
Key = "dnd/npc-interactions"
normalizationPolicy = "dnd.npc_interactions.normalize.v2"
Key = "dnd/npc-occurrences"
normalizationPolicy = "dnd.npc_occurrences.normalize.v2"
NormalizationPolicy = normalizationPolicy
ReasonCodeSourceRefsNormalized = "source_references_normalized"
ReasonCodeInteractionsReordered = "npc_interactions_reordered"
ReasonCodeDuplicateCollapsed = "duplicate_npc_interaction_collapsed"
ReasonCodeWarningsOmitted = "npc_interaction_normalization_warnings_omitted"
ReasonCodeSourceRefsNormalized = "source_references_normalized"
ReasonCodeOccurrencesReordered = "npc_occurrences_reordered"
ReasonCodeDuplicateCollapsed = "duplicate_npc_occurrence_collapsed"
ReasonCodeWarningsOmitted = "npc_occurrence_normalization_warnings_omitted"
)
const (
@@ -36,10 +36,10 @@ var requiredCapabilities = []string{"merged"}
var providedCapabilities = []string{"normalized"}
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
Glossary: "Optional campaign glossary reference material used only for interaction disambiguation.",
Party: "Optional party roster reference material used only for interaction disambiguation.",
Players: "Optional player list reference material used only for interaction disambiguation.",
Roster: "Deprecated alias for party roster reference material used only for interaction disambiguation.",
Glossary: "Optional campaign glossary reference material used only for occurrence disambiguation.",
Party: "Optional party roster reference material used only for occurrence disambiguation.",
Players: "Optional player list reference material used only for occurrence disambiguation.",
Roster: "Deprecated alias for party roster reference material used only for occurrence disambiguation.",
}
var _ contracts.Normalizer[dnd.NPCOccurrenceList] = (*Normalizer)(nil)
@@ -152,7 +152,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
}
sort.SliceStable(records, func(left, right int) bool {
return interactionmodel.Less(order, records[left].occurrence, records[right].occurrence)
return occurrencemodel.Less(order, records[left].occurrence, records[right].occurrence)
})
for position, record := range records {
if position == record.inputIndex {
@@ -160,7 +160,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
}
warnings = append(warnings, contracts.Warning{
Scope: occurrenceScope(record.inputIndex),
ReasonCode: ReasonCodeInteractionsReordered,
ReasonCode: ReasonCodeOccurrencesReordered,
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology", record.inputIndex, position),
})
}
@@ -168,7 +168,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
output, duplicateWarnings := collapseDuplicates(records, documentIndex)
warnings = append(warnings, duplicateWarnings...)
return dnd.NPCOccurrenceList{Occurrences: output},
diagnostics.LimitWarnings(warnings, "npc_interactions", ReasonCodeWarningsOmitted), nil
diagnostics.LimitWarnings(warnings, "npc_occurrences", ReasonCodeWarningsOmitted), nil
}
func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrence, bool, error) {
@@ -181,7 +181,7 @@ func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, r
}
output := cloneOccurrence(input)
output.SourceRefs = order.Canonicalize(input.SourceRefs)
return output, !interactionmodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs), nil
return output, !occurrencemodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs), nil
}
func cloneOccurrence(input dnd.NPCOccurrence) dnd.NPCOccurrence {
@@ -205,11 +205,11 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
groups := make([]duplicateGroup, 0)
groupByKey := make(map[string]int)
for index, record := range records {
if !interactionmodel.ValidSourceRefs(documentIndex, record.occurrence.SourceRefs) {
if !occurrencemodel.ValidSourceRefs(documentIndex, record.occurrence.SourceRefs) {
keep[index] = true
continue
}
key := interactionmodel.ExactIdentity(record.occurrence)
key := occurrencemodel.ExactIdentity(record.occurrence)
groupIndex, exists := groupByKey[key]
if !exists {
groupByKey[key] = len(groups)
@@ -243,7 +243,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
Scope: occurrenceScope(retainedIndex),
ReasonCode: ReasonCodeDuplicateCollapsed,
Message: diagnostics.Aggregate(
fmt.Sprintf("duplicate NPC interaction collapsed; retained input index %d", retainedIndex), issues),
fmt.Sprintf("duplicate NPC occurrence collapsed; retained input index %d", retainedIndex), issues),
}
}
@@ -253,7 +253,7 @@ func referenceSlots() []contracts.ReferenceSlot {
slots := shared.ReferenceSlots(referenceSlotDescriptions)
slots = append(slots, contracts.ReferenceSlot{
Name: NPCRegistryReferenceSlot,
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.",
Description: "Required normalized NPC registry used only for occurrence identity grounding, never as occurrence evidence.",
Required: true,
AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
@@ -295,5 +295,5 @@ func DecodeOptions(options map[string]any) (Options, error) {
func validateOptions(options map[string]any) error { _, err := DecodeOptions(options); return err }
func normalizerErrorf(format string, args ...any) error {
return fmt.Errorf("dnd NPC interactions normalizer: "+format, args...)
return fmt.Errorf("dnd NPC occurrences normalizer: "+format, args...)
}

View File

@@ -1,4 +1,4 @@
package npcinteractions
package npcoccurrences
import (
"context"
@@ -31,7 +31,7 @@ func TestNormalizeValidatesPairsAndClones(t *testing.T) {
}
got := result.Value.Occurrences[0]
if got.NPCID != identity.DeriveID("Ária") || got.Name != "Ária" || !reflect.DeepEqual(got.SourceRefs, []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}) {
t.Fatalf("normalized interaction = %#v", got)
t.Fatalf("normalized occurrence = %#v", got)
}
if !hasWarning(result.Warnings, ReasonCodeSourceRefsNormalized) {
t.Fatalf("warnings = %#v", result.Warnings)
@@ -77,7 +77,7 @@ func TestNormalizeRejectsUnknownIDsAndMismatchedNames(t *testing.T) {
t.Fatal(err)
}
for _, occurrence := range []dnd.NPCOccurrence{
interaction("Unknown NPC", dnd.NPCOccurrenceKindOther, source.SourceRef{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}),
occurrence("Unknown NPC", dnd.NPCOccurrenceKindOther, source.SourceRef{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}),
{NPCID: identity.DeriveID("Ária"), Name: "Borin", Kind: dnd.NPCOccurrenceKindOther, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
} {
input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{occurrence}}
@@ -92,14 +92,14 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) {
ref := func(unit int) source.SourceRef {
return source.SourceRef{SourceID: doc.ID, StartUnitID: unit, EndUnitID: unit}
}
first := interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(50))
first := occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(50))
input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{
interaction("Borin", dnd.NPCOccurrenceKindMentioned, ref(90)),
occurrence("Borin", dnd.NPCOccurrenceKindMentioned, ref(90)),
first,
first,
interaction("Ária", dnd.NPCOccurrenceKindCombatAlly, ref(50)),
interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(10)),
interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(999)),
occurrence("Ária", dnd.NPCOccurrenceKindCombatAlly, ref(50)),
occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(10)),
occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(999)),
}}
normalizer, err := New(Options{}, npcReferences(t))
if err != nil {
@@ -111,12 +111,12 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) {
}
got := result.Value.Occurrences
if len(got) != 5 {
t.Fatalf("interaction count = %d, want 5: %#v", len(got), got)
t.Fatalf("occurrence count = %d, want 5: %#v", len(got), got)
}
if got[0].Kind != dnd.NPCOccurrenceKindCombatAlly || got[0].SourceRefs[0].StartUnitID != 50 || got[1].SourceRefs[0].StartUnitID != 50 || got[2].SourceRefs[0].StartUnitID != 10 || got[3].SourceRefs[0].StartUnitID != 90 || got[4].SourceRefs[0].StartUnitID != 999 {
t.Fatalf("canonical order = %#v", got)
}
if !hasWarning(result.Warnings, ReasonCodeInteractionsReordered) || !hasWarning(result.Warnings, ReasonCodeDuplicateCollapsed) {
if !hasWarning(result.Warnings, ReasonCodeOccurrencesReordered) || !hasWarning(result.Warnings, ReasonCodeDuplicateCollapsed) {
t.Fatalf("warnings = %#v", result.Warnings)
}
}
@@ -147,7 +147,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) {
for index := range doc.Units {
doc.Units[index].ID = index + 1
unitID := count - index
input.Occurrences[index] = interaction(
input.Occurrences[index] = occurrence(
"Ária",
dnd.NPCOccurrenceKindDialogue,
source.SourceRef{SourceID: doc.ID, StartUnitID: unitID, EndUnitID: unitID},
@@ -169,7 +169,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) {
}
}
func interaction(name string, kind dnd.NPCOccurrenceKind, ref source.SourceRef) dnd.NPCOccurrence {
func occurrence(name string, kind dnd.NPCOccurrenceKind, ref source.SourceRef) dnd.NPCOccurrence {
return dnd.NPCOccurrence{NPCID: identity.DeriveID(name), Name: name, Kind: kind, SourceRefs: []source.SourceRef{ref}}
}