156 lines
5.8 KiB
Go
156 lines
5.8 KiB
Go
package itemregistry
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/semanticreconcile"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
|
"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"
|
|
)
|
|
|
|
const incompatibleCurrencyGroup semanticreconcile.RejectionCategory = "incompatible_currency"
|
|
|
|
func reconciliationInputs(records []normalizedRecord) ([]semanticreconcile.Candidate, []semanticreconcile.Record[dnd.Item], error) {
|
|
candidates := make([]semanticreconcile.Candidate, len(records))
|
|
envelopes := make([]semanticreconcile.Record[dnd.Item], len(records))
|
|
for index, record := range records {
|
|
candidates[index] = semanticreconcile.Candidate{
|
|
Label: record.item.Name,
|
|
SourceRefs: cloneSourceRefs(record.item.SourceRefs),
|
|
}
|
|
envelope, err := semanticreconcile.NewRecord(record.item, record.inputIndexes, record.earliest, cloneItem)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("record %d: %w", index, err)
|
|
}
|
|
envelopes[index] = envelope
|
|
}
|
|
return candidates, envelopes, nil
|
|
}
|
|
|
|
func applyReconciliationPlan(plan semanticreconcile.Plan, records []normalizedRecord, envelopes []semanticreconcile.Record[dnd.Item], order shared.SourceRefOrder) ([]normalizedRecord, []diagnostics.Finding, []diagnostics.Finding, int, error) {
|
|
application, err := semanticreconcile.ApplyPlan(plan, envelopes, semanticreconcile.ApplicationPolicy[dnd.Item]{
|
|
CloneValue: cloneItem,
|
|
RejectGroup: func(members []dnd.Item, _ dnd.Item) semanticreconcile.RejectionCategory {
|
|
if !canConsolidate(members) {
|
|
return incompatibleCurrencyGroup
|
|
}
|
|
return ""
|
|
},
|
|
ConsolidateGroup: func(members []dnd.Item, canonical dnd.Item) (dnd.Item, error) {
|
|
output := cloneItem(canonical)
|
|
output.SourceRefs = nil
|
|
for _, member := range members {
|
|
output.SourceRefs = append(output.SourceRefs, member.SourceRefs...)
|
|
}
|
|
output.SourceRefs = order.Canonicalize(output.SourceRefs)
|
|
output.ID = identity.DeriveID(output.Name)
|
|
return output, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, 0, err
|
|
}
|
|
|
|
applied := application.Records()
|
|
output := make([]normalizedRecord, len(applied))
|
|
for index, record := range applied {
|
|
output[index] = normalizedRecord{
|
|
item: record.Value(),
|
|
inputIndexes: record.OriginalInputIndexes(),
|
|
earliest: record.EarliestInputPosition(),
|
|
}
|
|
}
|
|
type orderedFinding struct {
|
|
position int
|
|
finding diagnostics.Finding
|
|
}
|
|
observations := make([]orderedFinding, 0, len(application.AppliedGroups()))
|
|
advisories := make([]orderedFinding, 0, len(application.RejectedGroups()))
|
|
for _, event := range application.AppliedGroups() {
|
|
provenance := event.Provenance()
|
|
observations = append(observations, orderedFinding{
|
|
position: provenance.EarliestInputPosition(),
|
|
finding: semanticDuplicateFinding(provenance, records[provenance.CanonicalPosition()]),
|
|
})
|
|
}
|
|
for _, event := range application.RejectedGroups() {
|
|
provenance := event.Provenance()
|
|
advisories = append(advisories, orderedFinding{
|
|
position: provenance.EarliestInputPosition(),
|
|
finding: diagnostics.Finding{
|
|
Scope: itemScope(provenance.EarliestInputPosition()),
|
|
ReasonCode: ReasonCodeItemSemanticProposalInvalid,
|
|
Message: "proposal group preserved because currency may only be consolidated with aliases of one denomination",
|
|
},
|
|
})
|
|
}
|
|
sort.SliceStable(observations, func(left, right int) bool { return observations[left].position < observations[right].position })
|
|
sort.SliceStable(advisories, func(left, right int) bool { return advisories[left].position < advisories[right].position })
|
|
observationFindings := make([]diagnostics.Finding, len(observations))
|
|
for index, entry := range observations {
|
|
observationFindings[index] = entry.finding
|
|
}
|
|
advisoryFindings := make([]diagnostics.Finding, len(advisories))
|
|
for index, entry := range advisories {
|
|
advisoryFindings[index] = entry.finding
|
|
}
|
|
return output, observationFindings, advisoryFindings, len(application.RejectedGroups()), nil
|
|
}
|
|
|
|
func canConsolidate(items []dnd.Item) bool {
|
|
denomination := ""
|
|
hasCurrency := false
|
|
hasNonCurrency := false
|
|
hasConflictingDenominations := false
|
|
for _, item := range items {
|
|
current := currencyDenomination(item.Name)
|
|
if current == "" {
|
|
hasNonCurrency = true
|
|
continue
|
|
}
|
|
hasCurrency = true
|
|
if denomination != "" && denomination != current {
|
|
hasConflictingDenominations = true
|
|
continue
|
|
}
|
|
denomination = current
|
|
}
|
|
return !hasCurrency || (!hasNonCurrency && !hasConflictingDenominations)
|
|
}
|
|
|
|
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 semanticDuplicateFinding(provenance semanticreconcile.GroupProvenance, canonical normalizedRecord) diagnostics.Finding {
|
|
inputIndexes := provenance.OriginalInputIndexes()
|
|
details := make([]string, 0, len(inputIndexes)+1)
|
|
for _, inputIndex := range inputIndexes {
|
|
details = append(details, fmt.Sprintf("input index %d", inputIndex))
|
|
}
|
|
if canonical.earliest != provenance.EarliestInputPosition() {
|
|
details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest))
|
|
}
|
|
return diagnostics.Finding{
|
|
Scope: itemScope(provenance.EarliestInputPosition()),
|
|
ReasonCode: ReasonCodeDuplicateItemCollapsed,
|
|
Message: diagnostics.Aggregate("semantic duplicate consolidation", details),
|
|
}
|
|
}
|