// Package npcinteractions owns canonical ordering and exact-identity rules for // D&D NPC interaction artifacts. package npcinteractions import ( "strconv" "strings" "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" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) // SourceRefsEqual reports whether two source-reference lists have identical // representations and values. 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 } // Less defines the canonical order for NPC interaction occurrences. func Less(order shared.SourceRefOrder, left, right dnd.NPCInteraction) bool { leftPosition, leftHasEvidence := order.EarliestValid(left.SourceRefs) rightPosition, rightHasEvidence := order.EarliestValid(right.SourceRefs) if leftHasEvidence != rightHasEvidence { return leftHasEvidence } if leftHasEvidence && leftPosition != rightPosition { return leftPosition < rightPosition } leftKey := identity.ComparisonKey(left.Name) rightKey := identity.ComparisonKey(right.Name) if leftKey != rightKey { return leftKey < rightKey } if left.Name != right.Name { return left.Name < right.Name } if left.Kind != right.Kind { return left.Kind < right.Kind } return sourceRefsLess(order, left.SourceRefs, right.SourceRefs) } // ValidSourceRefs reports whether an interaction has non-empty, valid // current-document evidence. func ValidSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool { if len(refs) == 0 { return false } for _, ref := range refs { if index.ValidateRef(ref) != nil { return false } } return true } // ExactIdentity returns a collision-safe key over every durable interaction // field. Callers decide whether the record is eligible for duplicate handling. func ExactIdentity(interaction dnd.NPCInteraction) string { 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() } func sourceRefsLess(order shared.SourceRefOrder, left, right []source.SourceRef) bool { for index := 0; index < len(left) && index < len(right); index++ { if left[index] == right[index] { continue } return order.Less(left[index], right[index]) } return len(left) < len(right) } 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(';') }