Reuse document indexes in D&D normalization
This commit is contained in:
@@ -156,28 +156,25 @@ func planFromResponse(doc *source.SourceDocument, response chunkResponse) (sourc
|
||||
return source.ChunkPlan{}, fmt.Errorf("scenes must not be empty")
|
||||
}
|
||||
|
||||
unitIndexes := make(map[int]int, len(doc.Units))
|
||||
for i, unit := range doc.Units {
|
||||
unitIndexes[unit.ID] = i
|
||||
}
|
||||
index := source.NewDocumentIndex(doc)
|
||||
|
||||
ranges := make([]source.ChunkRange, 0, len(response.Scenes))
|
||||
previousEnd := -1
|
||||
for i, scene := range response.Scenes {
|
||||
startUnitID, err := shared.ResolveUnitID(doc, "start_unit_id", scene.StartUnitID)
|
||||
startUnitID, err := shared.ResolveUnitID(index, "start_unit_id", scene.StartUnitID)
|
||||
if err != nil {
|
||||
return source.ChunkPlan{}, fmt.Errorf("scene[%d] %w", i, err)
|
||||
}
|
||||
endUnitID, err := shared.ResolveUnitID(doc, "end_unit_id", scene.EndUnitID)
|
||||
endUnitID, err := shared.ResolveUnitID(index, "end_unit_id", scene.EndUnitID)
|
||||
if err != nil {
|
||||
return source.ChunkPlan{}, fmt.Errorf("scene[%d] %w", i, err)
|
||||
}
|
||||
|
||||
startIndex, ok := unitIndexes[startUnitID]
|
||||
startIndex, ok := index.Position(startUnitID)
|
||||
if !ok {
|
||||
return source.ChunkPlan{}, fmt.Errorf("scene[%d] start_unit_id %d was not found", i, startUnitID)
|
||||
}
|
||||
endIndex, ok := unitIndexes[endUnitID]
|
||||
endIndex, ok := index.Position(endUnitID)
|
||||
if !ok {
|
||||
return source.ChunkPlan{}, fmt.Errorf("scene[%d] end_unit_id %d was not found", i, endUnitID)
|
||||
}
|
||||
|
||||
@@ -110,8 +110,9 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
if err != nil {
|
||||
return contracts.TypedNormalizeResult[dnd.CombatTurnList]{}, normalizerErrorf("resolve NPC registry: %w", err)
|
||||
}
|
||||
order := shared.NewSourceRefOrder(req.Source)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, req.Source, order, npcRegistry)
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderWithIndex(req.Source, index)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, index, order, npcRegistry)
|
||||
return contracts.TypedNormalizeResult[dnd.CombatTurnList]{Value: value, Warnings: warnings}, nil
|
||||
}
|
||||
|
||||
@@ -127,7 +128,7 @@ type actorCanonicalization struct {
|
||||
to string
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurnList, []contracts.Warning) {
|
||||
func normalizeList(input dnd.CombatTurnList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.CombatTurnList, []contracts.Warning) {
|
||||
if input.CombatTurns == nil {
|
||||
return dnd.CombatTurnList{}, nil
|
||||
}
|
||||
@@ -182,7 +183,7 @@ func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, order s
|
||||
})
|
||||
}
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records, doc)
|
||||
output, duplicateWarnings := collapseDuplicates(records, documentIndex)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.CombatTurnList{CombatTurns: output}, warnings
|
||||
}
|
||||
@@ -200,8 +201,7 @@ func normalizeTurn(input dnd.CombatTurn, order shared.SourceRefOrder, registry *
|
||||
actorChange = &actorCanonicalization{from: input.Actor, to: output.Actor}
|
||||
}
|
||||
|
||||
canonicalRefs, _, _ := canonicalizeSourceRefs(order, input.SourceRefs)
|
||||
output.SourceRefs = canonicalRefs
|
||||
output.SourceRefs = order.Canonicalize(input.SourceRefs)
|
||||
refsChanged := !sourceRefsEqual(input.SourceRefs, output.SourceRefs)
|
||||
return output, actorChange, refsChanged
|
||||
}
|
||||
@@ -227,17 +227,12 @@ func sourceRefsEqual(left, right []source.SourceRef) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func canonicalizeSourceRefs(order shared.SourceRefOrder, input []source.SourceRef) ([]source.SourceRef, bool, int) {
|
||||
canonical := order.Canonicalize(input)
|
||||
return canonical, !sourceRefsEqual(input, canonical), len(input) - len(canonical)
|
||||
}
|
||||
|
||||
type duplicateGroup struct {
|
||||
retainedIndex int
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument) ([]dnd.CombatTurn, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.CombatTurn, []contracts.Warning) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.CombatTurn, 0), nil
|
||||
}
|
||||
@@ -246,7 +241,7 @@ func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument)
|
||||
groups := make([]duplicateGroup, 0)
|
||||
groupByKey := make(map[string]int)
|
||||
for index, record := range records {
|
||||
key, eligible := duplicateKey(record.turn, doc)
|
||||
key, eligible := duplicateKey(record.turn, documentIndex)
|
||||
if !eligible {
|
||||
keep[index] = true
|
||||
continue
|
||||
@@ -278,12 +273,12 @@ func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument)
|
||||
return output, warnings
|
||||
}
|
||||
|
||||
func duplicateKey(turn dnd.CombatTurn, doc *source.SourceDocument) (string, bool) {
|
||||
func duplicateKey(turn dnd.CombatTurn, documentIndex source.DocumentIndex) (string, bool) {
|
||||
if len(turn.SourceRefs) == 0 {
|
||||
return "", false
|
||||
}
|
||||
for _, ref := range turn.SourceRefs {
|
||||
if source.ValidateRef(doc, ref) != nil {
|
||||
if documentIndex.ValidateRef(ref) != nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,9 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
if !registry.Bound() {
|
||||
return contracts.TypedNormalizeResult[dnd.NPCInteractionList]{}, normalizerErrorf("NPC registry reference is required")
|
||||
}
|
||||
order := shared.NewSourceRefOrder(req.Source)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, req.Source, order, registry)
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderWithIndex(req.Source, index)
|
||||
value, warnings := normalizeList(req.MergeOutput.Value, index, order, registry)
|
||||
return contracts.TypedNormalizeResult[dnd.NPCInteractionList]{Value: value, Warnings: warnings}, nil
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ type nameCanonicalization struct {
|
||||
to string
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.NPCInteractionList, doc *source.SourceDocument, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCInteractionList, []contracts.Warning) {
|
||||
func normalizeList(input dnd.NPCInteractionList, documentIndex source.DocumentIndex, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCInteractionList, []contracts.Warning) {
|
||||
if input.Interactions == nil {
|
||||
return dnd.NPCInteractionList{}, nil
|
||||
}
|
||||
@@ -175,7 +176,7 @@ func normalizeList(input dnd.NPCInteractionList, doc *source.SourceDocument, ord
|
||||
})
|
||||
}
|
||||
|
||||
output, duplicateWarnings := collapseDuplicates(records, doc)
|
||||
output, duplicateWarnings := collapseDuplicates(records, documentIndex)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return dnd.NPCInteractionList{Interactions: output},
|
||||
diagnostics.LimitWarnings(warnings, "npc_interactions", ReasonCodeWarningsOmitted)
|
||||
@@ -207,7 +208,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument) ([]dnd.NPCInteraction, []contracts.Warning) {
|
||||
func collapseDuplicates(records []normalizedRecord, documentIndex source.DocumentIndex) ([]dnd.NPCInteraction, []contracts.Warning) {
|
||||
if len(records) == 0 {
|
||||
return make([]dnd.NPCInteraction, 0), nil
|
||||
}
|
||||
@@ -215,7 +216,7 @@ func collapseDuplicates(records []normalizedRecord, doc *source.SourceDocument)
|
||||
groups := make([]duplicateGroup, 0)
|
||||
groupByKey := make(map[string]int)
|
||||
for index, record := range records {
|
||||
if !interactionmodel.ValidSourceRefs(doc, record.interaction.SourceRefs) {
|
||||
if !interactionmodel.ValidSourceRefs(documentIndex, record.interaction.SourceRefs) {
|
||||
keep[index] = true
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func normalizeList(input dnd.NPCList, order shared.SourceRefOrder) (dnd.NPCList,
|
||||
func normalizeRecord(input dnd.NPC, order shared.SourceRefOrder) (dnd.NPC, bool, bool) {
|
||||
output := cloneNPC(input)
|
||||
output.Name = identity.NormalizeDisplay(input.Name)
|
||||
output.SourceRefs, _, _ = canonicalizeSourceRefs(order, input.SourceRefs)
|
||||
output.SourceRefs = order.Canonicalize(input.SourceRefs)
|
||||
output.ID = identity.DeriveID(output.Name)
|
||||
return output, input.Name != output.Name, !reflect.DeepEqual(input.SourceRefs, output.SourceRefs)
|
||||
}
|
||||
@@ -150,16 +150,11 @@ func consolidate(records []normalizedRecord, members []int, order shared.SourceR
|
||||
for _, member := range members[1:] {
|
||||
output.SourceRefs = append(output.SourceRefs, records[member].npc.SourceRefs...)
|
||||
}
|
||||
output.SourceRefs, _, _ = canonicalizeSourceRefs(order, output.SourceRefs)
|
||||
output.SourceRefs = order.Canonicalize(output.SourceRefs)
|
||||
output.ID = identity.DeriveID(output.Name)
|
||||
return output, !reflect.DeepEqual(originalRefs, output.SourceRefs)
|
||||
}
|
||||
|
||||
func canonicalizeSourceRefs(order shared.SourceRefOrder, input []source.SourceRef) ([]source.SourceRef, bool, int) {
|
||||
canonical := order.Canonicalize(input)
|
||||
return canonical, !reflect.DeepEqual(input, canonical), len(input) - len(canonical)
|
||||
}
|
||||
|
||||
func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
|
||||
if input == nil {
|
||||
return nil
|
||||
|
||||
@@ -78,26 +78,23 @@ func normalizeList(input dnd.SceneDescriptionList, doc *source.SourceDocument) (
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("scenes must not be empty")
|
||||
}
|
||||
|
||||
unitPositions := make(map[int]int, len(doc.Units))
|
||||
for index, unit := range doc.Units {
|
||||
unitPositions[unit.ID] = index
|
||||
}
|
||||
documentIndex := source.NewDocumentIndex(doc)
|
||||
output := dnd.SceneDescriptionList{Scenes: make([]dnd.SceneDescription, len(input.Scenes))}
|
||||
for index, scene := range input.Scenes {
|
||||
for sceneIndex, scene := range input.Scenes {
|
||||
scene.Title = strings.TrimSpace(scene.Title)
|
||||
scene.Summary = strings.TrimSpace(scene.Summary)
|
||||
if err := shape.Validate(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{scene}}); err != nil {
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("scenes[%d]: %w", index, err)
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("scenes[%d]: %w", sceneIndex, err)
|
||||
}
|
||||
if err := source.ValidateRef(doc, scene.SourceRef); err != nil {
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("scenes[%d].source_ref: %s", index, diagnostics.Truncate(err.Error()))
|
||||
if err := documentIndex.ValidateRef(scene.SourceRef); err != nil {
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("scenes[%d].source_ref: %s", sceneIndex, diagnostics.Truncate(err.Error()))
|
||||
}
|
||||
output.Scenes[index] = scene
|
||||
output.Scenes[sceneIndex] = scene
|
||||
}
|
||||
|
||||
sort.SliceStable(output.Scenes, func(left, right int) bool {
|
||||
leftStart := unitPositions[output.Scenes[left].SourceRef.StartUnitID]
|
||||
rightStart := unitPositions[output.Scenes[right].SourceRef.StartUnitID]
|
||||
leftStart, _ := documentIndex.Position(output.Scenes[left].SourceRef.StartUnitID)
|
||||
rightStart, _ := documentIndex.Position(output.Scenes[right].SourceRef.StartUnitID)
|
||||
if leftStart != rightStart {
|
||||
return leftStart < rightStart
|
||||
}
|
||||
|
||||
@@ -97,9 +97,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
||||
return contracts.TypedNormalizeResult[dnd.SpellList]{}, normalizerErrorf("context error before normalize: %w", err)
|
||||
}
|
||||
|
||||
order := shared.NewSourceRefOrder(req.Source)
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
order := shared.NewSourceRefOrderWithIndex(req.Source, index)
|
||||
value, warnings := normalizeSpellList(req.MergeOutput.Value, n.effectiveCatalog, order)
|
||||
value, duplicateWarnings := collapseDuplicateSpellCasts(value, req.Source, n.effectiveCatalog)
|
||||
value, duplicateWarnings := collapseDuplicateSpellCasts(value, index, n.effectiveCatalog)
|
||||
warnings = append(warnings, duplicateWarnings...)
|
||||
return contracts.TypedNormalizeResult[dnd.SpellList]{Value: value, Warnings: warnings}, nil
|
||||
}
|
||||
@@ -173,7 +174,7 @@ type duplicateGroup struct {
|
||||
removed []int
|
||||
}
|
||||
|
||||
func collapseDuplicateSpellCasts(input dnd.SpellList, doc *source.SourceDocument, catalog spellcatalog.EffectiveCatalog) (dnd.SpellList, []contracts.Warning) {
|
||||
func collapseDuplicateSpellCasts(input dnd.SpellList, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (dnd.SpellList, []contracts.Warning) {
|
||||
if len(input.SpellCasts) == 0 {
|
||||
return input, nil
|
||||
}
|
||||
@@ -182,7 +183,7 @@ func collapseDuplicateSpellCasts(input dnd.SpellList, doc *source.SourceDocument
|
||||
groups := make([]duplicateGroup, 0)
|
||||
groupByKey := make(map[string]int)
|
||||
for index, cast := range input.SpellCasts {
|
||||
key, eligible := duplicateKey(cast, doc, catalog)
|
||||
key, eligible := duplicateKey(cast, documentIndex, catalog)
|
||||
if !eligible {
|
||||
keep[index] = true
|
||||
continue
|
||||
@@ -225,13 +226,13 @@ func collapseDuplicateSpellCasts(input dnd.SpellList, doc *source.SourceDocument
|
||||
return output, warnings
|
||||
}
|
||||
|
||||
func duplicateKey(cast dnd.SpellCast, doc *source.SourceDocument, catalog spellcatalog.EffectiveCatalog) (string, bool) {
|
||||
func duplicateKey(cast dnd.SpellCast, documentIndex source.DocumentIndex, catalog spellcatalog.EffectiveCatalog) (string, bool) {
|
||||
canonicalName, resolved := catalog.Lookup(cast.Spell)
|
||||
if !resolved || len(cast.SourceRefs) == 0 {
|
||||
return "", false
|
||||
}
|
||||
for _, ref := range cast.SourceRefs {
|
||||
if source.ValidateRef(doc, ref) != nil {
|
||||
if documentIndex.ValidateRef(ref) != nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ func Less(order shared.SourceRefOrder, left, right dnd.NPCInteraction) bool {
|
||||
|
||||
// ValidSourceRefs reports whether an interaction has non-empty, valid
|
||||
// current-document evidence.
|
||||
func ValidSourceRefs(doc *source.SourceDocument, refs []source.SourceRef) bool {
|
||||
func ValidSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool {
|
||||
if len(refs) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, ref := range refs {
|
||||
if source.ValidateRef(doc, ref) != nil {
|
||||
if index.ValidateRef(ref) != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,11 @@ func (ref UnitRef) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(ref.String())
|
||||
}
|
||||
|
||||
func ResolveUnitID(doc *source.SourceDocument, field string, ref UnitRef) (int, error) {
|
||||
func ResolveUnitID(index source.DocumentIndex, field string, ref UnitRef) (int, error) {
|
||||
if ref.value <= 0 {
|
||||
return 0, fmt.Errorf("%s must be positive", field)
|
||||
}
|
||||
if _, ok := source.UnitIndex(doc, ref.value); !ok {
|
||||
if _, ok := index.Position(ref.value); !ok {
|
||||
return 0, fmt.Errorf("%s %d was not found", field, ref.value)
|
||||
}
|
||||
return ref.value, nil
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestUnitRefUnmarshalRejectsNonIntegerValues(t *testing.T) {
|
||||
func TestResolveUnitIDReturnsExistingIntegerSourceUnitID(t *testing.T) {
|
||||
doc := unitRefSourceDocument(2, 10)
|
||||
|
||||
got, err := ResolveUnitID(doc, "start_unit_id", UnitRefFromInt(2))
|
||||
got, err := ResolveUnitID(source.NewDocumentIndex(doc), "start_unit_id", UnitRefFromInt(2))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveUnitID() error = %v, want nil", err)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func TestResolveUnitIDReturnsExistingIntegerSourceUnitID(t *testing.T) {
|
||||
func TestResolveUnitIDDoesNotFallbackToOneBasedUnitNumber(t *testing.T) {
|
||||
doc := unitRefSourceDocument(10, 20)
|
||||
|
||||
_, err := ResolveUnitID(doc, "end_unit_id", UnitRefFromInt(2))
|
||||
_, err := ResolveUnitID(source.NewDocumentIndex(doc), "end_unit_id", UnitRefFromInt(2))
|
||||
if err == nil {
|
||||
t.Fatal("ResolveUnitID() error = nil, want missing source-unit ID")
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func TestResolveUnitIDDoesNotFallbackToOneBasedUnitNumber(t *testing.T) {
|
||||
func TestResolveUnitIDRejectsMissingUnit(t *testing.T) {
|
||||
doc := unitRefSourceDocument(1)
|
||||
|
||||
_, err := ResolveUnitID(doc, "start_unit_id", UnitRefFromInt(9))
|
||||
_, err := ResolveUnitID(source.NewDocumentIndex(doc), "start_unit_id", UnitRefFromInt(9))
|
||||
if err == nil {
|
||||
t.Fatal("ResolveUnitID() error = nil, want error")
|
||||
}
|
||||
|
||||
@@ -107,10 +107,8 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
|
||||
func allSourceRefsValid(index source.DocumentIndex, value dnd.NPCInteractionList) bool {
|
||||
for _, interaction := range value.Interactions {
|
||||
for _, ref := range interaction.SourceRefs {
|
||||
if index.ValidateRef(ref) != nil {
|
||||
return false
|
||||
}
|
||||
if !interactionmodel.ValidSourceRefs(index, interaction.SourceRefs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user