From a26d6ed042b100ca6f1d35bcaebfed4c1a292d00 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 26 Aug 2026 23:59:05 +0000 Subject: [PATCH] Enable correction-aware downstream D&D extractors --- docs/roadmap/implementation.md | 2 +- .../dnd/extract/combatturns/extractor.go | 27 ++++++++++------ .../dnd/extract/combatturns/extractor_test.go | 2 +- .../dnd/extract/enemyevents/extractor.go | 28 ++++++++++------ .../dnd/extract/enemyevents/extractor_test.go | 2 +- .../dnd/extract/itemoccurrences/extractor.go | 29 ++++++++++------- .../extract/itemoccurrences/extractor_test.go | 32 ++++++++++++++++--- .../extract/itemoccurrences/registry_test.go | 3 ++ .../extract/locationoccurrences/extractor.go | 17 +++++++--- .../locationoccurrences/extractor_test.go | 2 +- .../dnd/extract/npcoccurrences/extractor.go | 27 ++++++++++------ .../extract/npcoccurrences/extractor_test.go | 2 +- 12 files changed, 118 insertions(+), 55 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 7159d099..796d20e5 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -347,7 +347,7 @@ All named producers truthfully advertise and implement the exact-response protocol, with unchanged ordinary extraction behavior. This stage is one Terra prompt. -## Stage 7 — Migrate Downstream D&D Extractors +## Stage 7 — Migrate Downstream D&D Extractors ✅ ### Goal diff --git a/internal/modules/dnd/extract/combatturns/extractor.go b/internal/modules/dnd/extract/combatturns/extractor.go index 38027ea5..aa690316 100644 --- a/internal/modules/dnd/extract/combatturns/extractor.go +++ b/internal/modules/dnd/extract/combatturns/extractor.go @@ -208,30 +208,37 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe var response extractionResponse inputs := shared.PromptInputs(sourceInput, req.References) inputs[NPCRegistryReferenceSlot] = npcRegistry.PromptInput() - if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ + completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, + Correction: req.Correction, Inputs: inputs, - }, &response); err != nil { + }, &response) + if err != nil { return contracts.TypedExtractionResult[dnd.CombatTurnList]{}, extractorErrorf("complete structured output: %w", err) } + candidate, err := shared.ModelCandidateFromResponse(completion) + if err != nil { + return contracts.TypedExtractionResult[dnd.CombatTurnList]{}, extractorErrorf("capture model candidate: %w", err) + } canonicalizeResponse(&response, order, req.Source.ID) - return contracts.TypedExtractionResult[dnd.CombatTurnList]{Value: canonicalCombatTurnList(response, req.Source.ID)}, nil + return contracts.TypedExtractionResult[dnd.CombatTurnList]{Value: canonicalCombatTurnList(response, req.Source.ID), ModelCandidate: candidate}, nil } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageExtract, - ExecutionClass: contracts.ExecutionClassLLMBacked, - Requires: append([]string(nil), requiredCapabilities...), - Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.CombatTurnListKind, - ReferenceSlots: referenceSlots(), + Key: Key, + Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, + CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, + Requires: append([]string(nil), requiredCapabilities...), + Provides: append([]string(nil), providedCapabilities...), + ArtifactKind: dnd.CombatTurnListKind, + ReferenceSlots: referenceSlots(), } } diff --git a/internal/modules/dnd/extract/combatturns/extractor_test.go b/internal/modules/dnd/extract/combatturns/extractor_test.go index e766f4de..f1fa4c4c 100644 --- a/internal/modules/dnd/extract/combatturns/extractor_test.go +++ b/internal/modules/dnd/extract/combatturns/extractor_test.go @@ -448,7 +448,7 @@ func TestExtractorManifestMetadataAndFingerprints(t *testing.T) { func TestModuleSpecAndRegistration(t *testing.T) { wantSlots := referenceSlots() got := ModuleSpec() - if got.Key != Key || got.Stage != pipeline.StageExtract || got.ArtifactKind != dnd.CombatTurnListKind || !reflect.DeepEqual(got.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(got.Provides, []string{"dnd.combat_turns"}) || !reflect.DeepEqual(got.ReferenceSlots, wantSlots) { + if got.Key != Key || got.Stage != pipeline.StageExtract || got.CorrectionProtocol != contracts.CorrectionProtocolSingleResponseV1 || got.ArtifactKind != dnd.CombatTurnListKind || !reflect.DeepEqual(got.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(got.Provides, []string{"dnd.combat_turns"}) || !reflect.DeepEqual(got.ReferenceSlots, wantSlots) { t.Fatalf("ModuleSpec() = %#v, want combat extractor contract", got) } got.Requires[0] = "changed" diff --git a/internal/modules/dnd/extract/enemyevents/extractor.go b/internal/modules/dnd/extract/enemyevents/extractor.go index 0f789c86..83851d4f 100644 --- a/internal/modules/dnd/extract/enemyevents/extractor.go +++ b/internal/modules/dnd/extract/enemyevents/extractor.go @@ -142,19 +142,26 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe inputs[name] = input } var response extractionResponse - if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ + completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, + Correction: req.Correction, Inputs: inputs, - }, &response); err != nil { + }, &response) + if err != nil { return contracts.TypedExtractionResult[dnd.EnemyEventList]{}, extractorErrorf("complete structured output: %w", err) } + candidate, err := shared.ModelCandidateFromResponse(completion) + if err != nil { + return contracts.TypedExtractionResult[dnd.EnemyEventList]{}, extractorErrorf("capture model candidate: %w", err) + } return contracts.TypedExtractionResult[dnd.EnemyEventList]{ - Value: canonicalEnemyEventList(response, shared.NewSourceRefOrder(req.Source), req.Source.ID), + Value: canonicalEnemyEventList(response, shared.NewSourceRefOrder(req.Source), req.Source.ID), + ModelCandidate: candidate, }, nil } @@ -174,13 +181,14 @@ func unavailableSceneResult() contracts.TypedExtractionResult[dnd.EnemyEventList func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageExtract, - ExecutionClass: contracts.ExecutionClassLLMBacked, - Requires: append([]string(nil), requiredCapabilities...), - Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.EnemyEventListKind, - ReferenceSlots: referenceSlots(), + Key: Key, + Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, + CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, + Requires: append([]string(nil), requiredCapabilities...), + Provides: append([]string(nil), providedCapabilities...), + ArtifactKind: dnd.EnemyEventListKind, + ReferenceSlots: referenceSlots(), } } diff --git a/internal/modules/dnd/extract/enemyevents/extractor_test.go b/internal/modules/dnd/extract/enemyevents/extractor_test.go index 6da25d11..10db980f 100644 --- a/internal/modules/dnd/extract/enemyevents/extractor_test.go +++ b/internal/modules/dnd/extract/enemyevents/extractor_test.go @@ -137,7 +137,7 @@ func TestConstructorSpecOptionsAndSafeMetadata(t *testing.T) { first.Requires[0] = "changed" first.ReferenceSlots[0].AcceptedMediaTypes[0] = "changed" second := ModuleSpec() - if second.Requires[0] != "chunks" || second.ArtifactKind != dnd.EnemyEventListKind || second.ExecutionClass != contracts.ExecutionClassLLMBacked || second.ReferenceSlots[0].AcceptedMediaTypes[0] == "changed" { + if second.Requires[0] != "chunks" || second.ArtifactKind != dnd.EnemyEventListKind || second.ExecutionClass != contracts.ExecutionClassLLMBacked || second.CorrectionProtocol != contracts.CorrectionProtocolSingleResponseV1 || second.ReferenceSlots[0].AcceptedMediaTypes[0] == "changed" { t.Fatalf("ModuleSpec() reused mutable state: %#v", second) } registry := pipeline.NewExtractorRegistry() diff --git a/internal/modules/dnd/extract/itemoccurrences/extractor.go b/internal/modules/dnd/extract/itemoccurrences/extractor.go index 6db36d93..5d9a3c14 100644 --- a/internal/modules/dnd/extract/itemoccurrences/extractor.go +++ b/internal/modules/dnd/extract/itemoccurrences/extractor.go @@ -155,28 +155,35 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe var response extractionResponse inputs := shared.PromptInputs(sourceInput, req.References) inputs[ItemRegistryReferenceSlot] = registry.PromptInput() - if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ + completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion, - ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, Inputs: inputs, - }, &response); err != nil { + ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, + Correction: req.Correction, Inputs: inputs, + }, &response) + if err != nil { return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("complete structured output: %w", err) } + candidate, err := shared.ModelCandidateFromResponse(completion) + if err != nil { + return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("capture model candidate: %w", err) + } value, err := canonicalItemOccurrenceList(response, order, req.Source.ID, registry) if err != nil { return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("map item occurrence response: %w", err) } - return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{Value: value}, nil + return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{Value: value, ModelCandidate: candidate}, nil } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageExtract, - ExecutionClass: contracts.ExecutionClassLLMBacked, - Requires: append([]string(nil), requiredCapabilities...), - Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.ItemOccurrenceListKind, - ReferenceSlots: referenceSlots(), + Key: Key, + Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, + CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, + Requires: append([]string(nil), requiredCapabilities...), + Provides: append([]string(nil), providedCapabilities...), + ArtifactKind: dnd.ItemOccurrenceListKind, + ReferenceSlots: referenceSlots(), } } diff --git a/internal/modules/dnd/extract/itemoccurrences/extractor_test.go b/internal/modules/dnd/extract/itemoccurrences/extractor_test.go index 59610247..7d7c291a 100644 --- a/internal/modules/dnd/extract/itemoccurrences/extractor_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/extractor_test.go @@ -11,11 +11,15 @@ import ( func TestExtractGroundsOccurrencesInRequiredRegistry(t *testing.T) { id := itemidentity.DeriveID("Torch") - client := &fakeItemOccurrencesLLMClient{response: extractionResponse{Occurrences: []itemOccurrenceResponse{ - {Name: "Torch", Kind: "lost", From: "party", SourceRefs: responseRefs(1, 1)}, - }}} + rawResponse := []byte(`{"occurrences":[{"name":"Torch","kind":"lost","from":"party","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`) + client := &fakeItemOccurrencesLLMClient{content: append([]byte(nil), rawResponse...)} req := extractionRequest() req.References = itemRegistryReferences(t) + correction, err := contracts.NewSemanticCorrection([]byte(`{"occurrences":[]}`), "Keep the transcript-grounded item occurrence.") + if err != nil { + t.Fatalf("NewSemanticCorrection() error = %v", err) + } + req.Correction = correction result, err := newExtractor(t, client, req.References).Extract(context.Background(), req) if err != nil { t.Fatal(err) @@ -26,10 +30,30 @@ func TestExtractGroundsOccurrencesInRequiredRegistry(t *testing.T) { if refs := result.Value.Occurrences[0].SourceRefs; len(refs) != 1 || refs[0].SourceID != req.Source.ID || refs[0].StartUnitID != 1 || refs[0].EndUnitID != 1 { t.Fatalf("occurrence evidence = %#v, want current-source unit range", refs) } - input := client.requests[0].Inputs[ItemRegistryReferenceSlot] + request := client.requests[0] + input := request.Inputs[ItemRegistryReferenceSlot] if input.Name != ItemRegistryReferenceSlot || string(input.Content) != `{"items":[{"name":"Torch"}]}` || strings.Contains(string(input.Content), "item:sha256:") { t.Fatalf("registry prompt input = %#v, want names-only projection", input) } + if request.Correction == nil || string(request.Correction.AssistantResponse) != `{"occurrences":[]}` || request.Correction.UserGuidance != "Keep the transcript-grounded item occurrence." { + t.Fatalf("correction = %#v, want exact request correction", request.Correction) + } + for _, material := range []string{string(request.Correction.AssistantResponse), request.Correction.UserGuidance} { + if strings.Contains(material, id) || strings.Contains(material, "item:sha256:") { + t.Fatalf("correction material leaked opaque item identity: %q", material) + } + } + correction.AssistantResponse[0] = '[' + if got := string(request.Correction.AssistantResponse); got != `{"occurrences":[]}` { + t.Fatalf("captured correction changed after caller mutation: %q", got) + } + if result.ModelCandidate == nil || result.ModelCandidate.Protocol != contracts.CorrectionProtocolSingleResponseV1 || string(result.ModelCandidate.Response) != string(rawResponse) { + t.Fatalf("model candidate = %#v, want exact validated response", result.ModelCandidate) + } + client.content[0] = '[' + if got := string(result.ModelCandidate.Response); got != string(rawResponse) { + t.Fatalf("model candidate changed after provider buffer mutation: %q", got) + } } func TestExtractCanonicalizesComparisonEquivalentNames(t *testing.T) { diff --git a/internal/modules/dnd/extract/itemoccurrences/registry_test.go b/internal/modules/dnd/extract/itemoccurrences/registry_test.go index c3d9793a..74242069 100644 --- a/internal/modules/dnd/extract/itemoccurrences/registry_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/registry_test.go @@ -36,6 +36,9 @@ func testSourceRefs() []source.SourceRef { func TestModuleSpecDeclaresRequiredRegistry(t *testing.T) { spec := ModuleSpec() + if spec.CorrectionProtocol != contracts.CorrectionProtocolSingleResponseV1 { + t.Fatalf("correction protocol = %q, want %q", spec.CorrectionProtocol, contracts.CorrectionProtocolSingleResponseV1) + } var slot contracts.ReferenceSlot for _, candidate := range spec.ReferenceSlots { if candidate.Name == ItemRegistryReferenceSlot { diff --git a/internal/modules/dnd/extract/locationoccurrences/extractor.go b/internal/modules/dnd/extract/locationoccurrences/extractor.go index 9c93f7bd..1faa569f 100644 --- a/internal/modules/dnd/extract/locationoccurrences/extractor.go +++ b/internal/modules/dnd/extract/locationoccurrences/extractor.go @@ -151,23 +151,30 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe var response extractionResponse inputs := shared.PromptInputs(sourceInput, req.References) inputs[LocationRegistryReferenceSlot] = grounding.PromptInput() - if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ + completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion, - ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, Inputs: inputs, - }, &response); err != nil { + ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, + Correction: req.Correction, Inputs: inputs, + }, &response) + if err != nil { return contracts.TypedExtractionResult[dnd.LocationOccurrenceList]{}, extractorErrorf("complete structured output: %w", err) } + candidate, err := shared.ModelCandidateFromResponse(completion) + if err != nil { + return contracts.TypedExtractionResult[dnd.LocationOccurrenceList]{}, extractorErrorf("capture model candidate: %w", err) + } occurrences, err := canonicalOccurrenceList(response, shared.NewSourceRefOrder(req.Source), req.Source.ID, grounding) if err != nil { return contracts.TypedExtractionResult[dnd.LocationOccurrenceList]{}, extractorErrorf("resolve location grounding: %w", err) } - return contracts.TypedExtractionResult[dnd.LocationOccurrenceList]{Value: occurrences}, nil + return contracts.TypedExtractionResult[dnd.LocationOccurrenceList]{Value: occurrences, ModelCandidate: candidate}, nil } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, - Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), + CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, + Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.LocationOccurrenceListKind, ReferenceSlots: referenceSlots(), } } diff --git a/internal/modules/dnd/extract/locationoccurrences/extractor_test.go b/internal/modules/dnd/extract/locationoccurrences/extractor_test.go index 6a83fa11..2312eb24 100644 --- a/internal/modules/dnd/extract/locationoccurrences/extractor_test.go +++ b/internal/modules/dnd/extract/locationoccurrences/extractor_test.go @@ -212,7 +212,7 @@ func TestExtractorContractsMetadataAndFailures(t *testing.T) { } spec := ModuleSpec() - if spec.Key != Key || spec.Stage != pipeline.StageExtract || spec.ExecutionClass != contracts.ExecutionClassLLMBacked || spec.ArtifactKind != dnd.LocationOccurrenceListKind { + if spec.Key != Key || spec.Stage != pipeline.StageExtract || spec.ExecutionClass != contracts.ExecutionClassLLMBacked || spec.CorrectionProtocol != contracts.CorrectionProtocolSingleResponseV1 || spec.ArtifactKind != dnd.LocationOccurrenceListKind { t.Fatalf("ModuleSpec() = %#v", spec) } var slot contracts.ReferenceSlot diff --git a/internal/modules/dnd/extract/npcoccurrences/extractor.go b/internal/modules/dnd/extract/npcoccurrences/extractor.go index 3bba2a1d..f2352b63 100644 --- a/internal/modules/dnd/extract/npcoccurrences/extractor.go +++ b/internal/modules/dnd/extract/npcoccurrences/extractor.go @@ -159,34 +159,41 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe var response extractionResponse inputs := shared.PromptInputs(sourceInput, req.References) inputs[NPCRegistryReferenceSlot] = npcRegistry.PromptInput() - if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ + completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{ StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts, + Correction: req.Correction, Inputs: inputs, - }, &response); err != nil { + }, &response) + if err != nil { return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("complete structured output: %w", err) } + candidate, err := shared.ModelCandidateFromResponse(completion) + if err != nil { + return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("capture model candidate: %w", err) + } canonicalizeResponse(&response, order, req.Source.ID) value, err := canonicalOccurrenceList(response, req.Source.ID, npcRegistry) if err != nil { return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("resolve NPC names against registry: %w", err) } - return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{Value: value}, nil + return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{Value: value, ModelCandidate: candidate}, nil } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageExtract, - ExecutionClass: contracts.ExecutionClassLLMBacked, - Requires: append([]string(nil), requiredCapabilities...), - Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.NPCOccurrenceListKind, - ReferenceSlots: referenceSlots(), + Key: Key, + Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, + CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, + Requires: append([]string(nil), requiredCapabilities...), + Provides: append([]string(nil), providedCapabilities...), + ArtifactKind: dnd.NPCOccurrenceListKind, + ReferenceSlots: referenceSlots(), } } diff --git a/internal/modules/dnd/extract/npcoccurrences/extractor_test.go b/internal/modules/dnd/extract/npcoccurrences/extractor_test.go index 8e018694..3db78ee8 100644 --- a/internal/modules/dnd/extract/npcoccurrences/extractor_test.go +++ b/internal/modules/dnd/extract/npcoccurrences/extractor_test.go @@ -285,7 +285,7 @@ func TestExtractRejectsInvalidRequestsAndProviderFailures(t *testing.T) { func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) { got := ModuleSpec() - if got.Key != Key || got.Stage != pipeline.StageExtract || got.ArtifactKind != dnd.NPCOccurrenceListKind || !reflect.DeepEqual(got.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(got.Provides, []string{"dnd.npc_occurrences"}) { + if got.Key != Key || got.Stage != pipeline.StageExtract || got.CorrectionProtocol != contracts.CorrectionProtocolSingleResponseV1 || got.ArtifactKind != dnd.NPCOccurrenceListKind || !reflect.DeepEqual(got.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(got.Provides, []string{"dnd.npc_occurrences"}) { t.Fatalf("ModuleSpec() = %#v", got) } var registrySlot contracts.ReferenceSlot