Move item occurrences to canonical namespace

This commit is contained in:
2026-08-05 20:00:20 +00:00
parent 3dfefd0e14
commit a6e176e160
46 changed files with 342 additions and 337 deletions

View File

@@ -1,5 +1,5 @@
// Package itemevents normalizes merged D&D item-event candidates.
package itemevents
// Package itemoccurrences normalizes merged D&D item-occurrence candidates.
package itemoccurrences
import (
"context"
@@ -10,15 +10,15 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
itemeventmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents"
itemoccurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
)
const (
Key = "dnd/item-events"
normalizationPolicy = "dnd.item_events.normalize.v1"
Key = "dnd/item-occurrences"
normalizationPolicy = "dnd.item_occurrences.normalize.v1"
NormalizationPolicy = normalizationPolicy
ReasonCodeNameCanonicalized = "item_occurrence_name_canonicalized"
@@ -111,7 +111,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
}
type normalizedRecord struct {
event dnd.ItemOccurrence
occurrence dnd.ItemOccurrence
inputIndex int
}
@@ -122,44 +122,44 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
records := make([]normalizedRecord, len(input.Occurrences))
warnings := make([]contracts.Warning, 0)
for index, inputEvent := range input.Occurrences {
event, changedFields, found, refsChanged := normalizeEvent(inputEvent, order, registry)
records[index] = normalizedRecord{event: event, inputIndex: index}
for index, inputOccurrence := range input.Occurrences {
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
if len(changedFields) != 0 {
warnings = append(warnings, contracts.Warning{
Scope: eventScope(index),
Scope: occurrenceScope(index),
ReasonCode: ReasonCodeNameCanonicalized,
Message: fmt.Sprintf("input index %d: normalized display whitespace in %s", index,
diagnostics.Aggregate("fields", changedFields)),
})
}
if found && inputEvent.Name != event.Name {
warnings = append(warnings, contracts.Warning{Scope: eventScope(index), ReasonCode: ReasonCodeNameCanonicalized,
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputEvent.Name), diagnostics.Quote(event.Name))})
if found && inputOccurrence.Name != occurrence.Name {
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputOccurrence.Name), diagnostics.Quote(occurrence.Name))})
}
if !found {
warnings = append(warnings, contracts.Warning{Scope: eventScope(index), ReasonCode: ReasonCodeUnknownItemID,
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputEvent.ItemID))})
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.ItemID))})
}
if refsChanged {
warnings = append(warnings, contracts.Warning{
Scope: eventScope(index),
Scope: occurrenceScope(index),
ReasonCode: ReasonCodeSourceRefsNormalized,
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
index, len(inputEvent.SourceRefs), len(event.SourceRefs)),
index, len(inputOccurrence.SourceRefs), len(occurrence.SourceRefs)),
})
}
}
sort.SliceStable(records, func(left, right int) bool {
return itemeventmodel.Less(order, records[left].event, records[right].event)
return itemoccurrencemodel.Less(order, records[left].occurrence, records[right].occurrence)
})
for position, record := range records {
if position == record.inputIndex {
continue
}
warnings = append(warnings, contracts.Warning{
Scope: eventScope(record.inputIndex),
Scope: occurrenceScope(record.inputIndex),
ReasonCode: ReasonCodeOccurrencesReordered,
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
})
@@ -170,8 +170,8 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
return dnd.ItemOccurrenceList{Occurrences: output}, diagnostics.LimitWarnings(warnings, "item_occurrences", ReasonCodeWarningsOmitted)
}
func normalizeEvent(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) {
output := cloneEvent(input)
func normalizeOccurrence(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) {
output := cloneOccurrence(input)
canonical, found := registry.LookupID(input.ItemID)
if found {
output.Name = canonical.Name
@@ -184,17 +184,17 @@ func normalizeEvent(input dnd.ItemOccurrence, order shared.SourceRefOrder, regis
{name: "from", value: &output.From},
{name: "to", value: &output.To},
} {
trimmed := itemeventmodel.DisplayValue(*field.value)
trimmed := itemoccurrencemodel.DisplayValue(*field.value)
if *field.value != trimmed {
*field.value = trimmed
changedFields = append(changedFields, field.name)
}
}
output.SourceRefs = order.Canonicalize(input.SourceRefs)
return output, changedFields, found, !itemeventmodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs)
return output, changedFields, found, !itemoccurrencemodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs)
}
func cloneEvent(input dnd.ItemOccurrence) dnd.ItemOccurrence {
func cloneOccurrence(input dnd.ItemOccurrence) dnd.ItemOccurrence {
output := input
if input.Quantity != nil {
quantity := *input.Quantity
@@ -220,11 +220,11 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex,
groups := make([]duplicateGroup, 0)
groupByKey := make(map[string]int)
for recordIndex, record := range records {
if !itemeventmodel.ValidSourceRefs(index, record.event.SourceRefs) {
if !itemoccurrencemodel.ValidSourceRefs(index, record.occurrence.SourceRefs) {
keep[recordIndex] = true
continue
}
key := itemeventmodel.ExactIdentity(order, record.event)
key := itemoccurrencemodel.ExactIdentity(order, record.occurrence)
groupIndex, exists := groupByKey[key]
if !exists {
groupByKey[key] = len(groups)
@@ -238,7 +238,7 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex,
output := make([]dnd.ItemOccurrence, 0, len(records))
for recordIndex, record := range records {
if keep[recordIndex] {
output = append(output, cloneEvent(record.event))
output = append(output, cloneOccurrence(record.occurrence))
}
}
warnings := make([]contracts.Warning, 0)
@@ -256,14 +256,14 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
}
return contracts.Warning{
Scope: eventScope(retainedIndex),
Scope: occurrenceScope(retainedIndex),
ReasonCode: ReasonCodeDuplicateCollapsed,
Message: diagnostics.Aggregate(
fmt.Sprintf("duplicate item occurrence collapsed; retained input index %d", retainedIndex), issues),
}
}
func eventScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) }
func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"