# D&D Scene Descriptions Implementation Plan Status: Ready for implementation ## Objective And Authority Implement the accepted target state in [D&D Scene Descriptions](dnd-scene-descriptions.md): one strict, typed `dnd/scene-description-list` artifact record for every successfully extracted accepted scene chunk. The model owns only `kind`, `title`, and `summary`; Notarius owns the scene ID and exact current-source range. This plan is the execution document. The feature roadmap owns intent, durable policy, classification semantics, and non-goals. If wording here appears to conflict with that roadmap, preserve the roadmap's contract and update this plan before proceeding. Follow: - [Architecture Policy](../policy/architecture.md), especially typed domain codecs, source-reference ownership, central registration, and the fixed input/chunk/extract/merge/normalize/output flow; - [Testing Policy](../policy/testing.md), especially behavioral tests at the stable owner, offline fake-LLM coverage, and the prohibition on prompt-length or shared-prefix change-detector tests; and - [Documentation Policy](../policy/documentation.md), especially one canonical owner for each contract and updating current-state docs only when behavior is implemented. The repository is pre-release. Use the `v1` identities fixed below; do not add compatibility aliases, dual schemas, migration adapters, or legacy decoding. ## Fixed Implementation Decisions ### Public identities Use these values exactly: | Concern | Value | | --- | --- | | Extractor key | `dnd/scene-descriptions` | | Normalizer key | `dnd/scene-descriptions` | | Artifact kind | `dnd/scene-description-list` | | Durable schema ID | `notarius.dnd.scene_descriptions` | | Durable schema name | `notarius_dnd_scene_descriptions_v1` | | Durable schema version | `v1` | | Durable media type | `application/json` | | Prompt ID | `dnd.scene_descriptions` | | Private response-schema key | `dnd_scene_descriptions_llm` | | Private response-schema ID | `notarius.dnd.scene_descriptions.llm` | | Private response-schema name | `notarius_dnd_scene_descriptions_llm_v1` | | Private response-schema version | `v1` | | Extractor mapping policy | `dnd.scene_descriptions.mapping.v1` | | Normalizer policy | `dnd.scene_descriptions.normalizer.v1` | ### Typed and JSON shapes Add the following canonical D&D types in `internal/modules/dnd/types.go`: ```go type SceneKind string const ( SceneKindCombat SceneKind = "combat" SceneKindNarrative SceneKind = "narrative" SceneKindRecap SceneKind = "recap" SceneKindMeta SceneKind = "meta" ) type SceneDescriptionList struct { Scenes []SceneDescription `json:"scenes"` } type SceneDescription struct { ID string `json:"id"` SourceRef source.SourceRef `json:"source_ref"` Kind SceneKind `json:"kind"` Title string `json:"title"` Summary string `json:"summary"` } ``` Also add `SceneDescriptionListKind contracts.ArtifactKind = "dnd/scene-description-list"`. The durable JSON object contains exactly `scenes`; every scene object contains exactly `id`, `source_ref`, `kind`, `title`, and `summary`. The private model response is one object containing exactly `kind`, `title`, and `summary`. It is not wrapped in `scenes`, cannot be an array, and cannot represent “no result.” Both schemas reject unknown fields. Both schemas enumerate all four kind values. Durable strings and private `title` and `summary` use `minLength: 1`; do not impose arbitrary maximum lengths in schema or deterministic validation. ### Application-owned mapping For each extraction request: - obtain transcript material with `shared.ChunkPromptMaterial`; - prepare optional `players`, `party`, and `glossary` inputs, including the established explicit empty placeholders, with `shared.PromptInputs`; - call the private structured prompt once; - copy `req.Chunk.ID` to the durable `id`; - copy `req.Chunk.Ref` to the durable `source_ref`; - copy `kind` without trimming, case-folding, aliasing, or repair; and - trim surrounding whitespace from `title` and `summary`, then return a single-element durable `scenes` list. An invalid or absent model object is an extraction failure handled by existing structured-output and retry policy. The extractor must never synthesize a fallback classification or empty scene list. Include the prompt digest, response-schema digest, and mapping policy in extractor manifest metadata and checkpoint fingerprints, following the combat turn extractor pattern. The optional campaign references affect the prompt input identity through existing framework behavior; do not add a generated artifact reference or an NPC registry slot. ### Prompt assets and caching Create package-owned prompt assets under `internal/modules/dnd/extract/scenedescriptions/assets/`. Use this exact message order: 1. shared `common-dnd-system.md` as the system message; 2. shared `common-dnd-identity.md` as a user message and cache boundary; 3. shared `common-dnd-references.md` as a user message and cache boundary; 4. package-owned `task.md`; 5. package-owned `instructions.md` as the final stable cache boundary; and 6. shared `common-dnd-transcript.md` as the final user message, without cache control. Declare `transcript`, `players`, `party`, and `glossary` inputs with the same media types and required/optional status used by `dnd/npcs`. Reuse the shared assets through the existing Scriptorium module/shared filesystem pattern; do not copy their text into this package. Do **not** render `common-dnd-extraction-evidence.md`. Its citation instructions require the model to emit ranges, which conflicts with the application-owned whole-chunk evidence contract. Put the scene-kind vocabulary, residual mixed-scene precedence, title constraints, summary constraints, and the rule that the response describes exactly the supplied accepted chunk in the package-owned task/instruction assets. Do not ask for IDs, ranges, source IDs, participants, confidence, or additional fields. ### Merge, normalization, and conflicts Register the existing append-order merger specialized for `SceneDescriptionList`. Its append function must preserve nil-versus-present slice semantics and return independently owned values, matching the other D&D specializations. The normalizer is deterministic and has no reference slots. It must: 1. reject a nil source, malformed current-source reference, blank ID, invalid kind, or blank title/summary; 2. trim only surrounding whitespace from title and summary; 3. sort by the source document's start-unit position, then scene ID as the deterministic tie-breaker; 4. remove only records identical in all five durable fields; 5. reject any repeated scene ID whose remaining fields are not identical; and 6. reject records with the same exact source range and different `kind`, `title`, or `summary`. Two records with different IDs but the same range and identical model-owned content are not exact duplicates and are not a conflict under this contract; retain both in deterministic ID order. Do not merge adjacent ranges, reconcile prose, change kinds heuristically, infer missing scenes, or use chunk annotations. ### Validation ownership and default chains Add these deterministic validators: | Key | Owner | | --- | --- | | `extract/dnd/scene-descriptions/shape` | Required list and record fields, exact scene-kind vocabulary, trimmed non-empty strings, and exactly one scene during extraction. | | `extract/dnd/scene-descriptions/source_refs` | Current-source validity at all stages; at extraction, requires a current chunk and exact equality of both scene ID and source range to that chunk. | | `extract/dnd/scene-descriptions/source_relatedness` | Advisory lexical grounding for title and summary against the scene's current transcript range only. | | `normalize/dnd/scene-descriptions/invariants` | Normalized order, exact-duplicate removal, unique/conflict rules, and normalized strings. | The shape validator requires a present, non-empty `scenes` list at all stages and exactly one element when `req.Stage == string(pipeline.StageExtract)`. Stage-independent record validation rejects blank IDs, unsupported kinds, blank or untrimmed title/summary, and structurally empty source references. The source-reference validator first defers malformed shape to the shape validator. At extraction it rejects a nil `req.Chunk`, an ID unequal to `req.Chunk.ID`, or a `source_ref` unequal to `req.Chunk.Ref`; containment is not sufficient. At normalization, where no current chunk is available, it validates the reference against `req.Source` but does not attempt to reconstruct chunk identity. The source-relatedness validator is warning-only and runs only after shape and source-reference validity. For each scene, tokenize the cited current-source text, title, and summary with `shared.NormalizedTokens`. Ignore tokens shorter than three Unicode code points and these case-normalized function words: `a`, `an`, `and`, `are`, `as`, `at`, `be`, `but`, `by`, `for`, `from`, `had`, `has`, `have`, `he`, `her`, `him`, `his`, `in`, `into`, `is`, `it`, `its`, `of`, `on`, `or`, `she`, `that`, `the`, `their`, `them`, `they`, `this`, `to`, `was`, `were`, `with`. Emit one warning scoped to `scenes[i].title` if no remaining title token occurs in the cited text, and independently one warning scoped to `scenes[i].summary` if no remaining summary token occurs there. If a field has no remaining significant token, emit its warning. Never use campaign references for this check. Bound and safely quote diagnostics through the existing D&D diagnostic helpers; do not include transcript or reference content in messages. Register these exact default chains: ```text extract dnd/scene-descriptions: generic/valid_json extract/dnd/scene-descriptions/shape extract/dnd/scene-descriptions/source_refs generic/valid_json_schema extract/dnd/scene-descriptions/source_relatedness normalize dnd/scene-descriptions: generic/valid_json extract/dnd/scene-descriptions/shape normalize/dnd/scene-descriptions/invariants extract/dnd/scene-descriptions/source_refs generic/valid_json_schema extract/dnd/scene-descriptions/source_relatedness ``` Do not add merge-stage validators, LLM-backed validators, options, generated references, stage dependencies, chunk-map dependencies, or framework changes. ## Stage 1: Add The Durable Typed Contract And Codec ### Work 1. Extend `internal/modules/dnd/types.go` with the fixed artifact kind, scene kind, constants, list, and record types. 2. Add `internal/modules/dnd/codec/scenedescriptions/` following the strict candidate/approved encode/decode pattern in the existing D&D codecs. 3. Embed `assets/schemas/dnd_scene_descriptions.v1.json` with the exact durable shape and identities above. 4. Return `scene_count` from codec metadata. 5. Add one representative valid JSON fixture under `testdata/`. ### Tests Add focused codec tests covering schema identity and validity, valid round-trip, strict unknown/trailing JSON rejection, nil `scenes`, invalid kinds, blank required strings, malformed source fields, candidate decoding of semantically invalid but structurally decodable values, metadata, and independent ownership of encoded/schema bytes where the existing codec contract requires it. Do not duplicate shape-validator and durable JSON Schema coverage exhaustively in the codec suite. ### Completion gate Run: ```sh gofmt -w internal/modules/dnd/types.go internal/modules/dnd/codec/scenedescriptions/*.go go test ./internal/modules/dnd/codec/scenedescriptions ``` The repository must compile with no production registration added yet. ## Stage 2: Add The Private Model Contract, Prompt, And Extractor ### Work 1. Add `internal/modules/dnd/extract/scenedescriptions/` using the established D&D extractor package layout: assets, model, schema loader, prompt registration/fingerprints, constructor/options handling, extractor, and deterministic mapper. 2. Define a private Go response with exactly `kind`, `title`, and `summary`. 3. Embed `assets/schemas/dnd_scene_descriptions_llm.v1.json` with the fixed strict private contract. 4. Add `assets/prompts/dnd.scene_descriptions.yaml`, `task.md`, and `instructions.md` with the fixed message order and policy above. 5. Use `shared.ReferenceSlots` with package-owned descriptions for optional `players`, `party`, and `glossary` slots. The extractor and its module spec expose no other slots. 6. Map the private response to exactly one durable record using the current chunk ID and exact `Chunk.Ref`. ### Tests At the package owners, cover: - private schema identity, strict shape, closed enum, and rejection of application-owned or extra fields; - prompt and response-schema asset registration and fingerprints; - prepared message roles/order, compatible shared-asset use, explicit empty campaign placeholders, one transcript rendering, and transcript-last placement; - module spec, capabilities, optional reference slots, option rejection, and registration; - request validation and provider failure; - exact application-owned ID/range mapping; - title/summary trimming and non-repair of kind; and - prompt inputs using only current chunk material plus optional references. Do not assert prompt byte length, shared-prefix length, exact generated prose, or provider output quality. ### Completion gate Run: ```sh gofmt -w internal/modules/dnd/extract/scenedescriptions/*.go go test ./internal/modules/dnd/extract/scenedescriptions ``` Do not register the extractor in the production D&D family in this stage. ## Stage 3: Add Deterministic Normalization And Validation ### Work 1. Add `internal/modules/dnd/normalize/scenedescriptions/` with the fixed normalization and conflict behavior. Expose no options or reference slots. 2. Add the four validator packages under `internal/modules/dnd/validate/scenedescriptions/` using the exact keys and responsibilities above: `shape`, `source_refs`, `source_relatedness`, and `invariants`. 3. Give each normalizer/validator a checkpoint policy fingerprint that changes when its owned deterministic behavior changes. 4. Use shared source and diagnostic helpers where their semantics match. Keep the scene-specific lexical stopword policy local to the relatedness validator. ### Tests Cover normalization of whitespace and source ordering; ID tie-breaking; exact deduplication; both conflict classes; preservation of same-range, same-content, different-ID records; invalid source/shape/kind handling; input ownership; cancellation/nil requests; and module registration metadata. Cover validators at their stable owners, including: - extraction requires exactly one scene; - extraction ID and range must exactly equal the current chunk; - normalization validates current-source membership without a chunk; - shape and source-reference deferral between validators; - supported and unsupported kinds; - invariant order, trimming, duplicate, and conflict checks; - separate bounded title and summary relatedness warnings; - zero-significant-token warnings; - transcript-only relatedness even when campaign references contain matching text; and - safe bounded diagnostics. Use table-driven cases where several inputs exercise one behavior. Do not replicate complete codec or JSON Schema test matrices in every validator. ### Completion gate Run: ```sh gofmt -w internal/modules/dnd/normalize/scenedescriptions/*.go internal/modules/dnd/validate/scenedescriptions/*/*.go go test ./internal/modules/dnd/normalize/scenedescriptions ./internal/modules/dnd/validate/scenedescriptions/... ``` Production registration remains deferred until all leaf packages pass. ## Stage 4: Compose The Production Lane ### Work 1. Update `internal/modules/dnd/register/modules.go` to register the codec, extractor, append-order specialization, normalizer, typed no-op normalizer, and prompt/schema assets. 2. Add the list append/clone helper in `internal/modules/dnd/register/merge.go`. 3. Update `internal/modules/dnd/register/validators.go` to register all four validators plus typed always-accept and always-reject specializations. 4. Update `internal/modules/dnd/register/chains.go` with the exact extract and normalize chains above. 5. Extend `internal/modules/dnd/register/register_test.go` to assert the new keys, artifact-kind specializations, assets, module specs, and exact production chains. 6. Add one offline assembled production workflow test under `internal/cli/` using the real family registration/configuration/runner/output path and a fake structured LLM. It must exercise at least two accepted scene chunks and prove: - one model description is mapped to each chunk's ID and exact range; - append merge and normalization produce source order; - model-owned fields survive with only specified trimming; - durable schema identity and output payload are correct; and - no NPC or generated-artifact reference is required. Keep this as one representative assembled test. Do not add equivalent end-to-end cases in multiple packages. ### Completion gate Run: ```sh gofmt -w internal/modules/dnd/register/*.go internal/cli/dnd_scene_descriptions_contract_test.go go test ./internal/modules/dnd/register ./internal/cli ``` At this gate the lane must be selectable through production registration, with no direct production import outside the D&D family registrar and CLI composition root. ## Stage 5: Publish Current Documentation And Maintained Example ### Work Only after Stage 4 is green: 1. Add `docs/integrations/dnd-scene-description-artifacts.md` as the canonical durable contract. Document identities, exact JSON shape, scene kinds, application-owned ID/range mapping, merge/normalization rules, conflict behavior, advisory relatedness warnings, optional campaign references, and a copyable lane example. 2. Update `docs/config.md` with the maintained example link; extractor, normalizer, and validator keys; optional `players`, `party`, and `glossary` slots; and the exact default chains. 3. Update `docs/internal/modules.md` and `docs/internal/overview.md` for the new codec/extractor/normalizer/validators and family registration. 4. Update `docs/internal/llm.md` with the prompt manifest and the deliberate omission of the citation-oriented shared extraction-evidence message. Make clear that compatible shared messages remain canonical shared assets and the transcript remains last. 5. Update `docs/internal/pipeline.md` only where its current artifact-lane or source-attachment inventory requires the new lane; do not describe a new stage or dependency. 6. Update `docs/integrations/json-output.md` to link the new durable artifact contract alongside the other typed D&D schemas. 7. Add `examples/dnd-scene-descriptions.config.yml`. It should use `chunk: dnd/scenes`, one scene-description artifact lane, and `normalize: dnd/scene-descriptions`. Keep it minimal and do not present chunk annotations as the durable description artifact. 8. Mark `docs/roadmap/dnd-scene-descriptions.md` as `Status: Implemented`, add a link to the canonical integration document near the top, and remove the implemented “Extract D&D Scene Descriptions” entry from `docs/roadmap/future.md`. Leave chunker minimization and combat gating in future work. ### Documentation checks Verify every new selectable key, schema identity, enum, validator order, and example against production code. Check all relative Markdown links and run the maintained example configuration through config loading or the closest existing example/config test. Do not document private package mechanics in the integration contract. ## Stage 6: Final Quality Gate 1. Review the diff for accidental changes to `dnd/scenes`, combat gating, framework stages, generated-reference behavior, or unrelated D&D contracts. 2. Confirm all new JSON objects reject unknown fields and the model is never asked to return application-owned identity/evidence. 3. Confirm diagnostics contain no transcript or campaign-reference content. 4. Confirm the prompt uses shared assets by reference and that the transcript is the final message; do not introduce a prompt-length or exact-prefix detector. 5. Run: ```sh gofmt -l . go test ./... go vet ./... git diff --check ``` `gofmt -l .` must produce no output. Do not mark the feature roadmap implemented until all commands pass and the current documentation describes the shipped behavior. ## Open Questions None. The accepted feature roadmap and the fixed decisions in this plan are sufficient to implement all stages without further product or architecture choices.