Centralize D&D source reference ordering
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -61,6 +61,23 @@ func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
|
||||
doc := &source.SourceDocument{ID: "source", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
|
||||
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Lady Ash", SourceRefs: []source.SourceRef{
|
||||
{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
|
||||
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
|
||||
{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999},
|
||||
}}}}
|
||||
result, err := New(Options{}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Normalize() error = %v", err)
|
||||
}
|
||||
got := result.Value.NPCs[0].SourceRefs
|
||||
if got[0].StartUnitID != 30 || got[1].StartUnitID != 10 || got[2].StartUnitID != 999 {
|
||||
t.Fatalf("source refs = %#v, want source-document order followed by invalid reference", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T) {
|
||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
||||
{Name: " Captain Vale ", SourceRefs: []source.SourceRef{{SourceID: "a", StartUnitID: 1, EndUnitID: 1}}},
|
||||
@@ -115,6 +132,12 @@ func normalizeRequest(value dnd.NPCList) contracts.TypedNormalizeRequest[dnd.NPC
|
||||
return contracts.TypedNormalizeRequest[dnd.NPCList]{MergeOutput: contracts.MergeArtifact[dnd.NPCList]{Value: value}}
|
||||
}
|
||||
|
||||
func normalizeRequestWithSource(value dnd.NPCList, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.NPCList] {
|
||||
request := normalizeRequest(value)
|
||||
request.Source = doc
|
||||
return request
|
||||
}
|
||||
|
||||
func hasWarning(warnings []contracts.Warning, reason, scope string) bool {
|
||||
for _, warning := range warnings {
|
||||
if warning.ReasonCode == reason && warning.Scope == scope {
|
||||
|
||||
Reference in New Issue
Block a user