Reuse canonical item occurrence evidence
This commit is contained in:
@@ -93,6 +93,14 @@ func ValidSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool {
|
|||||||
// comparable through SourceRefOrder's literal fallback so malformed candidates
|
// comparable through SourceRefOrder's literal fallback so malformed candidates
|
||||||
// are still safe to sort and diagnose.
|
// are still safe to sort and diagnose.
|
||||||
func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||||
|
left.SourceRefs = order.Canonicalize(left.SourceRefs)
|
||||||
|
right.SourceRefs = order.Canonicalize(right.SourceRefs)
|
||||||
|
return CanonicalLess(order, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalLess defines the canonical occurrence order without modifying or
|
||||||
|
// canonicalizing source references. Callers must provide canonical evidence.
|
||||||
|
func CanonicalLess(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||||
leftPosition, leftHasEvidence := order.EarliestValid(left.SourceRefs)
|
leftPosition, leftHasEvidence := order.EarliestValid(left.SourceRefs)
|
||||||
rightPosition, rightHasEvidence := order.EarliestValid(right.SourceRefs)
|
rightPosition, rightHasEvidence := order.EarliestValid(right.SourceRefs)
|
||||||
if leftHasEvidence != rightHasEvidence {
|
if leftHasEvidence != rightHasEvidence {
|
||||||
@@ -122,12 +130,20 @@ func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
|||||||
if less, decided := optionalQuantityLess(left.Quantity, right.Quantity); decided {
|
if less, decided := optionalQuantityLess(left.Quantity, right.Quantity); decided {
|
||||||
return less
|
return less
|
||||||
}
|
}
|
||||||
return sourceRefsLess(order, order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs))
|
return sourceRefsLess(order, left.SourceRefs, right.SourceRefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExactEqual reports whether occurrences are exact duplicates after their display
|
// ExactEqual reports whether occurrences are exact duplicates after their display
|
||||||
// fields and evidence have been canonicalized for the supplied source order.
|
// fields and evidence have been canonicalized for the supplied source order.
|
||||||
func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||||
|
left.SourceRefs = order.Canonicalize(left.SourceRefs)
|
||||||
|
right.SourceRefs = order.Canonicalize(right.SourceRefs)
|
||||||
|
return CanonicalExactEqual(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalExactEqual reports whether canonical occurrences are exact
|
||||||
|
// duplicates without modifying or canonicalizing their source references.
|
||||||
|
func CanonicalExactEqual(left, right dnd.ItemOccurrence) bool {
|
||||||
if left.ItemID != right.ItemID || DisplayValue(left.Name) != DisplayValue(right.Name) || left.Kind != right.Kind ||
|
if left.ItemID != right.ItemID || DisplayValue(left.Name) != DisplayValue(right.Name) || left.Kind != right.Kind ||
|
||||||
DisplayValue(left.From) != DisplayValue(right.From) || DisplayValue(left.To) != DisplayValue(right.To) ||
|
DisplayValue(left.From) != DisplayValue(right.From) || DisplayValue(left.To) != DisplayValue(right.To) ||
|
||||||
(left.Quantity == nil) != (right.Quantity == nil) {
|
(left.Quantity == nil) != (right.Quantity == nil) {
|
||||||
@@ -136,13 +152,21 @@ func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) boo
|
|||||||
if left.Quantity != nil && *left.Quantity != *right.Quantity {
|
if left.Quantity != nil && *left.Quantity != *right.Quantity {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return SourceRefsEqual(order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs))
|
return SourceRefsEqual(left.SourceRefs, right.SourceRefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExactIdentity returns a collision-safe duplicate key after display and
|
// ExactIdentity returns a collision-safe duplicate key after display and
|
||||||
// evidence canonicalization. It is intended for callers that have already
|
// evidence canonicalization. It is intended for callers that have already
|
||||||
// decided the occurrence is eligible for duplicate handling.
|
// decided the occurrence is eligible for duplicate handling.
|
||||||
func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) string {
|
func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) string {
|
||||||
|
occurrence.SourceRefs = order.Canonicalize(occurrence.SourceRefs)
|
||||||
|
return CanonicalExactIdentity(occurrence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalExactIdentity returns a collision-safe duplicate key without
|
||||||
|
// modifying or canonicalizing source references. Callers must provide
|
||||||
|
// canonical evidence.
|
||||||
|
func CanonicalExactIdentity(occurrence dnd.ItemOccurrence) string {
|
||||||
var key strings.Builder
|
var key strings.Builder
|
||||||
writeKeyString(&key, occurrence.ItemID)
|
writeKeyString(&key, occurrence.ItemID)
|
||||||
writeKeyString(&key, DisplayValue(occurrence.Name))
|
writeKeyString(&key, DisplayValue(occurrence.Name))
|
||||||
@@ -155,14 +179,24 @@ func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) s
|
|||||||
key.WriteByte('1')
|
key.WriteByte('1')
|
||||||
writeKeyInt(&key, *occurrence.Quantity)
|
writeKeyInt(&key, *occurrence.Quantity)
|
||||||
}
|
}
|
||||||
for _, ref := range order.Canonicalize(occurrence.SourceRefs) {
|
writeKeySourceRefs(&key, occurrence.SourceRefs)
|
||||||
writeKeyString(&key, ref.SourceID)
|
|
||||||
writeKeyInt(&key, ref.StartUnitID)
|
|
||||||
writeKeyInt(&key, ref.EndUnitID)
|
|
||||||
}
|
|
||||||
return key.String()
|
return key.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeKeySourceRefs(builder *strings.Builder, refs []source.SourceRef) {
|
||||||
|
if refs == nil {
|
||||||
|
builder.WriteByte('0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
builder.WriteByte('1')
|
||||||
|
writeKeyInt(builder, len(refs))
|
||||||
|
for _, ref := range refs {
|
||||||
|
writeKeyString(builder, ref.SourceID)
|
||||||
|
writeKeyInt(builder, ref.StartUnitID)
|
||||||
|
writeKeyInt(builder, ref.EndUnitID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func optionalStringLess(left, right string) (bool, bool) {
|
func optionalStringLess(left, right string) (bool, bool) {
|
||||||
leftPresent, rightPresent := HolderPresent(left), HolderPresent(right)
|
leftPresent, rightPresent := HolderPresent(left), HolderPresent(right)
|
||||||
if leftPresent != rightPresent {
|
if leftPresent != rightPresent {
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ func TestLessAndExactEqualityCanonicalizeEvidence(t *testing.T) {
|
|||||||
if ExactIdentity(order, first) != ExactIdentity(order, second) {
|
if ExactIdentity(order, first) != ExactIdentity(order, second) {
|
||||||
t.Fatal("ExactIdentity() differs for canonical duplicates")
|
t.Fatal("ExactIdentity() differs for canonical duplicates")
|
||||||
}
|
}
|
||||||
|
canonicalFirst := first
|
||||||
|
canonicalFirst.SourceRefs = order.Canonicalize(first.SourceRefs)
|
||||||
|
if !CanonicalExactEqual(canonicalFirst, second) || CanonicalExactIdentity(canonicalFirst) != CanonicalExactIdentity(second) {
|
||||||
|
t.Fatal("canonical helpers did not recognize canonical duplicates")
|
||||||
|
}
|
||||||
|
if CanonicalExactEqual(first, second) || CanonicalExactIdentity(first) == CanonicalExactIdentity(second) {
|
||||||
|
t.Fatal("canonical helpers repaired noncanonical source references")
|
||||||
|
}
|
||||||
|
|
||||||
quantity := 1
|
quantity := 1
|
||||||
differentQuantity := second
|
differentQuantity := second
|
||||||
@@ -132,6 +140,12 @@ func TestSourceReferenceHelpers(t *testing.T) {
|
|||||||
if ValidSourceRefs(source.NewDocumentIndex(doc), []source.SourceRef{{SourceID: "other", StartUnitID: 10, EndUnitID: 20}}) {
|
if ValidSourceRefs(source.NewDocumentIndex(doc), []source.SourceRef{{SourceID: "other", StartUnitID: 10, EndUnitID: 20}}) {
|
||||||
t.Fatal("ValidSourceRefs() = true, want invalid source identifier rejection")
|
t.Fatal("ValidSourceRefs() = true, want invalid source identifier rejection")
|
||||||
}
|
}
|
||||||
|
nilEvidence := dnd.ItemOccurrence{ItemID: "item"}
|
||||||
|
emptyEvidence := nilEvidence
|
||||||
|
emptyEvidence.SourceRefs = []source.SourceRef{}
|
||||||
|
if CanonicalExactIdentity(nilEvidence) == CanonicalExactIdentity(emptyEvidence) {
|
||||||
|
t.Fatal("CanonicalExactIdentity() conflated nil and empty source references")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLessSortsMalformedReferencesDeterministically(t *testing.T) {
|
func TestLessSortsMalformedReferencesDeterministically(t *testing.T) {
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
|
|
||||||
type normalizedRecord struct {
|
type normalizedRecord struct {
|
||||||
occurrence dnd.ItemOccurrence
|
occurrence dnd.ItemOccurrence
|
||||||
|
identity string
|
||||||
inputIndex int
|
inputIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
|||||||
warnings := make([]contracts.Warning, 0)
|
warnings := make([]contracts.Warning, 0)
|
||||||
for index, inputOccurrence := range input.Occurrences {
|
for index, inputOccurrence := range input.Occurrences {
|
||||||
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
|
||||||
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
|
records[index] = normalizedRecord{occurrence: occurrence, identity: itemoccurrencemodel.CanonicalExactIdentity(occurrence), inputIndex: index}
|
||||||
if len(changedFields) != 0 {
|
if len(changedFields) != 0 {
|
||||||
warnings = append(warnings, contracts.Warning{
|
warnings = append(warnings, contracts.Warning{
|
||||||
Scope: occurrenceScope(index),
|
Scope: occurrenceScope(index),
|
||||||
@@ -153,7 +154,7 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.SliceStable(records, func(left, right int) bool {
|
sort.SliceStable(records, func(left, right int) bool {
|
||||||
return itemoccurrencemodel.Less(order, records[left].occurrence, records[right].occurrence)
|
return itemoccurrencemodel.CanonicalLess(order, records[left].occurrence, records[right].occurrence)
|
||||||
})
|
})
|
||||||
for position, record := range records {
|
for position, record := range records {
|
||||||
if position == record.inputIndex {
|
if position == record.inputIndex {
|
||||||
@@ -166,7 +167,7 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
output, duplicateWarnings := collapseDuplicates(records, index, order)
|
output, duplicateWarnings := collapseDuplicates(records, index)
|
||||||
warnings = append(warnings, duplicateWarnings...)
|
warnings = append(warnings, duplicateWarnings...)
|
||||||
return dnd.ItemOccurrenceList{Occurrences: output}, diagnostics.LimitWarnings(warnings, "item_occurrences", ReasonCodeWarningsOmitted)
|
return dnd.ItemOccurrenceList{Occurrences: output}, diagnostics.LimitWarnings(warnings, "item_occurrences", ReasonCodeWarningsOmitted)
|
||||||
}
|
}
|
||||||
@@ -208,28 +209,35 @@ func cloneOccurrence(input dnd.ItemOccurrence) dnd.ItemOccurrence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type duplicateGroup struct {
|
type duplicateGroup struct {
|
||||||
retainedIndex int
|
retainedIndex int
|
||||||
removed []int
|
retainedRecord int
|
||||||
|
removed []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex, order shared.SourceRefOrder) ([]dnd.ItemOccurrence, []contracts.Warning) {
|
func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex) ([]dnd.ItemOccurrence, []contracts.Warning) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return make([]dnd.ItemOccurrence, 0), nil
|
return make([]dnd.ItemOccurrence, 0), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
keep := make([]bool, len(records))
|
keep := make([]bool, len(records))
|
||||||
groups := make([]duplicateGroup, 0)
|
groups := make([]duplicateGroup, 0)
|
||||||
groupByKey := make(map[string]int)
|
groupByKey := make(map[string][]int)
|
||||||
for recordIndex, record := range records {
|
for recordIndex, record := range records {
|
||||||
if !itemoccurrencemodel.ValidSourceRefs(index, record.occurrence.SourceRefs) {
|
if !itemoccurrencemodel.ValidSourceRefs(index, record.occurrence.SourceRefs) {
|
||||||
keep[recordIndex] = true
|
keep[recordIndex] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := itemoccurrencemodel.ExactIdentity(order, record.occurrence)
|
groupIndex := -1
|
||||||
groupIndex, exists := groupByKey[key]
|
for _, candidate := range groupByKey[record.identity] {
|
||||||
if !exists {
|
if itemoccurrencemodel.CanonicalExactEqual(records[groups[candidate].retainedRecord].occurrence, record.occurrence) {
|
||||||
groupByKey[key] = len(groups)
|
groupIndex = candidate
|
||||||
groups = append(groups, duplicateGroup{retainedIndex: record.inputIndex})
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if groupIndex < 0 {
|
||||||
|
groupIndex = len(groups)
|
||||||
|
groupByKey[record.identity] = append(groupByKey[record.identity], groupIndex)
|
||||||
|
groups = append(groups, duplicateGroup{retainedIndex: record.inputIndex, retainedRecord: recordIndex})
|
||||||
keep[recordIndex] = true
|
keep[recordIndex] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ const (
|
|||||||
var requiredCapabilities = []string{"merged"}
|
var requiredCapabilities = []string{"merged"}
|
||||||
var providedCapabilities = []string{"normalized"}
|
var providedCapabilities = []string{"normalized"}
|
||||||
|
|
||||||
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
|
|
||||||
Glossary: "Optional campaign glossary reference material used only for occurrence disambiguation.",
|
|
||||||
Party: "Optional party roster reference material used only for occurrence disambiguation.",
|
|
||||||
Players: "Optional player list reference material used only for occurrence disambiguation.",
|
|
||||||
Roster: "Deprecated alias for party roster reference material used only for occurrence disambiguation.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ contracts.Normalizer[dnd.NPCOccurrenceList] = (*Normalizer)(nil)
|
var _ contracts.Normalizer[dnd.NPCOccurrenceList] = (*Normalizer)(nil)
|
||||||
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
|
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)
|
||||||
@@ -250,17 +243,14 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
|||||||
func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) }
|
func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) }
|
||||||
|
|
||||||
func referenceSlots() []contracts.ReferenceSlot {
|
func referenceSlots() []contracts.ReferenceSlot {
|
||||||
slots := shared.ReferenceSlots(referenceSlotDescriptions)
|
return []contracts.ReferenceSlot{{
|
||||||
slots = append(slots, contracts.ReferenceSlot{
|
|
||||||
Name: NPCRegistryReferenceSlot,
|
Name: NPCRegistryReferenceSlot,
|
||||||
Description: "Required normalized NPC registry used only for occurrence identity grounding, never as occurrence evidence.",
|
Description: "Required normalized NPC registry used only for occurrence identity grounding, never as occurrence evidence.",
|
||||||
Required: true,
|
Required: true,
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
})
|
}}
|
||||||
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })
|
|
||||||
return slots
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func TestNormalizerContractAndDeterministicWarnings(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if spec := ModuleSpec(); spec.Key != Key || spec.Stage != pipeline.StageNormalize || spec.ArtifactKind != dnd.NPCOccurrenceListKind || len(spec.ReferenceSlots) == 0 {
|
if spec := ModuleSpec(); spec.Key != Key || spec.Stage != pipeline.StageNormalize || spec.ArtifactKind != dnd.NPCOccurrenceListKind || len(spec.ReferenceSlots) != 1 || spec.ReferenceSlots[0].Name != NPCRegistryReferenceSlot || !spec.ReferenceSlots[0].Required {
|
||||||
t.Fatalf("ModuleSpec() = %#v", spec)
|
t.Fatalf("ModuleSpec() = %#v", spec)
|
||||||
}
|
}
|
||||||
if metadata := normalizer.ManifestMetadata(); metadata["normalization_policy"] != normalizationPolicy || metadata["npc_registry_digest"] == "" || metadata["npc_count"] != 2 {
|
if metadata := normalizer.ManifestMetadata(); metadata["normalization_policy"] != normalizationPolicy || metadata["npc_registry_digest"] == "" || metadata["npc_count"] != 2 {
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ func Validate(doc *source.SourceDocument, value dnd.ItemOccurrenceList) error {
|
|||||||
|
|
||||||
func issuesFor(order shared.SourceRefOrder, value dnd.ItemOccurrenceList) []string {
|
func issuesFor(order shared.SourceRefOrder, value dnd.ItemOccurrenceList) []string {
|
||||||
issues := make([]string, 0)
|
issues := make([]string, 0)
|
||||||
seenIdentity := make(map[string]int)
|
canonicalEvidence := make([]bool, len(value.Occurrences))
|
||||||
|
seenIdentity := make(map[string][]int)
|
||||||
for occurrenceIndex, occurrence := range value.Occurrences {
|
for occurrenceIndex, occurrence := range value.Occurrences {
|
||||||
prefix := fmt.Sprintf("occurrences[%d]", occurrenceIndex)
|
prefix := fmt.Sprintf("occurrences[%d]", occurrenceIndex)
|
||||||
|
canonicalEvidence[occurrenceIndex] = true
|
||||||
if occurrence.Name != itemoccurrencemodel.DisplayValue(occurrence.Name) {
|
if occurrence.Name != itemoccurrencemodel.DisplayValue(occurrence.Name) {
|
||||||
issues = append(issues, prefix+".name is not display-normalized")
|
issues = append(issues, prefix+".name is not display-normalized")
|
||||||
}
|
}
|
||||||
@@ -79,19 +81,29 @@ func issuesFor(order shared.SourceRefOrder, value dnd.ItemOccurrenceList) []stri
|
|||||||
current := occurrence.SourceRefs[refIndex]
|
current := occurrence.SourceRefs[refIndex]
|
||||||
if order.Less(current, previous) {
|
if order.Less(current, previous) {
|
||||||
issues = append(issues, fmt.Sprintf("%s.source_refs are not in canonical order at index %d", prefix, refIndex))
|
issues = append(issues, fmt.Sprintf("%s.source_refs are not in canonical order at index %d", prefix, refIndex))
|
||||||
|
canonicalEvidence[occurrenceIndex] = false
|
||||||
} else if current == previous {
|
} else if current == previous {
|
||||||
issues = append(issues, fmt.Sprintf("%s.source_refs[%d] duplicates the previous reference", prefix, refIndex))
|
issues = append(issues, fmt.Sprintf("%s.source_refs[%d] duplicates the previous reference", prefix, refIndex))
|
||||||
|
canonicalEvidence[occurrenceIndex] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
key := itemoccurrencemodel.ExactIdentity(order, occurrence)
|
if canonicalEvidence[occurrenceIndex] {
|
||||||
if previous, exists := seenIdentity[key]; exists {
|
key := itemoccurrencemodel.CanonicalExactIdentity(occurrence)
|
||||||
issues = append(issues, fmt.Sprintf("%s duplicates item occurrence %d under normalized identity", prefix, previous))
|
for _, previous := range seenIdentity[key] {
|
||||||
} else {
|
if itemoccurrencemodel.CanonicalExactEqual(value.Occurrences[previous], occurrence) {
|
||||||
seenIdentity[key] = occurrenceIndex
|
issues = append(issues, fmt.Sprintf("%s duplicates item occurrence %d under normalized identity", prefix, previous))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seenIdentity[key] = append(seenIdentity[key], occurrenceIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for occurrenceIndex := 1; occurrenceIndex < len(value.Occurrences); occurrenceIndex++ {
|
for occurrenceIndex := 1; occurrenceIndex < len(value.Occurrences); occurrenceIndex++ {
|
||||||
if itemoccurrencemodel.Less(order, value.Occurrences[occurrenceIndex], value.Occurrences[occurrenceIndex-1]) {
|
less := itemoccurrencemodel.Less
|
||||||
|
if canonicalEvidence[occurrenceIndex] && canonicalEvidence[occurrenceIndex-1] {
|
||||||
|
less = itemoccurrencemodel.CanonicalLess
|
||||||
|
}
|
||||||
|
if less(order, value.Occurrences[occurrenceIndex], value.Occurrences[occurrenceIndex-1]) {
|
||||||
issues = append(issues, fmt.Sprintf("occurrences[%d] is out of canonical order", occurrenceIndex))
|
issues = append(issues, fmt.Sprintf("occurrences[%d] is out of canonical order", occurrenceIndex))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user