Index enemy event duplicate identities

This commit is contained in:
2026-08-09 02:31:28 +00:00
parent b70d9f77e3
commit b3ebfcef37
4 changed files with 79 additions and 9 deletions

View File

@@ -111,6 +111,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
type normalizedRecord struct {
event dnd.EnemyEvent
identity string
inputIndex int
}
@@ -127,7 +128,7 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
warnings := make([]contracts.Warning, 0)
for index, inputEvent := range input.Events {
event, nameChange, refsChanged := normalizeEvent(inputEvent, order, registry)
records[index] = normalizedRecord{event: event, inputIndex: index}
records[index] = normalizedRecord{event: event, identity: enemyeventmodel.CanonicalIdentity(event), inputIndex: index}
if nameChange != nil {
warnings = append(warnings, contracts.Warning{
Scope: eventScope(index),
@@ -160,7 +161,7 @@ func normalizeList(input dnd.EnemyEventList, order shared.SourceRefOrder, regist
})
}
output, duplicateWarnings := collapseDuplicates(records, order)
output, duplicateWarnings := collapseDuplicates(records)
warnings = append(warnings, duplicateWarnings...)
return dnd.EnemyEventList{Events: output}, diagnostics.LimitWarnings(warnings, "enemy_events", ReasonCodeWarningsOmitted)
}
@@ -204,23 +205,25 @@ type duplicateGroup struct {
removed []int
}
func collapseDuplicates(records []normalizedRecord, order shared.SourceRefOrder) ([]dnd.EnemyEvent, []contracts.Warning) {
func collapseDuplicates(records []normalizedRecord) ([]dnd.EnemyEvent, []contracts.Warning) {
if len(records) == 0 {
return make([]dnd.EnemyEvent, 0), nil
}
kept := make([]normalizedRecord, 0, len(records))
groups := make([]duplicateGroup, 0)
groupPositions := make(map[string][]int, len(records))
for _, record := range records {
match := -1
for index := range kept {
if enemyeventmodel.ExactEqual(order, kept[index].event, record.event) {
match = index
for _, groupIndex := range groupPositions[record.identity] {
if enemyeventmodel.CanonicalExactEqual(kept[groupIndex].event, record.event) {
match = groupIndex
break
}
}
if match < 0 {
kept = append(kept, record)
groups = append(groups, duplicateGroup{retainedIndex: record.inputIndex})
groupPositions[record.identity] = append(groupPositions[record.identity], len(kept)-1)
continue
}
groups[match].removed = append(groups[match].removed, record.inputIndex)

View File

@@ -83,6 +83,21 @@ func TestNormalizeUsesExplicitKindOrderAndPreservesDistinctObservations(t *testi
}
}
func TestNormalizePreservesDistinctEvidenceWhenEncodedKeysCoincide(t *testing.T) {
invalidSourceID := string([]byte{'s', 0xff})
replacementSourceID := "s\uFFFD"
document := &source.SourceDocument{ID: invalidSourceID, Units: []source.SourceUnit{{ID: 1}}}
input := dnd.EnemyEventList{Events: []dnd.EnemyEvent{
{Name: "Ashfang", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: invalidSourceID, StartUnitID: 1, EndUnitID: 1}}},
{Name: "Ashfang", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: replacementSourceID, StartUnitID: 1, EndUnitID: 1}}},
}}
result, err := newNormalizer(t, npcReferences(t)).Normalize(context.Background(), normalizeRequest(document, input, contracts.ReferenceSet{}))
if err != nil || len(result.Value.Events) != 2 || hasWarning(result.Warnings, ReasonCodeDuplicateCollapsed) {
t.Fatalf("Normalize() = %#v, %v; want distinct evidence observations retained", result, err)
}
}
func TestNormalizeIsIdempotentAndPreservesEmptyRepresentation(t *testing.T) {
normalizer := newNormalizer(t, npcReferences(t))
for _, input := range []dnd.EnemyEventList{{}, {Events: []dnd.EnemyEvent{}}} {