# D&D Shared Helper Refactor Implementation Plan ## Summary Implement the target state defined in [D&D Shared Module Helper Roadmap](dnd.md). This is an internal refactor: do not change module keys, prompt IDs, prompt versions, Scriptorium-visible message order, response schema IDs or names, CLI/config semantics, manifest shape, diagnostics redaction policy, or D&D scene/spell interpretation behavior. The intended package split is: - `internal/modules/sharedassets`: generic prompt filesystem composition and non-domain-specific asset plumbing only; - `internal/modules/sharedassets/dnd`: reusable D&D prompt assets, prompt input helpers, reference slot helpers, reference rendering, and D&D shared prompt hash parts; - concrete D&D modules: stage contracts, module registration, module-local prompt definitions, response schemas, validators, and response interpretation. ## Stage 1: Add Generic Defensive-Copy Helper - Add `CloneReferenceSlots` to `internal/framework/contracts`. - The helper must: - return `nil` for an empty input slice; - allocate a new slot slice; - deep-copy each `AcceptedMediaTypes` slice; - preserve all other `ReferenceSlot` fields exactly. - Add focused tests in `internal/framework/contracts` proving nil/empty behavior, deep-copy behavior, and full field preservation. - Do not import any concrete module package from `contracts`. ## Stage 2: Make Shared Prompt FS Composition Domain-Neutral - Refactor `internal/modules/sharedassets.ModulePromptFS` so the parent package no longer knows about D&D prompt filenames or embedded D&D assets. - Add a generic shared prompt file descriptor: ```go type SharedPromptFile struct { Name string FS fs.FS Path string } ``` - Change `ModulePromptFS` to this shape: ```go func ModulePromptFS( moduleDir string, moduleFS fs.FS, files []ModulePromptFile, sharedFiles ...SharedPromptFile, ) (fs.FS, error) ``` - Keep existing `ModulePromptFile` semantics: - `Name` is mounted directly under `assets/prompts//`; - `Path` is read from the module embedded filesystem. - Mount each `SharedPromptFile` under `assets/prompts//sharedassets/`. - Validate all names and paths with contextual errors: - `moduleDir` must be a valid non-root fs path; - module prompt `Name` must be a single filename with no path separators; - shared prompt `Name` must be a single filename with no path separators; - `moduleFS` and each shared file `FS` must be non-nil; - missing module or shared files must return errors naming the source path. - Preserve the in-memory FS behavior currently covered by tests, including readable intermediate directories and root `.` support. - Update `internal/modules/sharedassets` tests so they use test-owned shared prompt files from `fstest.MapFS`, not production D&D prompt assets. - Remove D&D-specific helpers, constants, and embedded asset dependencies from the parent `sharedassets` package. ## Stage 3: Create `sharedassets/dnd` - Create `internal/modules/sharedassets/dnd` with package name `dnd`. - Move the current D&D shared prompt files into: ```text internal/modules/sharedassets/dnd/assets/prompts/common-dnd-system.md internal/modules/sharedassets/dnd/assets/prompts/common-dnd-transcript.md internal/modules/sharedassets/dnd/assets/prompts/common-dnd-references.md ``` - Add `assets.go` in the D&D package with `go:embed assets/prompts/*.md`. - Expose D&D shared asset helpers: ```go func SharedPromptFiles() []sharedassets.SharedPromptFile func CommonHashParts() []llm.AssetHashPart func ReferenceHashParts() []llm.AssetHashPart func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []sharedassets.ModulePromptFile) (fs.FS, error) ``` - `SharedPromptFiles` must return a new slice each call. - `CommonHashParts` must include `common-dnd-system.md` and `common-dnd-transcript.md`. - `ReferenceHashParts` must include `common-dnd-references.md`. - `ModulePromptFS` must call `sharedassets.ModulePromptFS` with `SharedPromptFiles()...`. - Do not expose a D&D package registration function unless a concrete caller needs top-level D&D shared prompt files outside module-local prompt composition. The D&D prompt definitions should continue to load shared files through `./sharedassets/...` inside each module prompt directory. ## Stage 4: Add D&D Reference And Prompt Input Helpers - In `internal/modules/sharedassets/dnd`, add D&D reference helpers: ```go type ReferenceSlotDescriptions struct { Glossary string Party string Players string Roster string } func ReferenceMediaTypes() []string func ReferenceSlots(descriptions ReferenceSlotDescriptions) []contracts.ReferenceSlot ``` - `ReferenceMediaTypes` must return a defensive copy of: - `application/json` - `application/x-yaml` - `application/yaml` - `text/markdown` - `text/plain` - `ReferenceSlots` must return slots in the same order currently exposed by both D&D modules: `glossary`, `party`, `players`, `roster`. - Slot names and alias semantics must remain unchanged. - Slot descriptions should be supplied by each concrete module through `ReferenceSlotDescriptions` so current module metadata text can be preserved. - `ReferenceSlots` must use `contracts.CloneReferenceSlots` or equivalent defensive-copy behavior before returning. - Add prompt input helpers: ```go func PromptInputs(sourceInput contracts.LLMInputMaterial, references contracts.ReferenceSet) contracts.LLMInputSet func TranscriptPromptMaterial(material contracts.LLMInputMaterial) contracts.LLMInputMaterial func ReferencePromptMaterial(name string, slot contracts.ResolvedReferenceSlot) contracts.LLMInputMaterial func ReferencePromptInput(slot contracts.ResolvedReferenceSlot) []byte ``` - `PromptInputs` must return inputs named `transcript`, `players`, `party`, and `glossary`. - If `party` has no items and `roster` has items, `PromptInputs` must use the `roster` slot content for the `party` input. - `PromptInputs` must not include a `roster` prompt input. - `TranscriptPromptMaterial` must clone the source input and set `Name` to `transcript`. - `ReferencePromptMaterial` must use media type `text/plain`; for a single reference item it must propagate that item's digest and origin URI. - `ReferencePromptInput` behavior must match current D&D module behavior: - empty slot renders as a single space; - one item renders as raw item content; - multiple items are copied, sorted deterministically by origin URI, digest, then content, and rendered with the same heading/metadata format currently used by `dnd/scenes` and `dnd/spells`. - Add focused tests under `internal/modules/sharedassets/dnd` for all helper behavior. These tests may assert rendering of helper-owned fixture inputs, but must not assert exact production embedded prompt prose. ## Stage 5: Update D&D Modules To Use Shared Helpers - Update `internal/modules/chunk/dnd/scenes`: - import `internal/modules/sharedassets/dnd`; - replace local accepted media type and slot cloning logic with `dnd.ReferenceSlots`; - preserve existing scene-specific slot descriptions; - replace local prompt input/reference rendering helpers with `dnd.PromptInputs`; - replace `sharedassets.ModulePromptFS` with `dnd.ModulePromptFS`; - replace `sharedassets.CommonHashParts` and `ReferenceHashParts` with the D&D package equivalents. - Update `internal/modules/extract/dnd/spells` the same way, preserving current extractor-specific slot descriptions. - Delete local duplicated helper functions that become unused: - `acceptedReferenceMediaTypes`; - `cloneReferenceSlots`; - `transcriptPromptInput`; - `referencePromptMaterial`; - `referencePromptInput`. - Keep module-local schema loading, prompt IDs, prompt versions, validators, request validation, response conversion, and manifest metadata ownership in each concrete module. ## Stage 6: Update Asset Registration And Tests - Update production prompt asset registration in `internal/cli` as needed: - remove any call that registers D&D shared prompt files from the generic `sharedassets` package; - ensure `dnd/scenes` and `dnd/spells` prompt registration still makes their module-local `./sharedassets/common-dnd-*.md` files available. - Update production prompt asset tests so they assert module-local shared asset paths such as: - `dnd.scenes/sharedassets/common-dnd-system.md` - `dnd.spells/sharedassets/common-dnd-system.md` - Remove expectations for top-level `common-dnd-*.md` prompt files unless a genuine top-level registration remains necessary. - Move reference rendering tests from concrete D&D modules into `internal/modules/sharedassets/dnd`. - Keep concrete D&D module tests focused on: - module specs and registry behavior; - LLM request prompt ID/version/profile/session fields; - transcript and reference input propagation; - Scriptorium prompt preparation; - diagnostics redaction; - manifest metadata; - scene/spell response interpretation. - Follow the prompt asset testing policy in `docs/policy/development.md`: do not assert exact production embedded prompt prose. ## Stage 7: Update Documentation - Update canonical internal docs for implemented behavior: - `docs/internal/modules.md`: mention that common D&D prompt/reference helper behavior lives under `internal/modules/sharedassets/dnd`; - `docs/internal/overview.md`: mention the package only if the overview lists shared module-support packages. - Do not add user-facing documentation unless visible behavior changes. - Update `docs/roadmap/dnd.md` after implementation so it no longer presents this refactor as future work. - Replace this file with a concise completed-note document once the work is implemented. ## Validation Run focused tests after each meaningful stage: ```sh go test ./internal/framework/contracts go test ./internal/modules/sharedassets go test ./internal/modules/sharedassets/dnd go test ./internal/modules/chunk/dnd/scenes go test ./internal/modules/extract/dnd/spells go test ./internal/cli ``` Run full validation before completion: ```sh go test ./... go vet ./... go build ./cmd/notarius ``` Run inspection searches: ```sh rg -n "common-dnd|CommonHashParts|ReferenceHashParts|referencePromptInput|cloneReferenceSlots|acceptedReferenceMediaTypes" internal/modules internal/framework rg -n "Divide the provided transcript|Extract Dungeons & Dragons spell-cast artifacts|A transcript of a Dungeons & Dragons gameplay session|Roster reference|Glossary reference" internal/**/*_test.go ``` Expected inspection outcomes: - D&D shared prompt files and hash helpers are owned by `internal/modules/sharedassets/dnd`; - parent `sharedassets` has no hard-coded `common-dnd-*` knowledge; - concrete D&D modules no longer carry duplicated reference rendering logic; - tests do not assert exact production embedded prompt prose. ## Non-Goals - Do not change D&D prompt wording except for file moves required by this refactor. - Do not change prompt IDs, prompt versions, profile defaults, Scriptorium message order, schema files, schema metadata, module keys, capabilities, manifest shapes, or diagnostics policy. - Do not move scene boundary logic, spell artifact logic, validators, or schema loading into the shared D&D package. - Do not make framework, core, or CLI packages depend on `internal/modules/sharedassets/dnd`.