Tighten NPC interaction validation and consistency
This commit is contained in:
@@ -5,13 +5,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcinteractions"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||
@@ -20,13 +19,14 @@ import (
|
||||
|
||||
const (
|
||||
Key = "dnd/npc-interactions"
|
||||
normalizationPolicy = "dnd.npc_interactions.normalize.v1"
|
||||
normalizationPolicy = "dnd.npc_interactions.normalize.v2"
|
||||
NormalizationPolicy = normalizationPolicy
|
||||
|
||||
ReasonCodeNameCanonicalized = "npc_interaction_name_canonicalized"
|
||||
ReasonCodeSourceRefsNormalized = "source_references_normalized"
|
||||
ReasonCodeInteractionsReordered = "npc_interactions_reordered"
|
||||
ReasonCodeDuplicateCollapsed = "duplicate_npc_interaction_collapsed"
|
||||
ReasonCodeWarningsOmitted = "npc_interaction_normalization_warnings_omitted"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -125,8 +125,6 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
type normalizedRecord struct {
|
||||
interaction dnd.NPCInteraction
|
||||
inputIndex int
|
||||
earliest int
|
||||
hasEvidence bool
|
||||
}
|
||||
|
||||
type nameCanonicalization struct {
|
||||
@@ -143,8 +141,7 @@ func normalizeList(input dnd.NPCInteractionList, doc *source.SourceDocument, reg
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
for index, inputInteraction := range input.Interactions {
|
||||
interaction, nameChange, refsChanged := normalizeInteraction(inputInteraction, doc, registry)
|
||||
earliest, hasEvidence := earliestSourcePosition(doc, interaction)
|
||||
records[index] = normalizedRecord{interaction: interaction, inputIndex: index, earliest: earliest, hasEvidence: hasEvidence}
|
||||
records[index] = normalizedRecord{interaction: interaction, inputIndex: index}
|
||||
if nameChange != nil {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: interactionScope(index),
|
||||
@@ -163,7 +160,9 @@ func normalizeList(input dnd.NPCInteractionList, doc *source.SourceDocument, reg
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(records, func(left, right int) bool { return recordLess(doc, records[left], records[right]) })
|
||||
sort.SliceStable(records, func(left, right int) bool {
|
||||
return interactionmodel.Less(doc, records[left].interaction, records[right].interaction)
|
||||
})
|
||||
for position, record := range records {
|
||||
if position == record.inputIndex {
|
||||
continue
|
||||
@@ -177,7 +176,8 @@ func normalizeList(input dnd.NPCInteractionList, doc *source.SourceDocument, reg
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records, doc)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.NPCInteractionList{Interactions: output}, warnings
|
||||
return dnd.NPCInteractionList{Interactions: output},
|
||||
diagnostics.LimitWarnings(warnings, "npc_interactions", ReasonCodeWarningsOmitted)
|
||||
}
|
||||
|
||||
func normalizeInteraction(input dnd.NPCInteraction, doc *source.SourceDocument, registry *npcregistry.Registry) (dnd.NPCInteraction, *nameCanonicalization, bool) {
|
||||
@@ -189,8 +189,8 @@ func normalizeInteraction(input dnd.NPCInteraction, doc *source.SourceDocument,
|
||||
if input.Name != output.Name {
|
||||
nameChange = &nameCanonicalization{from: input.Name, to: output.Name}
|
||||
}
|
||||
output.SourceRefs = canonicalizeSourceRefs(doc, input.SourceRefs)
|
||||
return output, nameChange, !sourceRefsEqual(input.SourceRefs, output.SourceRefs)
|
||||
output.SourceRefs = interactionmodel.CanonicalizeSourceRefs(doc, input.SourceRefs)
|
||||
return output, nameChange, !interactionmodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs)
|
||||
}
|
||||
|
||||
func cloneInteraction(input dnd.NPCInteraction) dnd.NPCInteraction {
|
||||
@@ -201,107 +201,6 @@ func cloneInteraction(input dnd.NPCInteraction) dnd.NPCInteraction {
|
||||
return output
|
||||
}
|
||||
|
||||
func canonicalizeSourceRefs(doc *source.SourceDocument, input []source.SourceRef) []source.SourceRef {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
canonical := append([]source.SourceRef(nil), input...)
|
||||
sort.SliceStable(canonical, func(left, right int) bool { return sourceRefLess(doc, canonical[left], canonical[right]) })
|
||||
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
|
||||
}
|
||||
|
||||
func sourceRefsEqual(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
|
||||
}
|
||||
|
||||
func sourceRefLess(doc *source.SourceDocument, left, right source.SourceRef) bool {
|
||||
if left.SourceID != right.SourceID {
|
||||
return left.SourceID < right.SourceID
|
||||
}
|
||||
leftStart, leftStartOK := source.UnitIndex(doc, left.StartUnitID)
|
||||
rightStart, rightStartOK := source.UnitIndex(doc, right.StartUnitID)
|
||||
if leftStartOK != rightStartOK {
|
||||
return leftStartOK
|
||||
}
|
||||
if leftStartOK && leftStart != rightStart {
|
||||
return leftStart < rightStart
|
||||
}
|
||||
if left.StartUnitID != right.StartUnitID {
|
||||
return left.StartUnitID < right.StartUnitID
|
||||
}
|
||||
leftEnd, leftEndOK := source.UnitIndex(doc, left.EndUnitID)
|
||||
rightEnd, rightEndOK := source.UnitIndex(doc, right.EndUnitID)
|
||||
if leftEndOK != rightEndOK {
|
||||
return leftEndOK
|
||||
}
|
||||
if leftEndOK && leftEnd != rightEnd {
|
||||
return leftEnd < rightEnd
|
||||
}
|
||||
return left.EndUnitID < right.EndUnitID
|
||||
}
|
||||
|
||||
func earliestSourcePosition(doc *source.SourceDocument, interaction dnd.NPCInteraction) (int, bool) {
|
||||
found := false
|
||||
earliest := 0
|
||||
for _, ref := range interaction.SourceRefs {
|
||||
if source.ValidateRef(doc, ref) != nil {
|
||||
continue
|
||||
}
|
||||
position, ok := source.UnitIndex(doc, ref.StartUnitID)
|
||||
if !ok || (found && position >= earliest) {
|
||||
continue
|
||||
}
|
||||
earliest = position
|
||||
found = true
|
||||
}
|
||||
return earliest, found
|
||||
}
|
||||
|
||||
func recordLess(doc *source.SourceDocument, left, right normalizedRecord) bool {
|
||||
if left.hasEvidence != right.hasEvidence {
|
||||
return left.hasEvidence
|
||||
}
|
||||
if left.hasEvidence && left.earliest != right.earliest {
|
||||
return left.earliest < right.earliest
|
||||
}
|
||||
leftKey := identity.ComparisonKey(left.interaction.Name)
|
||||
rightKey := identity.ComparisonKey(right.interaction.Name)
|
||||
if leftKey != rightKey {
|
||||
return leftKey < rightKey
|
||||
}
|
||||
if left.interaction.Name != right.interaction.Name {
|
||||
return left.interaction.Name < right.interaction.Name
|
||||
}
|
||||
if left.interaction.Kind != right.interaction.Kind {
|
||||
return left.interaction.Kind < right.interaction.Kind
|
||||
}
|
||||
return sourceRefsLess(doc, left.interaction.SourceRefs, right.interaction.SourceRefs)
|
||||
}
|
||||
|
||||
func sourceRefsLess(doc *source.SourceDocument, left, right []source.SourceRef) bool {
|
||||
for index := 0; index < len(left) && index < len(right); index++ {
|
||||
if left[index] == right[index] {
|
||||
continue
|
||||
}
|
||||
return sourceRefLess(doc, left[index], right[index])
|
||||
}
|
||||
return len(left) < len(right)
|
||||
}
|
||||
|
||||
type duplicateGroup struct {
|
||||
retainedIndex int
|
||||
removed []int
|
||||
@@ -315,11 +214,11 @@ func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument)
|
||||
groups := make([]duplicateGroup, 0)
|
||||
groupByKey := make(map[string]int)
|
||||
for index, record := range records {
|
||||
key, eligible := duplicateKey(record.interaction, doc)
|
||||
if !eligible {
|
||||
if !interactionmodel.ValidSourceRefs(doc, record.interaction.SourceRefs) {
|
||||
keep[index] = true
|
||||
continue
|
||||
}
|
||||
key := interactionmodel.ExactIdentity(record.interaction)
|
||||
groupIndex, exists := groupByKey[key]
|
||||
if !exists {
|
||||
groupByKey[key] = len(groups)
|
||||
@@ -344,37 +243,6 @@ func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument)
|
||||
return output, warnings
|
||||
}
|
||||
|
||||
func duplicateKey(interaction dnd.NPCInteraction, doc *source.SourceDocument) (string, bool) {
|
||||
if len(interaction.SourceRefs) == 0 {
|
||||
return "", false
|
||||
}
|
||||
for _, ref := range interaction.SourceRefs {
|
||||
if source.ValidateRef(doc, ref) != nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
var key strings.Builder
|
||||
writeKeyString(&key, interaction.Name)
|
||||
writeKeyString(&key, string(interaction.Kind))
|
||||
for _, ref := range interaction.SourceRefs {
|
||||
writeKeyString(&key, ref.SourceID)
|
||||
writeKeyInt(&key, ref.StartUnitID)
|
||||
writeKeyInt(&key, ref.EndUnitID)
|
||||
}
|
||||
return key.String(), true
|
||||
}
|
||||
|
||||
func writeKeyString(builder *strings.Builder, value string) {
|
||||
builder.WriteString(strconv.Itoa(len(value)))
|
||||
builder.WriteByte(':')
|
||||
builder.WriteString(value)
|
||||
}
|
||||
|
||||
func writeKeyInt(builder *strings.Builder, value int) {
|
||||
builder.WriteString(strconv.Itoa(value))
|
||||
builder.WriteByte(';')
|
||||
}
|
||||
|
||||
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
||||
issues := make([]string, len(removed))
|
||||
for index, removedIndex := range removed {
|
||||
|
||||
Reference in New Issue
Block a user