154 lines
5.4 KiB
Go
154 lines
5.4 KiB
Go
package itemregistry
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/entityreconcile"
|
|
)
|
|
|
|
type safeReconciliationGroup struct {
|
|
members []int
|
|
canonical int
|
|
}
|
|
|
|
func reconciliationCandidates(records []normalizedRecord) []entityreconcile.Candidate {
|
|
candidates := make([]entityreconcile.Candidate, len(records))
|
|
for index, record := range records {
|
|
candidates[index] = entityreconcile.Candidate{Name: record.item.Name, SourceRefs: cloneSourceRefs(record.item.SourceRefs)}
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func reconciliationGroups(assessment entityreconcile.Assessment, candidateKeys []string) []safeReconciliationGroup {
|
|
positions := make(map[string]int, len(candidateKeys))
|
|
for index, key := range candidateKeys {
|
|
positions[key] = index
|
|
}
|
|
safeGroups := assessment.SafeGroups()
|
|
groups := make([]safeReconciliationGroup, 0, len(safeGroups))
|
|
for _, group := range safeGroups {
|
|
members := group.Members()
|
|
memberPositions := make([]int, len(members))
|
|
valid := true
|
|
for index, key := range members {
|
|
position, ok := positions[key]
|
|
if !ok {
|
|
valid = false
|
|
break
|
|
}
|
|
memberPositions[index] = position
|
|
}
|
|
canonical, ok := positions[group.Canonical()]
|
|
if valid && ok {
|
|
groups = append(groups, safeReconciliationGroup{members: memberPositions, canonical: canonical})
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
|
|
func reconciliationIssues(assessment entityreconcile.Assessment) []string {
|
|
issues := assessment.Issues()
|
|
details := make([]string, len(issues))
|
|
for index, issue := range issues {
|
|
details[index] = fmt.Sprintf("group %d: %s", issue.GroupIndex, issue.Category)
|
|
}
|
|
return details
|
|
}
|
|
|
|
func applySafeGroups(records []normalizedRecord, groups []safeReconciliationGroup, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, int) {
|
|
byMember := make(map[int]safeReconciliationGroup, len(groups)*2)
|
|
for _, group := range groups {
|
|
for _, member := range group.members {
|
|
byMember[member] = group
|
|
}
|
|
}
|
|
output := make([]normalizedRecord, 0, len(records)-len(groups))
|
|
warnings := make([]contracts.Warning, 0, len(groups))
|
|
rejectedGroups := 0
|
|
for index, record := range records {
|
|
group, grouped := byMember[index]
|
|
if !grouped {
|
|
output = append(output, cloneRecord(record))
|
|
continue
|
|
}
|
|
if group.members[0] != index {
|
|
continue
|
|
}
|
|
if !canConsolidate(records, group) {
|
|
for _, member := range group.members {
|
|
output = append(output, cloneRecord(records[member]))
|
|
}
|
|
warnings = append(warnings, contracts.Warning{Scope: itemScope(records[group.members[0]].earliest), ReasonCode: ReasonCodeItemSemanticProposalInvalid, Message: "proposal group preserved because currency denominations must remain distinct"})
|
|
rejectedGroups++
|
|
continue
|
|
}
|
|
consolidated := consolidateSemanticGroup(records, group, order)
|
|
output = append(output, consolidated)
|
|
warnings = append(warnings, semanticDuplicateWarning(consolidated, records[group.canonical]))
|
|
}
|
|
return output, warnings, rejectedGroups
|
|
}
|
|
|
|
func canConsolidate(records []normalizedRecord, group safeReconciliationGroup) bool {
|
|
denomination := ""
|
|
for _, member := range group.members {
|
|
current := currencyDenomination(records[member].item.Name)
|
|
if current == "" {
|
|
continue
|
|
}
|
|
if denomination != "" && denomination != current {
|
|
return false
|
|
}
|
|
denomination = current
|
|
}
|
|
return true
|
|
}
|
|
|
|
func currencyDenomination(name string) string {
|
|
switch identity.ComparisonKey(name) {
|
|
case "copper piece", "copper pieces", "cp":
|
|
return "copper"
|
|
case "silver piece", "silver pieces", "sp":
|
|
return "silver"
|
|
case "electrum piece", "electrum pieces", "ep":
|
|
return "electrum"
|
|
case "gold piece", "gold pieces", "gp":
|
|
return "gold"
|
|
case "platinum piece", "platinum pieces", "pp":
|
|
return "platinum"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func consolidateSemanticGroup(records []normalizedRecord, group safeReconciliationGroup, order shared.SourceRefOrder) normalizedRecord {
|
|
output := cloneRecord(records[group.members[0]])
|
|
output.item.Name = records[group.canonical].item.Name
|
|
for _, member := range group.members[1:] {
|
|
output.item.SourceRefs = append(output.item.SourceRefs, records[member].item.SourceRefs...)
|
|
output.inputIndexes = append(output.inputIndexes, records[member].inputIndexes...)
|
|
if records[member].earliest < output.earliest {
|
|
output.earliest = records[member].earliest
|
|
}
|
|
}
|
|
output.inputIndexes = sortedUniqueIndexes(output.inputIndexes)
|
|
output.item.SourceRefs = order.Canonicalize(output.item.SourceRefs)
|
|
output.item.ID = identity.DeriveID(output.item.Name)
|
|
return output
|
|
}
|
|
|
|
func semanticDuplicateWarning(record normalizedRecord, canonical normalizedRecord) contracts.Warning {
|
|
details := make([]string, 0, len(record.inputIndexes)+1)
|
|
for _, inputIndex := range record.inputIndexes {
|
|
details = append(details, fmt.Sprintf("input index %d", inputIndex))
|
|
}
|
|
if canonical.earliest != record.earliest {
|
|
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
|
}
|
|
return contracts.Warning{Scope: itemScope(record.earliest), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: diagnostics.Aggregate("semantic duplicate consolidation", details)}
|
|
}
|