diff --git a/docs/internal/dnd.md b/docs/internal/dnd.md index 5f41d4b..e62afb7 100644 --- a/docs/internal/dnd.md +++ b/docs/internal/dnd.md @@ -43,59 +43,63 @@ the contracts above define durable data. ## Prompt Construction -D&D LLM-facing content lives beneath `assets/dnd/`. New extractor content uses -its feature subtree; when a family has both extraction and normalization -content, keep those in its `extract` and `normalize` subtrees. The owning module -still defines the ordered manifest and registers the resulting scoped filesystem. -Shared fragments belong to the D&D shared implementation and are selected by -name, never copied into individual module subtrees. +D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a +local `prompt.yaml` declaration and `instructions.md`; input-specific files +such as a catalog, registry, grounding projection, or candidate collection are +local only when that module needs them. New extractor content uses its feature +subtree, while families with both extraction and normalization content use their +`extract` and `normalize` subtrees. Shared visual-provenance fragments use +the `common-dnd-` prefix. -D&D extractors assemble prompts from an ordered manifest of shared and -module-selected assets. The location extractor and occurrence extractor reuse -the shared D&D system, evidence, identity, reference, and transcript assets -instead of copying their text into individual modules. A manifest’s declared -sequence, including cache-control placement, is part of the prompt behavior. +The owning module’s manifest is the source of truth for which local and shared +assets are selected, their mount paths, their message order, cache controls, +and the files included in its prompt fingerprint. Shared fragments belong to +the D&D shared implementation and are selected by name rather than copied into +module directories. The root `assets` package is a content-only boundary; its +physical ownership and rationale are defined by +[ADR-0011](../adr/0011-centralize-llm-assets.md). + +Put each rule at its narrowest owner: + +- universal behavior belongs in the shared system asset; +- D&D-family behavior belongs in a selected `common-dnd-` asset; +- rules for an input projection belong with that input asset; +- lane-specific policy belongs in the module’s `instructions.md`; and +- transport-envelope shape belongs in the private response schema. + +A rule is eligible for the system prompt only when every D&D LLM prompt needs +it regardless of lane, inputs, or response shape. Module instructions must not +repeat rules selected from shared assets or schemas. Reintroduce such repetition +only after observational evaluation with representative transcripts shows that +it improves results at the intended target models and cost; structural prompt +tests alone are not that evidence. Every maintained D&D LLM prompt selects `dnd-extraction` as its default -profile. The D&D registrar registers that fallback profile with the maintained -OpenRouter model, timeout, and service-tier policy. An operator may provide a -complete profile with the same ID through the configured PromptKit source; that -definition replaces the fallback rather than merging with it. The fallback -leaves reasoning and optional sampling controls unspecified. Deployment profile -selection and the maintained operator example are documented in +profile. The D&D registrar registers the fallback, while an operator can +replace it with a complete profile of the same ID from the configured PromptKit +source. Deployment profile selection is documented in [Configuration](../config.md#promptkit-profiles). -All extraction prompts share this four-message rendered prefix: the system -message without cache control, the identity message without cache control, the -campaign-reference message with ephemeral cache control, and the chunk -transcript message with ephemeral cache control. This gives equivalent -extraction requests the same reusable prefix through their source material. +The transcript assets have distinct consumers. Scene chunking consumes the +complete-session `common-dnd-transcript-full.md`; extraction prompts consume +the current-chunk `common-dnd-transcript-chunk.md`; and NPC and location +normalization consume `common-dnd-transcript-windows.md` alongside their +candidate collections. Player, party, glossary, and compatible campaign +references provide disambiguating context, not evidence. Reference material is +canonically ordered before rendering so equivalent inputs remain stable. -Extraction-evidence policy, generated NPC registries, spell catalogs, module -tasks, and instructions follow the transcript because they are not universal -across all extraction lanes. The final instructions message carries ephemeral -cache control; evidence, registry, catalog, and task messages do not. Preserve -this division when changing an extractor or its assets so prompt-cache behavior -remains stable. +Extraction prompts render the common system and identity messages first, then +cached campaign references and the cached chunk transcript. Evidence policy and +any lane-specific registry, catalog, or grounding projection follow that +prefix. The final module instructions message is ephemeral. This keeps the +reusable extraction prefix identical while preserving the lane-specific suffix. -The other D&D LLM prompts intentionally follow different patterns. Scene -chunking has no sibling extraction lane with which to share its full transcript, -so it renders campaign references before its task and instructions, then places -the cacheable full transcript last. NPC and location normalization share the -entity-reconciliation response schema and safety boundary while retaining their -own task and identity rules. NPC normalization keeps its task and -cacheable instructions before the candidate collection, followed by the -cacheable transcript windows: candidates must be available before their -supporting evidence is evaluated, and those windows are not a cross-lane -prefix. Mounted assets and their declared message order determine the prompt -fingerprint, so intentional prompt edits continue to invalidate stale -checkpoints. - -All extractors use the shared prompt-input preparation rules. The current chunk -is copied into transcript material; player, party, glossary, and compatible -campaign references are context for disambiguation, not source evidence. -Reference prompt material is canonically ordered before it is rendered, which -keeps equivalent inputs stable across runs. +Scene chunking intentionally uses a different order: system, cached campaign +references, uncached module instructions, then the final ephemeral full +transcript. Entity normalization also has its own order: system, uncached +module instructions, ephemeral reconciliation policy, uncached candidates, and +final ephemeral transcript windows. These orders and cache controls are prompt +behavior; change them only through the owning manifest and prompt declaration. ## Evidence, Candidates, And Normalization diff --git a/internal/framework/llm/asset_registry_test.go b/internal/framework/llm/asset_registry_test.go index bbe45de..0c59a24 100644 --- a/internal/framework/llm/asset_registry_test.go +++ b/internal/framework/llm/asset_registry_test.go @@ -185,13 +185,11 @@ func TestAssetRegistryFallbackProfileDigestTracksContentWithoutLeakingIt(t *test func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) { registry := NewAssetRegistry() mustRegisterPromptFS(t, registry, fstest.MapFS{ - "dnd.spells/dnd.spells.yaml": {Data: []byte(validPromptYAML("schema.json"))}, - "dnd.spells/task.md": {Data: []byte("spell task")}, + "dnd.spells/prompt.yaml": {Data: []byte(validPromptYAML("schema.json"))}, "dnd.spells/instructions.md": {Data: []byte("spell instructions")}, }, ".") mustRegisterPromptFS(t, registry, fstest.MapFS{ - "dnd.scenes/dnd.scenes.yaml": {Data: []byte(validPromptYAML("schema.json"))}, - "dnd.scenes/task.md": {Data: []byte("scene task")}, + "dnd.scenes/prompt.yaml": {Data: []byte(validPromptYAML("schema.json"))}, "dnd.scenes/instructions.md": {Data: []byte("scene instructions")}, }, ".") @@ -200,11 +198,9 @@ func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) { t.Fatalf("PromptFS() error = %v, want nil", err) } for _, name := range []string{ - "dnd.spells/dnd.spells.yaml", - "dnd.spells/task.md", + "dnd.spells/prompt.yaml", "dnd.spells/instructions.md", - "dnd.scenes/dnd.scenes.yaml", - "dnd.scenes/task.md", + "dnd.scenes/prompt.yaml", "dnd.scenes/instructions.md", } { if _, err := fsys.Open(name); err != nil { diff --git a/internal/framework/promptfs/prompt_fs_test.go b/internal/framework/promptfs/prompt_fs_test.go index 38577a9..a894daf 100644 --- a/internal/framework/promptfs/prompt_fs_test.go +++ b/internal/framework/promptfs/prompt_fs_test.go @@ -14,11 +14,11 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) { "nested/transcript.md": {Data: []byte("transcript")}, } fsys, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/module.test.yaml": {Data: []byte("id: module.test")}, - "assets/prompts/task.md": {Data: []byte("task")}, + "assets/prompts/prompt.yaml": {Data: []byte("id: module.test")}, + "assets/prompts/instructions.md": {Data: []byte("instructions")}, }, []ModulePromptFile{ - {Name: "module.test.yaml", Path: "assets/prompts/module.test.yaml"}, - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }, SharedPromptFile{Name: "system.md", FS: sharedFS, Path: "shared/system.md"}, SharedPromptFile{Name: "reference.md", FS: sharedFS, Path: "shared/reference.md"}, @@ -29,8 +29,8 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) { } tests := map[string]string{ - "assets/prompts/module.test/module.test.yaml": "id: module.test", - "assets/prompts/module.test/task.md": "task", + "assets/prompts/module.test/prompt.yaml": "id: module.test", + "assets/prompts/module.test/instructions.md": "instructions", "assets/prompts/module.test/sharedassets/system.md": "system", "assets/prompts/module.test/sharedassets/reference.md": "reference", "assets/prompts/module.test/sharedassets/transcript.md": "transcript", @@ -63,18 +63,18 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) { func TestModulePromptFSRejectsMissingModuleFile(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{}, []ModulePromptFile{ - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }) - if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") { + if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/instructions.md") { t.Fatalf("ModulePromptFS() error = %v, want missing module asset context", err) } } func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/task.md": {Data: []byte("task")}, + "assets/prompts/instructions.md": {Data: []byte("instructions")}, }, []ModulePromptFile{ - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }, SharedPromptFile{Name: "system.md", FS: fstest.MapFS{}, Path: "shared/system.md"}) if err == nil || !strings.Contains(err.Error(), "read shared prompt asset shared/system.md") { t.Fatalf("ModulePromptFS() error = %v, want missing shared asset context", err) @@ -83,9 +83,9 @@ func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) { func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/task.md": {Data: []byte("task")}, + "assets/prompts/instructions.md": {Data: []byte("instructions")}, }, []ModulePromptFile{ - {Name: "nested/task.md", Path: "assets/prompts/task.md"}, + {Name: "nested/instructions.md", Path: "assets/prompts/instructions.md"}, }) if err == nil || !strings.Contains(err.Error(), "must not contain path separators") { t.Fatalf("ModulePromptFS() error = %v, want nested file name error", err) @@ -94,9 +94,9 @@ func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) { func TestModulePromptFSRejectsNestedSharedFileName(t *testing.T) { _, err := ModulePromptFS("module.test", fstest.MapFS{ - "assets/prompts/task.md": {Data: []byte("task")}, + "assets/prompts/instructions.md": {Data: []byte("instructions")}, }, []ModulePromptFile{ - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }, SharedPromptFile{Name: "nested/system.md", FS: fstest.MapFS{}, Path: "shared/system.md"}) if err == nil || !strings.Contains(err.Error(), "shared prompt file name") || !strings.Contains(err.Error(), "must not contain path separators") { t.Fatalf("ModulePromptFS() error = %v, want nested shared file name error", err) diff --git a/internal/modules/dnd/chunk/scenes/prompt_assets_test.go b/internal/modules/dnd/chunk/scenes/prompt_assets_test.go index 678324c..58e0202 100644 --- a/internal/modules/dnd/chunk/scenes/prompt_assets_test.go +++ b/internal/modules/dnd/chunk/scenes/prompt_assets_test.go @@ -25,9 +25,6 @@ func TestPromptAssetsPrepareTranscriptWithInstructionOrdering(t *testing.T) { if prepared.OutputContract.SchemaPath != "dnd_scenes_llm.v1.json" { t.Fatalf("scene output schema path = %q, want private LLM schema", prepared.OutputContract.SchemaPath) } - if len(prepared.Messages) < 4 { - t.Fatalf("prepared messages = %#v, want system, references, instructions, and transcript", prepared.Messages) - } if prepared.Messages[0].Role != "system" || prepared.Messages[1].Role != "user" { t.Fatalf("initial prompt roles = %#v, want system followed by campaign references", prepared.Messages[:2]) } diff --git a/internal/modules/dnd/extract/combatturns/prompt_assets_test.go b/internal/modules/dnd/extract/combatturns/prompt_assets_test.go index e752bd0..737085b 100644 --- a/internal/modules/dnd/extract/combatturns/prompt_assets_test.go +++ b/internal/modules/dnd/extract/combatturns/prompt_assets_test.go @@ -62,9 +62,6 @@ func TestPromptAssetsPrepareRequiredInputs(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_combat_turns_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want combat prompt identity and schema", prepared) } - if len(prepared.Messages) < 6 { - t.Fatalf("prepared messages = %#v, want shared context, NPC grounding, and instructions", prepared.Messages) - } referenceIndex := -1 transcriptIndex := -1 npcIndex := -1 diff --git a/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go b/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go index a306155..f3f82e8 100644 --- a/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go @@ -41,7 +41,7 @@ func TestRegisterPromptAssetsAndPrepareEnemyEventPrompt(t *testing.T) { groundingIndices = append(groundingIndices, index) } } - instructionIndex := renderedMessageIndex(t, prepared.Messages, "Extract Dungeons & Dragons enemy events") + instructionIndex := len(prepared.Messages) - 1 for _, index := range groundingIndices { if instructionIndex <= index { t.Fatalf("instruction index = %d, want after grounding index %d", instructionIndex, index) diff --git a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go b/internal/modules/dnd/extract/itemevents/prompt_assets_test.go index dbcab5b..5e0858e 100644 --- a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/itemevents/prompt_assets_test.go @@ -3,7 +3,6 @@ package itemevents import ( "context" "encoding/json" - "reflect" "strings" "testing" "time" @@ -43,9 +42,6 @@ func TestPromptAssetsPrepareItemEventPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_item_events_llm.v1.json" { t.Fatalf("prepared prompt = %#v", prepared) } - if len(prepared.Messages) < 5 { - t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages) - } referenceIndex := -1 transcriptIndex := -1 for index, message := range prepared.Messages { @@ -91,16 +87,3 @@ func TestPromptAssetsDoNotLeakIntoMetadata(t *testing.T) { } } } - -func TestPromptManifestReusesOnlySharedAssets(t *testing.T) { - want := []string{ - "common-dnd-system.md", - "common-dnd-extraction-evidence.md", - "common-dnd-identity.md", - "common-dnd-references.md", - "common-dnd-transcript-chunk.md", - } - if !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) { - t.Fatalf("shared assets = %#v, want %#v", promptAssetManifest.SharedFiles, want) - } -} diff --git a/internal/modules/dnd/extract/locations/prompt_assets_test.go b/internal/modules/dnd/extract/locations/prompt_assets_test.go index 0549e61..64697c8 100644 --- a/internal/modules/dnd/extract/locations/prompt_assets_test.go +++ b/internal/modules/dnd/extract/locations/prompt_assets_test.go @@ -2,7 +2,6 @@ package locations import ( "context" - "slices" "strings" "testing" "time" @@ -12,11 +11,6 @@ import ( ) func TestRegisterPromptAssetsPreparesLocationPrompt(t *testing.T) { - for _, name := range []string{"common-dnd-system.md", "common-dnd-identity.md", "common-dnd-references.md", "common-dnd-transcript-chunk.md", "common-dnd-extraction-evidence.md"} { - if !slices.Contains(promptAssetManifest.SharedFiles, name) { - t.Fatalf("shared prompt assets = %#v, missing %q", promptAssetManifest.SharedFiles, name) - } - } registry := llm.NewAssetRegistry() if err := RegisterPromptAssets(registry); err != nil { t.Fatalf("RegisterPromptAssets() error = %v", err) @@ -39,9 +33,6 @@ func TestRegisterPromptAssetsPreparesLocationPrompt(t *testing.T) { if prepared.OutputContract.SchemaPath != "dnd_locations_llm.v1.json" { t.Fatalf("output contract = %#v", prepared.OutputContract) } - if len(prepared.Messages) < 5 { - t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages) - } referenceIndex := -1 transcriptIndex := -1 for index, message := range prepared.Messages { diff --git a/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go b/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go index f99f54f..b6c0bae 100644 --- a/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go +++ b/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go @@ -51,9 +51,6 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npc_interactions_llm.v1.json" { t.Fatalf("prepared prompt = %#v", prepared) } - if len(prepared.Messages) < 6 { - t.Fatalf("prepared messages = %#v, want shared context, NPC grounding, and instructions", prepared.Messages) - } referenceIndex := -1 transcriptIndex := -1 npcIndex := -1 diff --git a/internal/modules/dnd/extract/npcs/prompt_assets_test.go b/internal/modules/dnd/extract/npcs/prompt_assets_test.go index 8fcdb89..f63b722 100644 --- a/internal/modules/dnd/extract/npcs/prompt_assets_test.go +++ b/internal/modules/dnd/extract/npcs/prompt_assets_test.go @@ -42,9 +42,6 @@ func TestRegisterPromptAssetsAndPrepareNPCPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npcs_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want NPC prompt identity and wiring", prepared) } - if len(prepared.Messages) < 5 { - t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages) - } referenceIndex := -1 transcriptIndex := -1 for index, message := range prepared.Messages { diff --git a/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go b/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go index ed10bfb..c92608f 100644 --- a/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go +++ b/internal/modules/dnd/extract/scenedescriptions/prompt_assets_test.go @@ -42,9 +42,6 @@ func TestRegisterPromptAssetsPreparesSceneDescriptionPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_scene_descriptions_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want scene-description prompt identity and schema wiring", prepared) } - if len(prepared.Messages) < 5 { - t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages) - } referenceIndex := -1 transcriptIndex := -1 for index, message := range prepared.Messages { diff --git a/internal/modules/dnd/extract/spells/prompt_assets_test.go b/internal/modules/dnd/extract/spells/prompt_assets_test.go index a34ec7b..fa6a798 100644 --- a/internal/modules/dnd/extract/spells/prompt_assets_test.go +++ b/internal/modules/dnd/extract/spells/prompt_assets_test.go @@ -21,9 +21,6 @@ func TestPromptAssetsPrepareSpellPrompt(t *testing.T) { if prepared.OutputContract.SchemaPath != "dnd_spells_llm.v1.json" { t.Fatalf("schema path = %q, want LLM-only schema", prepared.OutputContract.SchemaPath) } - if len(prepared.Messages) < 7 { - t.Fatalf("prepared messages = %#v, want shared context, grounded inputs, and instructions", prepared.Messages) - } indices := map[string]int{ "spell-player": -1, "spell-transcript": -1, diff --git a/internal/modules/dnd/normalize/locations/prompt_assets_test.go b/internal/modules/dnd/normalize/locations/prompt_assets_test.go index a757ec8..72d947e 100644 --- a/internal/modules/dnd/normalize/locations/prompt_assets_test.go +++ b/internal/modules/dnd/normalize/locations/prompt_assets_test.go @@ -12,18 +12,6 @@ import ( ) func TestRegisterPromptAssetsPreparesLocationNormalizationPrompt(t *testing.T) { - hasInstructions := false - for _, file := range promptAssetManifest.ModuleFiles { - if file.Name == "task.md" { - t.Fatalf("module prompt files = %#v, want no retired task asset", promptAssetManifest.ModuleFiles) - } - if file.Name == "instructions.md" { - hasInstructions = true - } - } - if !hasInstructions { - t.Fatalf("module prompt files = %#v, want instructions asset", promptAssetManifest.ModuleFiles) - } registry := llm.NewAssetRegistry() if err := entityreconcile.RegisterSchemaAssets(registry); err != nil { t.Fatal(err) @@ -44,7 +32,7 @@ func TestRegisterPromptAssetsPreparesLocationNormalizationPrompt(t *testing.T) { if err != nil { t.Fatal(err) } - if prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" || len(prepared.Messages) < 5 { + if prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" { t.Fatalf("prepared prompt = %#v", prepared) } for _, index := range []int{2, 4} { diff --git a/internal/modules/dnd/normalize/npcs/prompt_assets_test.go b/internal/modules/dnd/normalize/npcs/prompt_assets_test.go index 825cceb..42b77ce 100644 --- a/internal/modules/dnd/normalize/npcs/prompt_assets_test.go +++ b/internal/modules/dnd/normalize/npcs/prompt_assets_test.go @@ -2,7 +2,6 @@ package npcs import ( "context" - "reflect" "strings" "testing" "time" @@ -13,21 +12,6 @@ import ( ) func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) { - if want := []string{"common-dnd-system.md", "common-dnd-entity-reconciliation.md", "common-dnd-transcript-windows.md"}; !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) { - t.Fatalf("shared prompt assets = %#v, want %#v", promptAssetManifest.SharedFiles, want) - } - hasInstructions := false - for _, file := range promptAssetManifest.ModuleFiles { - if file.Name == "task.md" { - t.Fatalf("module prompt files = %#v, want no retired task asset", promptAssetManifest.ModuleFiles) - } - if file.Name == "instructions.md" { - hasInstructions = true - } - } - if !hasInstructions { - t.Fatalf("module prompt files = %#v, want instructions asset", promptAssetManifest.ModuleFiles) - } if promptHash, err := promptAssetMetadata(); err != nil || promptHash == "" { t.Fatalf("promptAssetMetadata() = %q, %v; want prompt fingerprint", promptHash, err) } @@ -62,9 +46,6 @@ func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want normalization prompt identity and schema", prepared) } - if len(prepared.Messages) < 5 { - t.Fatalf("prepared messages = %#v, want system, instructions, reconciliation policy, candidates, and transcript windows", prepared.Messages) - } if prepared.Messages[0].Role != "system" { t.Fatalf("initial message role = %q, want system", prepared.Messages[0].Role) } diff --git a/internal/modules/dnd/shared/assets_test.go b/internal/modules/dnd/shared/assets_test.go index 9c8673e..9dececf 100644 --- a/internal/modules/dnd/shared/assets_test.go +++ b/internal/modules/dnd/shared/assets_test.go @@ -15,8 +15,8 @@ func TestPromptAssetManifestPromptFS(t *testing.T) { manifest := PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ - {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }, SharedFiles: []string{ "common-dnd-system.md", @@ -27,16 +27,16 @@ func TestPromptAssetManifestPromptFS(t *testing.T) { } fsys, err := manifest.PromptFS(fstest.MapFS{ - "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, - "assets/prompts/task.md": {Data: []byte("task")}, + "assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")}, + "assets/prompts/instructions.md": {Data: []byte("instructions")}, }) if err != nil { t.Fatalf("PromptFS() error = %v, want nil", err) } wantModule := map[string]string{ - "assets/prompts/dnd.test/dnd.test.yaml": "id: dnd.test", - "assets/prompts/dnd.test/task.md": "task", + "assets/prompts/dnd.test/prompt.yaml": "id: dnd.test", + "assets/prompts/dnd.test/instructions.md": "instructions", } for path, wantContent := range wantModule { content, err := fs.ReadFile(fsys, path) @@ -88,11 +88,11 @@ func TestPromptAssetManifestRejectsInvalidSharedNames(t *testing.T) { _, err := (PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ - {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, + {Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"}, }, SharedFiles: test.sharedFiles, }).PromptFS(fstest.MapFS{ - "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, + "assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")}, }) if err == nil || !strings.Contains(err.Error(), test.wantError) { t.Fatalf("PromptFS() error = %v, want %q", err, test.wantError) @@ -105,10 +105,10 @@ func TestPromptAssetManifestRejectsMissingModuleFile(t *testing.T) { _, err := (PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }, }).PromptFS(fstest.MapFS{}) - if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") { + if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/instructions.md") { t.Fatalf("PromptFS() error = %v, want missing module asset context", err) } } @@ -129,11 +129,11 @@ func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) { _, err := (PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ - {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, + {Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"}, }, SharedFiles: []string{name}, }).PromptFS(fstest.MapFS{ - "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, + "assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")}, }) if err == nil || !strings.Contains(err.Error(), "read shared prompt asset "+path) { t.Fatalf("PromptFS() error = %v, want missing shared asset context", err) @@ -142,14 +142,14 @@ func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) { func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) { moduleFS := fstest.MapFS{ - "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, - "assets/prompts/task.md": {Data: []byte("task")}, + "assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")}, + "assets/prompts/instructions.md": {Data: []byte("instructions")}, } manifest := PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ - {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, - {Name: "task.md", Path: "assets/prompts/task.md"}, + {Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"}, + {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, }, SharedFiles: []string{ "common-dnd-transcript-chunk.md", @@ -166,8 +166,8 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) { t.Fatalf("sharedAssetFS() error = %v, want nil", err) } want, err := llm.HashAssets([]llm.AssetHashPart{ - {FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"}, - {FS: moduleFS, Path: "assets/prompts/task.md"}, + {FS: moduleFS, Path: "assets/prompts/prompt.yaml"}, + {FS: moduleFS, Path: "assets/prompts/instructions.md"}, {FS: sharedFS, Path: "prompts/common-dnd-transcript-chunk.md"}, {FS: sharedFS, Path: "prompts/common-dnd-system.md"}, }) @@ -178,8 +178,8 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) { t.Fatalf("Hash() = %q, want independently assembled manifest hash %q", got, want) } withUnused, err := llm.HashAssets([]llm.AssetHashPart{ - {FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"}, - {FS: moduleFS, Path: "assets/prompts/task.md"}, + {FS: moduleFS, Path: "assets/prompts/prompt.yaml"}, + {FS: moduleFS, Path: "assets/prompts/instructions.md"}, {FS: sharedFS, Path: "prompts/common-dnd-transcript-chunk.md"}, {FS: sharedFS, Path: "prompts/common-dnd-system.md"}, {FS: sharedFS, Path: "prompts/common-dnd-npcs.md"}, diff --git a/internal/modules/integration/dnd_spells_runner_test.go b/internal/modules/integration/dnd_spells_runner_test.go index df36d6d..6f75b5c 100644 --- a/internal/modules/integration/dnd_spells_runner_test.go +++ b/internal/modules/integration/dnd_spells_runner_test.go @@ -61,8 +61,8 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) { t.Fatalf("len(NormalizeOutputs) = %d, want 1", len(output.NormalizeOutputs)) } serializedOutput := output.NormalizeOutputs[0] - if serializedOutput.LaneID != "spells" || serializedOutput.Artifact.Schema.ID != spells.ResponseSchemaID || serializedOutput.Artifact.Schema.Version != spells.SchemaVersion { - t.Fatalf("serialized output envelope = %#v, want dnd spells schema on spells lane", serializedOutput) + if serializedOutput.LaneID != "spells" || serializedOutput.Artifact.Schema.ID != spellcodec.SchemaID || serializedOutput.Artifact.Schema.Version != spellcodec.SchemaVersion { + t.Fatalf("serialized output envelope = %#v, want durable spell artifact schema on spells lane", serializedOutput) } response := decodeRunnerSpellResponse(t, serializedOutput.Artifact.Content) if len(response.SpellCasts) != 2 {