20 KiB
D&D Scene Descriptions Implementation Plan
Status: Ready for implementation
Objective And Authority
Implement the accepted target state in
D&D Scene Descriptions: 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, especially typed domain codecs, source-reference ownership, central registration, and the fixed input/chunk/extract/merge/normalize/output flow;
- Testing Policy, 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, 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:
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, andglossaryinputs, including the established explicit empty placeholders, withshared.PromptInputs; - call the private structured prompt once;
- copy
req.Chunk.IDto the durableid; - copy
req.Chunk.Refto the durablesource_ref; - copy
kindwithout trimming, case-folding, aliasing, or repair; and - trim surrounding whitespace from
titleandsummary, then return a single-element durablesceneslist.
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:
- shared
common-dnd-system.mdas the system message; - shared
common-dnd-identity.mdas a user message and cache boundary; - shared
common-dnd-references.mdas a user message and cache boundary; - package-owned
task.md; - package-owned
instructions.mdas the final stable cache boundary; and - shared
common-dnd-transcript.mdas 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:
- reject a nil source, malformed current-source reference, blank ID, invalid kind, or blank title/summary;
- trim only surrounding whitespace from title and summary;
- sort by the source document's start-unit position, then scene ID as the deterministic tie-breaker;
- remove only records identical in all five durable fields;
- reject any repeated scene ID whose remaining fields are not identical; and
- reject records with the same exact source range and different
kind,title, orsummary.
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:
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
- Extend
internal/modules/dnd/types.gowith the fixed artifact kind, scene kind, constants, list, and record types. - Add
internal/modules/dnd/codec/scenedescriptions/following the strict candidate/approved encode/decode pattern in the existing D&D codecs. - Embed
assets/schemas/dnd_scene_descriptions.v1.jsonwith the exact durable shape and identities above. - Return
scene_countfrom codec metadata. - 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:
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
- 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. - Define a private Go response with exactly
kind,title, andsummary. - Embed
assets/schemas/dnd_scene_descriptions_llm.v1.jsonwith the fixed strict private contract. - Add
assets/prompts/dnd.scene_descriptions.yaml,task.md, andinstructions.mdwith the fixed message order and policy above. - Use
shared.ReferenceSlotswith package-owned descriptions for optionalplayers,party, andglossaryslots. The extractor and its module spec expose no other slots. - 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:
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
- Add
internal/modules/dnd/normalize/scenedescriptions/with the fixed normalization and conflict behavior. Expose no options or reference slots. - Add the four validator packages under
internal/modules/dnd/validate/scenedescriptions/using the exact keys and responsibilities above:shape,source_refs,source_relatedness, andinvariants. - Give each normalizer/validator a checkpoint policy fingerprint that changes when its owned deterministic behavior changes.
- 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:
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
- Update
internal/modules/dnd/register/modules.goto register the codec, extractor, append-order specialization, normalizer, typed no-op normalizer, and prompt/schema assets. - Add the list append/clone helper in
internal/modules/dnd/register/merge.go. - Update
internal/modules/dnd/register/validators.goto register all four validators plus typed always-accept and always-reject specializations. - Update
internal/modules/dnd/register/chains.gowith the exact extract and normalize chains above. - Extend
internal/modules/dnd/register/register_test.goto assert the new keys, artifact-kind specializations, assets, module specs, and exact production chains. - 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:
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:
- Add
docs/integrations/dnd-scene-description-artifacts.mdas 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. - Update
docs/config.mdwith the maintained example link; extractor, normalizer, and validator keys; optionalplayers,party, andglossaryslots; and the exact default chains. - Update
docs/internal/modules.mdanddocs/internal/overview.mdfor the new codec/extractor/normalizer/validators and family registration. - Update
docs/internal/llm.mdwith 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. - Update
docs/internal/pipeline.mdonly where its current artifact-lane or source-attachment inventory requires the new lane; do not describe a new stage or dependency. - Update
docs/integrations/json-output.mdto link the new durable artifact contract alongside the other typed D&D schemas. - Add
examples/dnd-scene-descriptions.config.yml. It should usechunk: dnd/scenes, one scene-description artifact lane, andnormalize: dnd/scene-descriptions. Keep it minimal and do not present chunk annotations as the durable description artifact. - Mark
docs/roadmap/dnd-scene-descriptions.mdasStatus: Implemented, add a link to the canonical integration document near the top, and remove the implemented “Extract D&D Scene Descriptions” entry fromdocs/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
- Review the diff for accidental changes to
dnd/scenes, combat gating, framework stages, generated-reference behavior, or unrelated D&D contracts. - Confirm all new JSON objects reject unknown fields and the model is never asked to return application-owned identity/evidence.
- Confirm diagnostics contain no transcript or campaign-reference content.
- 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.
- Run:
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.