Migrate NPC registry durable contract

This commit is contained in:
2026-08-05 18:25:04 +00:00
parent 916a32195b
commit 9653e06297
56 changed files with 256 additions and 237 deletions

View File

@@ -327,7 +327,7 @@ func referenceSlots() []contracts.ReferenceSlot {
Name: NPCRegistryReferenceSlot,
Description: "Optional normalized NPC registry used for canonical actor grounding.",
AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
MaxBytes: NPCRegistryMaxBytes,
}}
}

View File

@@ -310,7 +310,7 @@ func testDocument() *source.SourceDocument {
func npcReferences(t *testing.T) contracts.ReferenceSet {
t.Helper()
npcs := dnd.NPCList{NPCs: []dnd.NPC{
npcs := dnd.NPCRegistry{NPCs: []dnd.NPC{
{ID: identity.DeriveID("Aria"), Name: "Aria", SourceRefs: []source.SourceRef{{SourceID: "npc-run", StartUnitID: 1, EndUnitID: 1}}},
{ID: identity.DeriveID("Goblin"), Name: "Goblin", SourceRefs: []source.SourceRef{{SourceID: "npc-run", StartUnitID: 1, EndUnitID: 1}}},
}}

View File

@@ -256,7 +256,7 @@ func referenceSlots() []contracts.ReferenceSlot {
Description: "Required normalized NPC registry used only to canonicalize enemy-subject names, never as event evidence.",
Required: true,
AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
MaxBytes: NPCRegistryMaxBytes,
}})
}

View File

@@ -183,7 +183,7 @@ func testDocument() *source.SourceDocument {
func npcReferences(t *testing.T) contracts.ReferenceSet {
t.Helper()
value := dnd.NPCList{NPCs: []dnd.NPC{{
value := dnd.NPCRegistry{NPCs: []dnd.NPC{{
ID: identity.DeriveID("Ária"), Name: "Ária",
SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}},
}}}

View File

@@ -267,7 +267,7 @@ func referenceSlots() []contracts.ReferenceSlot {
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.",
Required: true,
AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
MaxBytes: NPCRegistryMaxBytes,
})
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })

View File

@@ -175,7 +175,7 @@ func testDocument() *source.SourceDocument {
func npcReferences(t *testing.T) contracts.ReferenceSet {
t.Helper()
value := dnd.NPCList{NPCs: []dnd.NPC{
value := dnd.NPCRegistry{NPCs: []dnd.NPC{
{ID: identity.DeriveID("Ária"), Name: "Ária", SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}}},
{ID: identity.DeriveID("Borin"), Name: "Borin", SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}}},
}}
@@ -184,7 +184,7 @@ func npcReferences(t *testing.T) contracts.ReferenceSet {
t.Fatalf("encode registry: %v", err)
}
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {
Slot: contracts.ReferenceSlot{Name: NPCRegistryReferenceSlot, Required: true, AcceptedMediaTypes: []string{npccodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind}, MaxBytes: NPCRegistryMaxBytes},
Slot: contracts.ReferenceSlot{Name: NPCRegistryReferenceSlot, Required: true, AcceptedMediaTypes: []string{npccodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind}, MaxBytes: NPCRegistryMaxBytes},
Items: []contracts.ReferenceItem{{SlotName: NPCRegistryReferenceSlot, MediaType: npccodec.MediaType, Content: content}},
}}}
}

View File

@@ -39,7 +39,7 @@ const (
var requiredCapabilities = []string{"merged"}
var providedCapabilities = []string{"normalized"}
var _ contracts.Normalizer[dnd.NPCList] = (*Normalizer)(nil)
var _ contracts.Normalizer[dnd.NPCRegistry] = (*Normalizer)(nil)
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)
@@ -102,18 +102,18 @@ func (n *Normalizer) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
}
}
func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.NPCList]) (contracts.TypedNormalizeResult[dnd.NPCList], error) {
func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.NPCRegistry]) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
if n == nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("normalizer must not be nil")
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("normalizer must not be nil")
}
if n.llm == nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("LLM client must not be nil")
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("LLM client must not be nil")
}
if ctx == nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("context must not be nil")
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("context must not be nil")
}
if err := ctx.Err(); err != nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("context error before normalize: %w", err)
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("context error before normalize: %w", err)
}
order := shared.NewSourceRefOrder(req.Source)
@@ -121,10 +121,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
deterministic := recordList(records)
materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius)
if err != nil {
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("build semantic context: %w", err)
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("build semantic context: %w", err)
}
if !ready {
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
}
var response entityreconcile.ProposalResponse
@@ -136,20 +136,20 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
return n.invalidStructuredResult(deterministic, warnings), nil
}
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("complete structured output: %w", err)
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("complete structured output: %w", err)
}
assessment := materials.Assess(response)
applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order)
warnings = append(warnings, semanticWarnings...)
if assessment.DiscardedGroups() == 0 {
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
}
return retryResult(recordList(applied), warnings, assessment), nil
}
func (n *Normalizer) invalidStructuredResult(value dnd.NPCList, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.NPCList] {
return contracts.TypedNormalizeResult[dnd.NPCList]{
func (n *Normalizer) invalidStructuredResult(value dnd.NPCRegistry, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.NPCRegistry] {
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{
Value: value,
Warnings: limitWarningsForRetry(warnings),
Retry: &contracts.NormalizeRetry{
@@ -160,8 +160,8 @@ func (n *Normalizer) invalidStructuredResult(value dnd.NPCList, warnings []contr
}
}
func retryResult(value dnd.NPCList, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.NPCList] {
return contracts.TypedNormalizeResult[dnd.NPCList]{
func retryResult(value dnd.NPCRegistry, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.NPCRegistry] {
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{
Value: value,
Warnings: limitWarningsForRetry(warnings),
Retry: &contracts.NormalizeRetry{
@@ -205,7 +205,7 @@ type normalizedRecord struct {
earliest int
}
func preprocessRecords(input dnd.NPCList, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
if input.NPCs == nil {
return nil, nil
}
@@ -328,11 +328,11 @@ func recordValues(records []normalizedRecord) []dnd.NPC {
return values
}
func recordList(records []normalizedRecord) dnd.NPCList {
func recordList(records []normalizedRecord) dnd.NPCRegistry {
if records == nil {
return dnd.NPCList{}
return dnd.NPCRegistry{}
}
return dnd.NPCList{NPCs: recordValues(records)}
return dnd.NPCRegistry{NPCs: recordValues(records)}
}
func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
@@ -362,11 +362,11 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCListKind}
return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCRegistryKind}
}
func Register(registry *pipeline.NormalizerRegistry) error {
return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.NPCList], error) {
return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.NPCRegistry], error) {
options, err := DecodeOptions(request.Options)
if err != nil {
return nil, err

View File

@@ -22,7 +22,7 @@ func TestModuleContractAndIdentity(t *testing.T) {
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
t.Fatal("DecodeOptions() accepted unknown option")
}
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCListKind}
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCRegistryKind}
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
}
@@ -44,7 +44,7 @@ func TestModuleContractAndIdentity(t *testing.T) {
}
func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
input := dnd.NPCList{NPCs: []dnd.NPC{{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{
ID: "wrong", Name: " Lady\tAsh ", SourceRefs: []source.SourceRef{
{SourceID: "b", StartUnitID: 2, EndUnitID: 3},
{SourceID: "a", StartUnitID: 4, EndUnitID: 4},
@@ -68,7 +68,7 @@ func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
doc := &source.SourceDocument{ID: "source", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Lady Ash", SourceRefs: []source.SourceRef{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Lady Ash", SourceRefs: []source.SourceRef{
{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999},
@@ -84,7 +84,7 @@ func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
}
func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T) {
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{Name: " Captain Vale ", SourceRefs: []source.SourceRef{{SourceID: "a", StartUnitID: 1, EndUnitID: 1}}},
{Name: "captain vale", SourceRefs: []source.SourceRef{{SourceID: "b", StartUnitID: 2, EndUnitID: 2}}},
{Name: "The Captain", SourceRefs: []source.SourceRef{{SourceID: "c", StartUnitID: 3, EndUnitID: 3}}},
@@ -105,8 +105,8 @@ func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T)
}
func TestNormalizePreservesInvalidCandidatesAndDoesNotAliasInput(t *testing.T) {
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
before := dnd.NPCList{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
before := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
if err != nil || !reflect.DeepEqual(input, before) {
t.Fatalf("Normalize() = %#v, %v; input mutated to %#v", result, err, input)
@@ -122,13 +122,13 @@ func TestNormalizePreservesInvalidCandidatesAndDoesNotAliasInput(t *testing.T) {
func TestNormalizeHandlesNilAndCanceledCalls(t *testing.T) {
normalizer := newNormalizer(t, &recordingNPCNormalizerClient{})
result, err := normalizer.Normalize(context.Background(), normalizeRequest(dnd.NPCList{NPCs: nil}))
result, err := normalizer.Normalize(context.Background(), normalizeRequest(dnd.NPCRegistry{NPCs: nil}))
if err != nil || result.Value.NPCs != nil {
t.Fatalf("nil list result = %#v, error = %v", result.Value, err)
}
canceled, cancel := context.WithCancel(context.Background())
cancel()
if _, err := normalizer.Normalize(canceled, normalizeRequest(dnd.NPCList{})); err == nil || !strings.Contains(err.Error(), "context error") {
if _, err := normalizer.Normalize(canceled, normalizeRequest(dnd.NPCRegistry{})); err == nil || !strings.Contains(err.Error(), "context error") {
t.Fatalf("canceled Normalize() error = %v", err)
}
}
@@ -171,11 +171,11 @@ func newNormalizer(t *testing.T, client contracts.StructuredLLMClient) *Normaliz
return normalizer
}
func normalizeRequest(value dnd.NPCList) contracts.TypedNormalizeRequest[dnd.NPCList] {
return contracts.TypedNormalizeRequest[dnd.NPCList]{MergeOutput: contracts.MergeArtifact[dnd.NPCList]{Value: value}}
func normalizeRequest(value dnd.NPCRegistry) contracts.TypedNormalizeRequest[dnd.NPCRegistry] {
return contracts.TypedNormalizeRequest[dnd.NPCRegistry]{MergeOutput: contracts.MergeArtifact[dnd.NPCRegistry]{Value: value}}
}
func normalizeRequestWithSource(value dnd.NPCList, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.NPCList] {
func normalizeRequestWithSource(value dnd.NPCRegistry, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.NPCRegistry] {
request := normalizeRequest(value)
request.Source = doc
return request

View File

@@ -19,7 +19,7 @@ func TestNormalizeSkipsSemanticCompletionWithoutTwoEligibleCandidates(t *testing
client := &recordingNPCNormalizerClient{}
normalizer := newNormalizer(t, client)
doc := semanticDocument()
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}}}
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}}}
result, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil || len(client.requests) != 0 || result.Retry != nil {
t.Fatalf("Normalize() = %#v, %v; calls = %d, want deterministic no-call result", result, err, len(client.requests))
@@ -33,12 +33,12 @@ func TestNormalizeAppliesSafeProposalAndUsesPrivateInputs(t *testing.T) {
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
normalizer := newNormalizer(t, client)
doc := semanticDocument()
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{ID: "npc:sha256:short", Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{ID: "npc:sha256:long", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
{ID: "npc:sha256:captain", Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
}}
before := cloneNPCList(input)
before := cloneNPCRegistry(input)
request := normalizeRequestWithSource(input, doc)
request.LLMProfile = "normalizer-profile"
request.SessionID = "normalizer-session"
@@ -76,7 +76,7 @@ func TestNormalizeUnsafeProposalReturnsSafeRetryFallback(t *testing.T) {
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"},{"members":["candidate-000003","unknown"],"canonical":"candidate-000003"}]}`}
normalizer := newNormalizer(t, client)
doc := semanticDocument()
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
@@ -98,7 +98,7 @@ func TestNormalizeUnsafeProposalReturnsSafeRetryFallback(t *testing.T) {
func TestNormalizeInvalidStructuredOutputAndOperationalErrorsRemainDistinct(t *testing.T) {
doc := semanticDocument()
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
}}
@@ -129,7 +129,7 @@ func TestNormalizeRedactsContextMaterialFailures(t *testing.T) {
{ID: 10, Kind: "speech", Text: transcript, Metadata: map[string]any{metadataKey: math.NaN(), "value": metadataValue}},
{ID: 20, Kind: "speech", Text: "other context"},
}}
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{Name: firstName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: secondName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
}}
@@ -156,7 +156,7 @@ func TestNormalizeDoesNotAccumulateSafeGroupsAcrossAttempts(t *testing.T) {
}}
normalizer := newNormalizer(t, client)
doc := semanticDocument()
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
@@ -178,7 +178,7 @@ func TestNormalizeExcludesIneligibleRecordsFromSemanticGroups(t *testing.T) {
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000001"}]}`}
normalizer := newNormalizer(t, client)
doc := semanticDocument()
input := dnd.NPCList{NPCs: []dnd.NPC{
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: "Broken", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999}}},
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
@@ -235,8 +235,8 @@ func semanticDocument() *source.SourceDocument {
}}
}
func cloneNPCList(input dnd.NPCList) dnd.NPCList {
output := dnd.NPCList{NPCs: make([]dnd.NPC, len(input.NPCs))}
func cloneNPCRegistry(input dnd.NPCRegistry) dnd.NPCRegistry {
output := dnd.NPCRegistry{NPCs: make([]dnd.NPC, len(input.NPCs))}
for index, npc := range input.NPCs {
output.NPCs[index] = cloneNPC(npc)
}