Tighten NPC interaction validation and consistency
This commit is contained in:
@@ -5,14 +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"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcinteractions"
|
||||
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape"
|
||||
@@ -78,7 +76,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
||||
}
|
||||
|
||||
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCInteractionList]) (contracts.ValidationResult, error) {
|
||||
if interactionshape.Validate(req.Value) != nil || !sourceRefsValid(req.Source, req.Value) {
|
||||
if interactionshape.Validate(req.Value) != nil || !allSourceRefsValid(req.Source, req.Value) {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
if v == nil || v.npcResolver == nil {
|
||||
@@ -102,12 +100,10 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sourceRefsValid(doc *source.SourceDocument, value dnd.NPCInteractionList) bool {
|
||||
func allSourceRefsValid(doc *source.SourceDocument, value dnd.NPCInteractionList) bool {
|
||||
for _, interaction := range value.Interactions {
|
||||
for _, ref := range interaction.SourceRefs {
|
||||
if source.ValidateRef(doc, ref) != nil {
|
||||
return false
|
||||
}
|
||||
if !interactionmodel.ValidSourceRefs(doc, interaction.SourceRefs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -123,7 +119,7 @@ func issuesFor(doc *source.SourceDocument, value dnd.NPCInteractionList, npcRegi
|
||||
for refIndex := 1; refIndex < len(interaction.SourceRefs); refIndex++ {
|
||||
previous := interaction.SourceRefs[refIndex-1]
|
||||
current := interaction.SourceRefs[refIndex]
|
||||
if sourceRefLess(doc, current, previous) {
|
||||
if interactionmodel.SourceRefLess(doc, current, previous) {
|
||||
issues = append(issues, fmt.Sprintf("%s.source_refs are not in canonical order at index %d", prefix, refIndex))
|
||||
} else if current == previous {
|
||||
issues = append(issues, fmt.Sprintf("%s.source_refs[%d] duplicates the previous reference", prefix, refIndex))
|
||||
@@ -132,13 +128,13 @@ func issuesFor(doc *source.SourceDocument, value dnd.NPCInteractionList, npcRegi
|
||||
}
|
||||
|
||||
if !sort.SliceIsSorted(value.Interactions, func(left, right int) bool {
|
||||
return interactionLess(doc, value.Interactions[left], value.Interactions[right])
|
||||
return interactionmodel.Less(doc, value.Interactions[left], value.Interactions[right])
|
||||
}) {
|
||||
issues = append(issues, "interactions are not in canonical order")
|
||||
}
|
||||
seen := make(map[string]int)
|
||||
for index, interaction := range value.Interactions {
|
||||
key := duplicateKey(interaction)
|
||||
key := interactionmodel.ExactIdentity(interaction)
|
||||
if previous, ok := seen[key]; ok {
|
||||
issues = append(issues, fmt.Sprintf("interactions[%d] duplicates interaction %d", index, previous))
|
||||
continue
|
||||
@@ -148,105 +144,6 @@ func issuesFor(doc *source.SourceDocument, value dnd.NPCInteractionList, npcRegi
|
||||
return issues
|
||||
}
|
||||
|
||||
func interactionLess(doc *source.SourceDocument, left, right dnd.NPCInteraction) bool {
|
||||
leftPosition, leftHasEvidence := earliestSourcePosition(doc, left)
|
||||
rightPosition, rightHasEvidence := earliestSourcePosition(doc, right)
|
||||
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(doc, left.SourceRefs, right.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)
|
||||
}
|
||||
|
||||
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 duplicateKey(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 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 Spec() pipeline.ValidatorSpec {
|
||||
return pipeline.ValidatorSpec{Key: Key, ExecutionClass: contracts.ExecutionClassDeterministic}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user