Centralize D&D source reference ordering

This commit is contained in:
2026-07-24 14:20:10 +00:00
parent 6e21c83fd8
commit 1ff449435f
16 changed files with 363 additions and 460 deletions

View File

@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
@@ -14,12 +13,13 @@ 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/npcs/identity"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
)
const (
Key = "dnd/npcs"
normalizationPolicy = "dnd.npcs.normalize.v1"
normalizationPolicy = "dnd.npcs.normalize.v2"
NormalizationPolicy = normalizationPolicy
ReasonCodeNPCFieldsNormalized = "npc_fields_normalized"
@@ -69,7 +69,8 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
if err := ctx.Err(); err != nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("context error before normalize: %w", err)
}
value, warnings := normalizeList(req.MergeOutput.Value)
order := shared.NewSourceRefOrder(req.Source)
value, warnings := normalizeList(req.MergeOutput.Value, order)
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: value, Warnings: warnings}, nil
}
@@ -77,14 +78,14 @@ type normalizedRecord struct {
npc dnd.NPC
}
func normalizeList(input dnd.NPCList) (dnd.NPCList, []contracts.Warning) {
func normalizeList(input dnd.NPCList, order shared.SourceRefOrder) (dnd.NPCList, []contracts.Warning) {
if input.NPCs == nil {
return dnd.NPCList{}, nil
}
records := make([]normalizedRecord, len(input.NPCs))
warnings := make([]contracts.Warning, 0)
for index, inputNPC := range input.NPCs {
npc, fieldsChanged, referencesChanged := normalizeRecord(inputNPC)
npc, fieldsChanged, referencesChanged := normalizeRecord(inputNPC, order)
records[index] = normalizedRecord{npc: npc}
if fieldsChanged {
warnings = append(warnings, contracts.Warning{Scope: npcScope(index), ReasonCode: ReasonCodeNPCFieldsNormalized, Message: fmt.Sprintf("input index %d: NPC name normalized for %s", index, diagnostics.Quote(inputNPC.Name))})
@@ -100,7 +101,7 @@ func normalizeList(input dnd.NPCList) (dnd.NPCList, []contracts.Warning) {
groups := canonicalNameGroups(records)
output := dnd.NPCList{NPCs: make([]dnd.NPC, 0, len(groups))}
for _, members := range groups {
consolidated, referencesChanged := consolidate(records, members)
consolidated, referencesChanged := consolidate(records, members, order)
retainedIndex := members[0]
output.NPCs = append(output.NPCs, consolidated)
if referencesChanged {
@@ -113,10 +114,10 @@ func normalizeList(input dnd.NPCList) (dnd.NPCList, []contracts.Warning) {
return output, warnings
}
func normalizeRecord(input dnd.NPC) (dnd.NPC, bool, bool) {
func normalizeRecord(input dnd.NPC, order shared.SourceRefOrder) (dnd.NPC, bool, bool) {
output := cloneNPC(input)
output.Name = identity.NormalizeDisplay(input.Name)
output.SourceRefs, _, _ = canonicalizeSourceRefs(input.SourceRefs)
output.SourceRefs, _, _ = canonicalizeSourceRefs(order, input.SourceRefs)
output.ID = identity.DeriveID(output.Name)
return output, input.Name != output.Name, !reflect.DeepEqual(input.SourceRefs, output.SourceRefs)
}
@@ -143,39 +144,20 @@ func canonicalNameGroups(records []normalizedRecord) [][]int {
return groups
}
func consolidate(records []normalizedRecord, members []int) (dnd.NPC, bool) {
func consolidate(records []normalizedRecord, members []int, order shared.SourceRefOrder) (dnd.NPC, bool) {
output := cloneNPC(records[members[0]].npc)
originalRefs := cloneSourceRefs(output.SourceRefs)
for _, member := range members[1:] {
output.SourceRefs = append(output.SourceRefs, records[member].npc.SourceRefs...)
}
output.SourceRefs, _, _ = canonicalizeSourceRefs(output.SourceRefs)
output.SourceRefs, _, _ = canonicalizeSourceRefs(order, output.SourceRefs)
output.ID = identity.DeriveID(output.Name)
return output, !reflect.DeepEqual(originalRefs, output.SourceRefs)
}
func canonicalizeSourceRefs(input []source.SourceRef) ([]source.SourceRef, bool, int) {
if input == nil {
return nil, false, 0
}
canonical := cloneSourceRefs(input)
sort.SliceStable(canonical, func(left, right int) bool {
if canonical[left].SourceID != canonical[right].SourceID {
return canonical[left].SourceID < canonical[right].SourceID
}
if canonical[left].StartUnitID != canonical[right].StartUnitID {
return canonical[left].StartUnitID < canonical[right].StartUnitID
}
return canonical[left].EndUnitID < canonical[right].EndUnitID
})
orderChanged := !reflect.DeepEqual(input, canonical)
unique := make([]source.SourceRef, 0, len(canonical))
for _, ref := range canonical {
if len(unique) == 0 || unique[len(unique)-1] != ref {
unique = append(unique, ref)
}
}
return unique, orderChanged, len(input) - len(unique)
func canonicalizeSourceRefs(order shared.SourceRefOrder, input []source.SourceRef) ([]source.SourceRef, bool, int) {
canonical := order.Canonicalize(input)
return canonical, !reflect.DeepEqual(input, canonical), len(input) - len(canonical)
}
func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {