2026-08-05 19:39:06 +00:00
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 ] ) )
}
2026-08-06 13:31:15 +00:00
warnings = append ( warnings , contracts . Warning { Scope : itemScope ( records [ group . members [ 0 ] ] . earliest ) , ReasonCode : ReasonCodeItemSemanticProposalInvalid , Message : "proposal group preserved because currency may only be consolidated with aliases of one denomination" } )
2026-08-05 19:39:06 +00:00
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 := ""
2026-08-06 13:31:15 +00:00
hasCurrency := false
hasNonCurrency := false
hasConflictingDenominations := false
2026-08-05 19:39:06 +00:00
for _ , member := range group . members {
current := currencyDenomination ( records [ member ] . item . Name )
if current == "" {
2026-08-06 13:31:15 +00:00
hasNonCurrency = true
2026-08-05 19:39:06 +00:00
continue
}
2026-08-06 13:31:15 +00:00
hasCurrency = true
2026-08-05 19:39:06 +00:00
if denomination != "" && denomination != current {
2026-08-06 13:31:15 +00:00
hasConflictingDenominations = true
continue
2026-08-05 19:39:06 +00:00
}
denomination = current
}
2026-08-06 13:31:15 +00:00
return ! hasCurrency || ( ! hasNonCurrency && ! hasConflictingDenominations )
2026-08-05 19:39:06 +00:00
}
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 ) }
}