Improve D&D registry normalization efficiency

This commit is contained in:
2026-08-09 02:26:49 +00:00
parent 2a75f40871
commit b70d9f77e3
5 changed files with 93 additions and 49 deletions

View File

@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"sort"
"strings" "strings"
"gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/core/source"
@@ -58,9 +57,13 @@ func ComparisonKey(value string) string {
// available, leaving validation to report the problem instead of manufacturing // available, leaving validation to report the problem instead of manufacturing
// an ID. // an ID.
func DeriveID(name string, refs []source.SourceRef) string { func DeriveID(name string, refs []source.SourceRef) string {
comparisonName := ComparisonKey(name)
anchor, ok := earliestReference(refs) anchor, ok := earliestReference(refs)
if comparisonName == "" || !ok { return deriveIDFromAnchor(name, anchor, ok)
}
func deriveIDFromAnchor(name string, anchor source.SourceRef, hasAnchor bool) string {
comparisonName := ComparisonKey(name)
if comparisonName == "" || !hasAnchor {
return "" return ""
} }
@@ -111,13 +114,14 @@ func ValidateRegistry(locations []dnd.Location) []Issue {
if ComparisonKey(location.Name) == "" { if ComparisonKey(location.Name) == "" {
issues = append(issues, Issue{Code: IssueEmptyCanonicalName, RecordIndex: recordIndex, Value: location.Name}) issues = append(issues, Issue{Code: IssueEmptyCanonicalName, RecordIndex: recordIndex, Value: location.Name})
} }
if _, ok := earliestReference(location.SourceRefs); !ok { anchor, hasAnchor := earliestReference(location.SourceRefs)
if !hasAnchor {
issues = append(issues, Issue{Code: IssueMissingEvidence, RecordIndex: recordIndex}) issues = append(issues, Issue{Code: IssueMissingEvidence, RecordIndex: recordIndex})
} }
if !IsValidID(location.ID) { if !IsValidID(location.ID) {
issues = append(issues, Issue{Code: IssueInvalidID, RecordIndex: recordIndex, Value: location.ID}) issues = append(issues, Issue{Code: IssueInvalidID, RecordIndex: recordIndex, Value: location.ID})
} else if expected := DeriveID(location.Name, location.SourceRefs); location.ID != expected { } else if expected := deriveIDFromAnchor(location.Name, anchor, hasAnchor); location.ID != expected {
issues = append(issues, Issue{Code: IssueIDMismatch, RecordIndex: recordIndex, Value: location.ID}) issues = append(issues, Issue{Code: IssueIDMismatch, RecordIndex: recordIndex, Value: location.ID})
} }
if location.ID != "" { if location.ID != "" {
@@ -135,36 +139,28 @@ func ValidateRegistry(locations []dnd.Location) []Issue {
} }
func earliestReference(refs []source.SourceRef) (source.SourceRef, bool) { func earliestReference(refs []source.SourceRef) (source.SourceRef, bool) {
canonical := canonicalReferences(refs) var earliest source.SourceRef
if len(canonical) == 0 { found := false
return source.SourceRef{}, false for _, ref := range refs {
if !validIdentityReference(ref) {
continue
}
if !found || referenceLess(ref, earliest) {
earliest = ref
found = true
}
} }
return canonical[0], true return earliest, found
} }
func canonicalReferences(refs []source.SourceRef) []source.SourceRef { func referenceLess(left, right source.SourceRef) bool {
canonical := make([]source.SourceRef, 0, len(refs)) if left.SourceID != right.SourceID {
for _, ref := range refs { return left.SourceID < right.SourceID
if validIdentityReference(ref) {
canonical = append(canonical, ref)
}
} }
sort.Slice(canonical, func(left, right int) bool { if left.StartUnitID != right.StartUnitID {
if canonical[left].SourceID != canonical[right].SourceID { return left.StartUnitID < right.StartUnitID
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
})
unique := canonical[:0]
for _, ref := range canonical {
if len(unique) == 0 || unique[len(unique)-1] != ref {
unique = append(unique, ref)
}
} }
return unique return left.EndUnitID < right.EndUnitID
} }
func validIdentityReference(ref source.SourceRef) bool { func validIdentityReference(ref source.SourceRef) bool {

View File

@@ -241,20 +241,15 @@ func normalizeRecord(input dnd.Item, order shared.SourceRefOrder) (dnd.Item, boo
func comparisonNameGroups(records []normalizedRecord) [][]int { func comparisonNameGroups(records []normalizedRecord) [][]int {
groups := make([][]int, 0, len(records)) groups := make([][]int, 0, len(records))
groupPositions := make(map[string]int, len(records))
for index, record := range records { for index, record := range records {
key := identity.ComparisonKey(record.item.Name) key := identity.ComparisonKey(record.item.Name)
found := false if groupIndex, found := groupPositions[key]; found {
for groupIndex, members := range groups { groups[groupIndex] = append(groups[groupIndex], index)
first := records[members[0]] continue
if identity.ComparisonKey(first.item.Name) == key {
groups[groupIndex] = append(groups[groupIndex], index)
found = true
break
}
}
if !found {
groups = append(groups, []int{index})
} }
groupPositions[key] = len(groups)
groups = append(groups, []int{index})
} }
return groups return groups
} }

View File

@@ -85,6 +85,20 @@ func TestNormalizeConsolidatesEqualNamesAcrossEvidenceWithoutMutation(t *testing
} }
} }
func BenchmarkComparisonNameGroupsManyDistinct(b *testing.B) {
records := make([]normalizedRecord, 1_000)
for index := range records {
records[index] = normalizedRecord{item: dnd.Item{Name: "Item " + strconv.Itoa(index)}}
}
b.ReportAllocs()
b.ResetTimer()
for iteration := 0; iteration < b.N; iteration++ {
if groups := comparisonNameGroups(records); len(groups) != len(records) {
b.Fatalf("group count = %d, want %d", len(groups), len(records))
}
}
}
func TestNormalizeAppliesSafeAliasProposal(t *testing.T) { func TestNormalizeAppliesSafeAliasProposal(t *testing.T) {
doc := semanticDocument() doc := semanticDocument()
input := dnd.ItemRegistry{Items: []dnd.Item{ input := dnd.ItemRegistry{Items: []dnd.Item{

View File

@@ -3,6 +3,7 @@ package locationregistry
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
@@ -228,24 +229,44 @@ func normalizeRecord(input dnd.Location, order shared.SourceRefOrder) (dnd.Locat
func exactDuplicateGroups(records []normalizedRecord) [][]int { func exactDuplicateGroups(records []normalizedRecord) [][]int {
groups := make([][]int, 0, len(records)) groups := make([][]int, 0, len(records))
groupPositions := make(map[string][]int, len(records))
for index, record := range records { for index, record := range records {
key := identity.ComparisonKey(record.location.Name) key := exactDuplicateKey(record.location)
found := false comparisonName := identity.ComparisonKey(record.location.Name)
for groupIndex, members := range groups { matched := false
first := records[members[0]] for _, groupIndex := range groupPositions[key] {
if identity.ComparisonKey(first.location.Name) == key && reflect.DeepEqual(first.location.SourceRefs, record.location.SourceRefs) { first := records[groups[groupIndex][0]]
if identity.ComparisonKey(first.location.Name) == comparisonName && reflect.DeepEqual(first.location.SourceRefs, record.location.SourceRefs) {
groups[groupIndex] = append(groups[groupIndex], index) groups[groupIndex] = append(groups[groupIndex], index)
found = true matched = true
break break
} }
} }
if !found { if matched {
groups = append(groups, []int{index}) continue
} }
groupPositions[key] = append(groupPositions[key], len(groups))
groups = append(groups, []int{index})
} }
return groups return groups
} }
type duplicateKey struct {
ComparisonName string `json:"comparison_name"`
SourceRefs []source.SourceRef `json:"source_refs"`
}
func exactDuplicateKey(location dnd.Location) string {
encoded, err := json.Marshal(duplicateKey{
ComparisonName: identity.ComparisonKey(location.Name),
SourceRefs: location.SourceRefs,
})
if err != nil {
panic("encode location duplicate key")
}
return string(encoded)
}
func cloneLocation(input dnd.Location) dnd.Location { func cloneLocation(input dnd.Location) dnd.Location {
input.SourceRefs = cloneSourceRefs(input.SourceRefs) input.SourceRefs = cloneSourceRefs(input.SourceRefs)
return input return input

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"reflect" "reflect"
"strconv"
"strings" "strings"
"testing" "testing"
@@ -60,6 +61,23 @@ func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t
} }
} }
func BenchmarkExactDuplicateGroupsManyDistinct(b *testing.B) {
records := make([]normalizedRecord, 1_000)
for index := range records {
records[index] = normalizedRecord{location: dnd.Location{
Name: "Location " + strconv.Itoa(index),
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: index + 1, EndUnitID: index + 1}},
}}
}
b.ReportAllocs()
b.ResetTimer()
for iteration := 0; iteration < b.N; iteration++ {
if groups := exactDuplicateGroups(records); len(groups) != len(records) {
b.Fatalf("group count = %d, want %d", len(groups), len(records))
}
}
}
func TestNormalizeAppliesSafeAliasGroupAndUsesContextualInputs(t *testing.T) { func TestNormalizeAppliesSafeAliasGroupAndUsesContextualInputs(t *testing.T) {
client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`} client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
doc := semanticDocument() doc := semanticDocument()