Files
notarius/docs/roadmap/implementation.md

27 KiB

D&D Module Harmonization Implementation Plan

This document is the executable implementation plan for the target state in D&D Module Harmonization And Prompt Reuse. It is written for a coding agent and must be followed in stage order. Each stage must leave the repository building and its focused tests passing before the next stage begins.

Current behavior is documented in the module internals and LLM runtime internals. The policies in Architecture, Testing, and Documentation govern all stages.

Fixed Decisions And Constraints

The following decisions are complete and are not implementation-time choices:

  • Keep spell, NPC, and combat-turn response DTOs, schemas, canonicalization, codecs, normalizers, and domain validators in their current domain packages.
  • Put D&D-only reuse in internal/modules/dnd/shared; do not move D&D concepts into internal/framework.
  • Keep private LLM response schemas separate from durable codec schemas. Do not introduce shared JSON Schema fragments or a schema-generation step in this work.
  • Preserve all public module keys, artifact kinds, prompt IDs, schema IDs, schema versions, media types, reference slot names, durable JSON fields, and existing reason-code strings. In particular, retain the spell validator's existing invalid_source_refs reason code as a compatibility exception.
  • Preserve central ownership and ordering of default validator chains in the D&D registrar.
  • Do not create a generic extractor framework, use reflection for lane registration, or erase typed artifact relationships outside existing framework boundaries.
  • Exact cache identity means equal ordered message roles, content bytes, and cache-control values after Scriptorium has rendered the prompt. Tests must use scriptorium.PreparedRun.Messages; comparing source Markdown is insufficient.
  • Scriptorium and provider-specific types remain confined to the LLM runtime, prompt-asset wiring, and their tests. Production extractors continue to use only Notarius contracts.
  • Default tests remain offline, deterministic, and credential-free. Live model evaluation is an explicit manual acceptance activity, not part of go test ./....
  • Avoid broad snapshots. Exact byte assertions are warranted only for the rendered common message prefixes because byte identity is the feature.
  • Update current-behavior documentation only in the stage that changes that behavior. Do not describe a partially implemented later stage as complete.

Target Prompt Layout

Stage 3 must produce the following ordered messages. All listed shared entries must refer to one shared embedded file rather than package-local copies.

Index Spell NPC Combat turn Role Cache control
0 common system common system common system system none
1 common extraction evidence common extraction evidence common extraction evidence user none
2 common in-world identity common in-world identity common in-world identity user ephemeral
3 common transcript common transcript common transcript user ephemeral
4 common campaign references common campaign references common campaign references user ephemeral
5 common immediate resolution NPC task common immediate resolution user none
6 common NPC registry NPC instructions common NPC registry user ephemeral for spell/combat
7 spell catalog combat task user none
8 spell task combat instructions user none
9 spell instructions user none

The common extraction-evidence asset must state, in artifact-neutral language, that:

  • transcript units are the only event evidence;
  • campaign and registry references may disambiguate but are not evidence;
  • every reported factual claim is supported by cited transcript units;
  • references use integer start_unit_id and end_unit_id values;
  • source_id is omitted because Notarius assigns the current source identity;
  • non-contiguous evidence uses multiple narrow ranges rather than a broad bridge over unrelated conversation; and
  • output contains only the configured JSON object and schema-defined fields.

The common in-world-identity asset must require the most specific supported in-world character or creature identity instead of a human player, transcript speaker, or the GM as an out-of-world person. It may permit campaign references to disambiguate an identity, but must not establish participation from a reference alone. NPC-only exclusion rules and relationship rules remain in the NPC task or instructions.

The common immediate-resolution asset, used only by spells and combat turns, must limit an artifact to a declaration/action and its immediate observed resolution. It must exclude consequences on later turns or elsewhere in the scene. Spell-only persistent-effect language and combat-only classification rules remain local.

Remove equivalent prose from package-local task and instruction files after it has moved to a shared asset. Do not retain paraphrased copies. Read each final prompt as a whole to remove contradictions and preserve all artifact-specific requirements.

The cache-control choices above create three all-lane breakpoints and one spell/combat breakpoint. Use no more than these four markers so the request remains portable across the configured backends; do not add module-specific markers in this work.

Stage 1: Characterize The Rendered Cache Boundary

Goal

Protect the current three-message common prefix and establish reusable test support before changing prompt composition.

Implementation

  1. Add a cross-lane prompt test under internal/modules/dnd/shared, using the external test package shared_test so it can import all three extractor packages without an import cycle.
  2. In one llm.AssetRegistry, register spell, NPC, and combat prompt assets. Build one offline Scriptorium engine with one test profile and use Engine.Prepare for every prompt. Supply identical transcript, players, party, and glossary inputs. Supply the same unbound NPC JSON to spell and combat, plus the spell catalog required only by spells.
  3. Add a small test helper that compares an expected prefix of []scriptorium.RenderedMessage field by field: role, exact content string, and a canonical JSON representation of cache control. Do not compare prompt IDs, rendered prompt hashes, output contracts, or schemas across modules; those are intentionally different.
  4. Assert that the first three messages are identical across all lanes and that spell/combat NPC grounding is identical at their current corresponding position. Assert the existing message counts separately so Stage 3 must update them deliberately.
  5. Extend internal/modules/dnd/shared/prompt_inputs_test.go with one table-driven identity test covering:
    • identical source material and references produce deeply equal common input materials;
    • reference item insertion order does not change rendered reference bytes;
    • roster fallback produces the canonical party material;
    • an explicit non-empty party wins over roster; and
    • missing optional slots render the existing single-space placeholder.
  6. Do not add a second full HTTP/provider test. The Scriptorium engine's prepared messages are the stable boundary owned by this repository; Scriptorium owns provider request serialization. Existing LLM adapter tests continue to prove that prepared message order and content reach the engine request.

Verification

Run:

go test ./internal/modules/dnd/shared ./internal/modules/dnd/extract/spells ./internal/modules/dnd/extract/npcs ./internal/modules/dnd/extract/combatturns

Completion Criteria

  • The new cross-lane test fails if any role, content byte, cache-control value, or order within the intended common prefix differs.
  • Tests distinguish intended prompt identity from intentionally different prompt IDs, schemas, and module-specific suffixes.
  • No production behavior changes in this stage.

Stage 2: Make Prompt Asset Manifests Exact

Goal

Use one explicit asset manifest for prompt mounting and prompt fingerprinting, and stop fingerprinting shared assets a prompt does not render.

Implementation

  1. Replace the broad sharedPromptFiles, CommonHashParts, and ReferenceHashParts grouping in internal/modules/dnd/shared/assets.go with an explicit manifest abstraction:

    type PromptAssetManifest struct {
        ModuleDir  string
        ModuleFiles []promptfs.ModulePromptFile
        SharedFiles []string
    }
    

    Exact formatting may follow gofmt, but retain these fields and meanings.

  2. Give the manifest two operations:

    • PromptFS(moduleFS fs.FS) (fs.FS, error), which resolves only the named shared files and delegates composition to promptfs.ModulePromptFS; and
    • Hash(moduleFS fs.FS) (string, error), which hashes the same module and shared files in manifest order through llm.HashAssets.
  3. Keep the shared-name-to-embedded-path mapping private to the shared package. Reject unknown, duplicate, empty, or path-containing shared names. Return fresh slices so callers cannot mutate package state.

  4. In each scriptorium_assets.go, declare one package-local manifest that includes its YAML definition and every Markdown file referenced by that definition. Keep ordering stable within the module and shared lists. Use that manifest for both RegisterPromptAssets and scriptoriumPromptMetadata.

  5. Migrate the scene chunker to the same API because it uses the shared prompt helper. Its behavior and prompt ordering remain unchanged in this stage.

  6. Mount only shared assets actually referenced by each prompt. Before Stage 3:

    • NPC and scene prompts must not include or hash common-dnd-npcs.md;
    • spell and combat prompts must include and hash it; and
    • all four prompts must include and hash the shared system, transcript, and campaign-reference files they render.
  7. Retain the response-schema fingerprint as its existing independent fingerprint. Do not include response schema bytes in the prompt manifest.

Tests

  • Replace broad shared asset tests with table-driven manifest tests for valid composition, exact mounted files, unknown names, duplicate names, invalid names, missing module files, missing shared files, and defensive copying.
  • Independently construct the expected llm.AssetHashPart list in manifest tests and assert that Hash equals llm.HashAssets over exactly that list. This proves unused shared assets are excluded and listed assets participate without adding mutation hooks for the embedded filesystem.
  • Keep one package-level registration test per prompt; remove redundant per-file mounting assertions when the shared manifest tests already own that behavior.

Verification

Run:

go test ./internal/framework/promptfs ./internal/modules/dnd/shared ./internal/modules/dnd/chunk/scenes ./internal/modules/dnd/extract/spells ./internal/modules/dnd/extract/npcs ./internal/modules/dnd/extract/combatturns

Completion Criteria

  • Mounting and hashing are driven by the same ordered manifest.
  • No prompt fingerprints an unused shared asset or omits a rendered asset.
  • Prepared prompt messages are unchanged from Stage 1.

Stage 3: Factor And Reorder Shared Prompt Messages

Goal

Implement the exact target prompt layout defined above and maximize the byte-identical all-lane and spell/combat prefixes.

Implementation

  1. Add these embedded assets under internal/modules/dnd/shared/assets/prompts:
    • common-dnd-extraction-evidence.md;
    • common-dnd-identity.md; and
    • common-dnd-immediate-resolution.md.
  2. Write their content according to the fixed semantic boundaries in Target Prompt Layout. Use no template inputs in the identity or immediate assets. The evidence asset also needs no new input; it refers generically to the transcript and references already presented later.
  3. Update the three extractor YAML files to use the exact order, roles, and cache-control values in the target table. Do not change prompt IDs, versions, default profiles, input declarations, output schema paths, or repair counts.
  4. Update each package's prompt manifest from Stage 2 so it lists exactly the new shared dependencies in rendered order.
  5. Delete duplicated policy prose from local task.md and instructions.md files while retaining every module-specific rule. Preserve one and only one Return exactly one JSON object rule through the common evidence message.
  6. Keep the spell catalog input and NPC registry input byte generation unchanged. Keep the NPC extractor free of an npcs input.
  7. Update package asset tests for new message counts and positions. Replace content-substring assertions for moved common policy with assertions in the cross-lane rendered-prefix test.
  8. Update the cross-lane test to assert:
    • indices 0 through 4 are identical for all three lanes when common inputs are identical;
    • indices 0 through 2 remain identical when transcript and references differ;
    • spell and combat indices 5 and 6 are identical for the same NPC registry;
    • the shared prefixes end at the exact indices in the table; and
    • module-specific suffixes are not accidentally identical or reordered.
  9. Update docs/internal/modules.md and docs/internal/llm.md in the same change to describe the implemented common-prefix composition, exact prompt manifest fingerprinting, and cache-control placement. Describe current behavior, not this staged plan.

Verification

Run:

go test ./internal/modules/dnd/shared ./internal/modules/dnd/extract/spells ./internal/modules/dnd/extract/npcs ./internal/modules/dnd/extract/combatturns ./internal/modules/dnd/register
go test ./internal/framework/llm

Completion Criteria

  • Prepared messages match the target table exactly.
  • Shared rules exist in one embedded file and have no local paraphrased copy.
  • Prompt fingerprints change for the intentional prompt contract change and include every newly rendered asset.
  • No artifact schema, durable representation, module selection, or reference contract changes.

Stage 4: Centralize Chunk Prompt Material

Goal

Remove the three identical chunkSourceInput implementations and make common transcript input preparation a single D&D-owned behavior.

Implementation

  1. Add internal/modules/dnd/shared/extraction_inputs.go with:

    func ChunkPromptMaterial(req contracts.TypedExtractionRequest) (contracts.LLMInputMaterial, error)
    
  2. Preserve the existing behavior exactly: clone req.SourceInput; fall back to chunk content and media type when content is empty; require content bytes to equal req.Chunk.Content; default the material name to source; default media type from the chunk; and populate SizeBytes when zero.

  3. The helper may assume the caller has already checked req.Chunk != nil. Return a D&D-shared error without an extractor name. Each extractor wraps it with its existing extractorErrorf, retaining module context.

  4. Replace all three local helpers and remove now-unused bytes imports.

  5. Do not centralize the remaining request checks. Their typed result handling and module-specific errors make the small duplication clearer than a callback- or generic-heavy abstraction.

Tests

  • Add one table-driven shared helper test covering fallback, clone isolation, mismatch, default fields, and preservation of explicit metadata.
  • Remove duplicate extractor tests only when the shared test fully owns the behavior. Retain one extractor-level test per lane proving helper errors are wrapped with that module's context.
  • Keep existing tests proving all three extractors pass equal common prompt inputs to the LLM contract.

Verification

Run:

go test ./internal/modules/dnd/shared ./internal/modules/dnd/extract/spells ./internal/modules/dnd/extract/npcs ./internal/modules/dnd/extract/combatturns

Completion Criteria

  • Only one production implementation prepares chunk prompt material.
  • Extractor behavior and public errors retain useful module context.
  • The rendered prompt identity tests still pass.

Stage 5: Share Cited Source Traversal

Goal

Give all relatedness validators one deterministic implementation for resolving, ordering, and deduplicating cited source units while keeping matching semantics artifact-specific.

Implementation

  1. Add internal/modules/dnd/shared/citations.go with:

    func CitedText(doc *source.SourceDocument, refs []source.SourceRef) (string, error)
    
  2. Validate every range with source.ValidateRef. Resolve ranges against document order, include each covered source unit once even when ranges overlap, and join included unit text with a single newline. Return an error for a nil document or any invalid range. Return "", nil for an empty ref slice with a non-nil document. Do not mutate the document or refs.

  3. Add table-driven tests for nil documents, empty refs, invalid source IDs, unknown/reversed unit IDs, disjoint ranges supplied out of order, adjacent ranges, and overlapping/duplicate ranges. Output must always follow document order.

  4. Replace spell, NPC, and combat relatedness validators' local cited-text traversal with shared.CitedText.

  5. When shape is invalid or CitedText returns an error, relatedness validators approve without relatedness warnings so shape/source-reference validators remain the sole owners of those defects.

  6. Keep matching local:

    • spells compare the canonical spell name case-insensitively against combined cited text;
    • NPCs use identity.ComparisonKey for names and aliases; and
    • combat uses its existing comparison-key actor logic and declaration token heuristic.
  7. Strengthen matching tests with Unicode/apostrophe variants, multiword names, overlapping ranges, and a short-name substring false-positive case. For names and actors, require token/word-boundary-aware matching rather than raw substring matching. For multiword values, match the consecutive normalized token sequence. Keep the combat declaration rule of at least one normalized token of four or more runes.

  8. Put shared normalized tokenization and consecutive-token matching in internal/modules/dnd/shared only if at least two validators use it after the change. Otherwise leave the matching helper local; do not create a single configurable matching engine.

Verification

Run:

go test ./internal/modules/dnd/shared ./internal/modules/dnd/validate/spells/source_relatedness ./internal/modules/dnd/validate/npcs/source_relatedness ./internal/modules/dnd/validate/combatturns/source_relatedness

Completion Criteria

  • Cited range validation, ordering, overlap handling, and text assembly have one production owner.
  • Relatedness remains warning-only and ignores invalid prerequisite data.
  • Artifact-specific matching policies remain understandable in their validator packages.

Stage 6: Align Validator Policy And Diagnostics

Goal

Make validator checkpoint identity, prerequisite handling, and diagnostic bounding consistent without changing validator-chain order or durable reason codes.

Implementation

  1. Add these local policy constants and CheckpointFingerprintProvider implementations:
    • spell shape: dnd.spells.validator.shape.v1;
    • spell source refs: dnd.spells.validator.source_refs.v1; and
    • spell source relatedness: dnd.spells.validator.source_relatedness.v1.
  2. Do not add a separate policy fingerprint to the spell catalog validator; its effective catalog digest remains its existing semantic checkpoint identity. If its non-catalog validation policy changes during this work, add a second policy fingerprint rather than replacing effective_catalog.
  3. Change spell source-reference validation to approve when spell shape is invalid, matching NPC and combat prerequisite behavior.
  4. Change spell source-reference validation to collect all reference issues, truncate individual errors with shared/diagnostics.Truncate, and return a bounded aggregate with shared/diagnostics.Aggregate. Preserve invalid_source_refs.
  5. Confirm NPC and combat source-reference validators follow the same prerequisite and bounded-aggregate policy; refactor only enough to share obvious local structure. Do not introduce a generic typed validator builder.
  6. Keep shape validators responsible for malformed artifact fields and source reference validators responsible for document/range validity.
  7. Add or consolidate package-level tests for policy fingerprints, invalid-shape deferral, aggregation of multiple invalid refs, bounded diagnostics, strict options, registration, and deterministic execution class. Prefer a small table of behavioral expectations in each typed package over a reflection- based cross-package harness.
  8. Update docs/internal/modules.md to describe the aligned prerequisite and checkpoint policy after it is implemented.

Verification

Run:

go test ./internal/modules/dnd/validate/...
go test ./internal/modules/dnd/register

Completion Criteria

  • Every deterministic validator policy affecting checkpoint reuse has an explicit semantic fingerprint.
  • Later validators do not duplicate shape rejection.
  • All source-reference diagnostic output is bounded.
  • Existing reason codes and validator-chain order are unchanged.

Stage 7: Clarify D&D Registration And Naming

Goal

Make production composition easier to audit without introducing heterogeneous generic descriptors or changing registration behavior.

Implementation

  1. Keep package internal/modules/dnd/register, but split its current concerns into focused files:
    • register.go: public Register, registry validation, and ordered execution of named registration functions;
    • modules.go: codecs, chunker, extractors, mergers, normalizers, no-op normalizers, and prompt asset registrations;
    • validators.go: production and generic test validator registrations;
    • chains.go: the six default extract/normalize chain mappings; and
    • merge.go: typed append functions and deep-clone helpers.
  2. Use small private functions such as registerModules, registerValidators, registerPromptAssets, and registerDefaultChains. Keep the existing ordered registration{name, register} error-context pattern within each group.
  3. Do not create one slice containing generic lane descriptors; Go cannot retain the heterogeneous typed codec and module relationships there without erasure or callbacks that obscure more than they clarify.
  4. Keep append and clone behavior in the register package for this change. Moving it would require a new lane-ownership package with no independent domain responsibility.
  5. Normalize import aliases in the registrar to the pattern spellextract, npcextract, combatextract, spellnormalize, npcnormalize, and combatnormalize, with corresponding validator aliases. This is internal naming only.
  6. Preserve registration order, error prefixes, default chain contents and order, reference-slot/spec behavior, and prompt asset collection.
  7. Update register_test.go only as required by file movement. Tests should continue asserting observable registry contents and chain policy, not the new private helper call graph.

Verification

Run:

go test ./internal/modules/dnd/register
go test ./internal/modules/dnd/...

Completion Criteria

  • Production composition is grouped by responsibility and remains explicit.
  • The refactor produces no registry, chain, capability, or error behavior change.
  • Tests do not couple to private registration helpers.

Stage 8: Final Integration, Documentation, And Evaluation

Goal

Verify the complete target state, update canonical current-behavior documents, and gather quality/cache evidence without making live services part of the default test suite.

Implementation And Review

  1. Re-read docs/roadmap/dnd.md and verify every completion criterion against production code and tests. Do not mark an item complete based only on this implementation plan.
  2. Review naming across the three lanes. Harmonize internal aliases and private policy constant names, but do not rename compatibility-sensitive identifiers listed in Fixed Decisions And Constraints.
  3. Review prompt and durable schemas for accidental duplication. Make no schema refactor unless composition already exists and the change is behavior-free; the fixed decision for this plan is to leave them package-owned and separate.
  4. Consolidate redundant tests created by intermediate stages. Retain:
    • one shared manifest test suite;
    • one exact rendered-prefix suite;
    • focused package tests for artifact-specific prompts and validators; and
    • existing production registration contract coverage.
  5. Update docs/internal/modules.md and docs/internal/llm.md so they are the canonical description of the final implemented behavior. Remove superseded implementation details rather than appending a second description.
  6. Update docs/roadmap/dnd.md to record implementation status. Remove completed future-work details that are fully owned by current internal documentation, leaving only genuinely deferred outcomes. Do not turn the feature roadmap into a second current-behavior reference.
  7. If credentials and the maintained human-reviewed transcript set are available, run an explicitly opt-in comparison using identical profiles and inputs before and after the prompt change. Record only aggregate extraction review results, prompt token counts, cached-token/cache-write metrics, and non-secret prompt hashes. Never commit transcript content, rendered prompts, credentials, endpoints, or private reference material.
  8. Live evaluation is not a merge gate when credentials or reviewed fixtures are unavailable. In that case, record the missing external prerequisite in the remaining roadmap item; do not add a fake cache-hit claim and do not weaken offline identity tests.

Repository Verification

Run all required checks:

go test ./...
go vet ./...
go build ./cmd/notarius
git diff --check

Also inspect the final diff for:

  • unintended public identifier or durable schema changes;
  • prompt rules duplicated between shared and local assets;
  • prompt assets rendered but absent from fingerprints, or fingerprinted but not rendered;
  • provider-specific types outside allowed boundaries;
  • tests containing real transcript/reference material or credentials; and
  • unrelated changes in a pre-existing dirty worktree.

Completion Criteria

  • Every completion criterion in the feature roadmap is either implemented and documented in its canonical current-behavior owner or explicitly retained as deferred roadmap work.
  • All focused and repository-wide checks pass.
  • The final test suite protects behavioral contracts without retaining redundant implementation snapshots.

Open Questions

None. The plan fixes all choices required for implementation. Live provider evaluation may depend on credentials and reviewed fixtures, but that is an external acceptance prerequisite rather than an unresolved design decision.