Index enemy event duplicate identities
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
package enemyevents
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
@@ -58,9 +60,49 @@ func SourceRefsEqual(order shared.SourceRefOrder, left, right []source.SourceRef
|
||||
// ExactEqual reports whether events have the same subject identity, kind, and
|
||||
// complete canonical evidence sequence.
|
||||
func ExactEqual(order shared.SourceRefOrder, left, right dnd.EnemyEvent) bool {
|
||||
left.SourceRefs = order.Canonicalize(left.SourceRefs)
|
||||
right.SourceRefs = order.Canonicalize(right.SourceRefs)
|
||||
return CanonicalExactEqual(left, right)
|
||||
}
|
||||
|
||||
type canonicalIdentity struct {
|
||||
Subject string `json:"subject"`
|
||||
Kind dnd.EnemyEventKind `json:"kind"`
|
||||
SourceRefs []source.SourceRef `json:"source_refs"`
|
||||
}
|
||||
|
||||
// CanonicalIdentity returns the complete duplicate-lookup key for an event
|
||||
// whose source references are already canonical.
|
||||
func CanonicalIdentity(event dnd.EnemyEvent) string {
|
||||
encoded, err := json.Marshal(canonicalIdentity{
|
||||
Subject: ComparisonKey(event.Name),
|
||||
Kind: event.Kind,
|
||||
SourceRefs: event.SourceRefs,
|
||||
})
|
||||
if err != nil {
|
||||
panic("encode enemy event identity")
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
// CanonicalExactEqual reports whether events have the same complete duplicate
|
||||
// identity without modifying or canonicalizing their source references.
|
||||
func CanonicalExactEqual(left, right dnd.EnemyEvent) bool {
|
||||
return ComparisonKey(left.Name) == ComparisonKey(right.Name) &&
|
||||
left.Kind == right.Kind &&
|
||||
SourceRefsEqual(order, left.SourceRefs, right.SourceRefs)
|
||||
canonicalSourceRefsEqual(left.SourceRefs, right.SourceRefs)
|
||||
}
|
||||
|
||||
func canonicalSourceRefsEqual(left, right []source.SourceRef) bool {
|
||||
if (left == nil) != (right == nil) || len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Less defines the canonical stable event order. Invalid source references
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{}}} {
|
||||
|
||||
@@ -112,8 +112,10 @@ func allSourceRefsValid(index source.DocumentIndex, value dnd.EnemyEventList) bo
|
||||
|
||||
func issuesFor(order shared.SourceRefOrder, value dnd.EnemyEventList, registry *npcregistry.Registry) []string {
|
||||
issues := make([]string, 0)
|
||||
canonicalEvidence := make([]bool, len(value.Events))
|
||||
for eventIndex, event := range value.Events {
|
||||
prefix := fmt.Sprintf("events[%d]", eventIndex)
|
||||
canonicalEvidence[eventIndex] = true
|
||||
if normalized := enemyeventmodel.NormalizeDisplay(event.Name); event.Name != normalized {
|
||||
issues = append(issues, prefix+".name is not whitespace-normalized: "+diagnostics.Quote(event.Name))
|
||||
}
|
||||
@@ -125,8 +127,10 @@ func issuesFor(order shared.SourceRefOrder, value dnd.EnemyEventList, registry *
|
||||
current := event.SourceRefs[refIndex]
|
||||
if order.Less(current, previous) {
|
||||
issues = append(issues, fmt.Sprintf("%s.source_refs are not in canonical order at index %d", prefix, refIndex))
|
||||
canonicalEvidence[eventIndex] = false
|
||||
} else if current == previous {
|
||||
issues = append(issues, fmt.Sprintf("%s.source_refs[%d] duplicates the previous reference", prefix, refIndex))
|
||||
canonicalEvidence[eventIndex] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,13 +139,19 @@ func issuesFor(order shared.SourceRefOrder, value dnd.EnemyEventList, registry *
|
||||
}) {
|
||||
issues = append(issues, "events are not in canonical order")
|
||||
}
|
||||
firstByIdentity := make(map[string][]int, len(value.Events))
|
||||
for eventIndex, event := range value.Events {
|
||||
for previousIndex := 0; previousIndex < eventIndex; previousIndex++ {
|
||||
if enemyeventmodel.ExactEqual(order, value.Events[previousIndex], event) {
|
||||
if !canonicalEvidence[eventIndex] {
|
||||
continue
|
||||
}
|
||||
identity := enemyeventmodel.CanonicalIdentity(event)
|
||||
for _, previousIndex := range firstByIdentity[identity] {
|
||||
if enemyeventmodel.CanonicalExactEqual(value.Events[previousIndex], event) {
|
||||
issues = append(issues, fmt.Sprintf("events[%d] duplicates event %d", eventIndex, previousIndex))
|
||||
break
|
||||
}
|
||||
}
|
||||
firstByIdentity[identity] = append(firstByIdentity[identity], eventIndex)
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user