# D&D NPC Extraction And Registry Implementation Plan Status: Complete. Implement this plan in order. The feature policy and target state are defined in [D&D NPC Extraction And Registry](dnd-npc-extraction.md); this document owns implementation sequencing and concrete technical decisions. Do not introduce a DAG, implicit prior-run discovery, an LLM-backed validator, or generic semantic deduplication. Preserve the fixed `input -> chunk -> extract -> merge -> normalize -> output` architecture and the existing framework retry, warning, rejection, checkpoint, debug, and output contracts. ## Cross-Stage Decisions ### Artifact And Module Identities Use these exact production identities: - artifact kind: `dnd/npc-list`; - extractor key: `dnd/npcs`; - normalizer key: `dnd/npcs`; - extractor capability: `dnd.npcs`; - prompt ID: `dnd.npcs`, version `v1`; - private LLM response-schema key: `dnd_npcs_llm`; - private schema ID: `notarius.dnd.npcs.llm`; - private schema name: `notarius_dnd_npcs_llm_v1`; - durable codec schema ID: `notarius.dnd.npcs`; - durable codec schema name: `notarius_dnd_npcs_v1`; - durable schema version: `v1`; and - durable media type: `application/json`. The private LLM schema omits framework-assigned `id` and `source_id` fields. The durable codec schema includes both. ### Durable Go And JSON Shape Add the following canonical D&D types in `internal/modules/dnd/types.go`: ```go type NPCList struct { NPCs []NPC `json:"npcs"` } type NPC struct { ID string `json:"id"` Name string `json:"name"` Aliases []string `json:"aliases"` Description string `json:"description"` Relationships []NPCRelationship `json:"relationships"` SourceRefs []source.SourceRef `json:"source_refs"` } type NPCRelationship struct { Target string `json:"target"` Relationship string `json:"relationship"` } ``` All six NPC fields and both relationship fields are required in durable JSON. `npcs`, `aliases`, and `relationships` must be present arrays; the first may be empty and the latter two may be empty for an individual record. `source_refs` must contain at least one item. Required strings must be non-empty after trimming. Unknown JSON fields are rejected. One NPC-level `source_refs` collection collectively supports the canonical name, aliases, description, and relationships. Do not add per-alias or per-relationship evidence in this version. ### Canonical Identity And IDs Create `internal/modules/dnd/npcs/identity` as the sole owner of NPC identity comparison and ID derivation. Define comparison keys by applying, in order: 1. Unicode NFKC normalization; 2. replacement of U+2018, U+2019, and U+02BC with ASCII apostrophe; 3. Unicode whitespace collapsing with `strings.Fields` and one ASCII space; 4. Unicode case folding with `golang.org/x/text/cases.Fold`. Do not alter display punctuation merely to match a comparison key. Display normalization trims and collapses whitespace but otherwise retains the first observed spelling. For a non-empty comparison key, derive the ID as: ```text npc:sha256:<64 lowercase hexadecimal SHA-256 characters> ``` The hash input is the UTF-8 comparison key with no prefix, suffix, separator, or salt. An empty identity produces an empty ID and is rejected by shape validation. Export a stable identity-policy value `dnd.npcs.identity.v1`; components that depend on these rules use it as their `identity_policy` checkpoint fingerprint. A future semantic identity change must change this policy value. ### Deterministic Consolidation Normalizer identity components are built in merged input order. Union two records only when: - their canonical-name comparison keys match; or - either record's canonical-name key appears in the other record's alias keys. Do not union records merely because their alias sets intersect. This prevents a shared title from silently merging otherwise distinct NPCs. After consolidation, an alias owned by more than one retained component, or an alias matching another retained canonical name, is an identity collision and causes normalize-stage validation rejection. The first record in a component supplies the retained canonical display name, description, and output position. Add every distinct later canonical display name to its aliases, followed by later aliases in encounter order. Union relationships by the pair of target and relationship comparison keys, and union exact source references. Recompute the retained ID from the retained canonical name. Never ask an LLM to choose merged prose. After components are known, rewrite a relationship target to a retained NPC's canonical display name when its comparison key matches exactly one retained canonical name or alias. Preserve targets that have no NPC match; they may name a PC, group, place, or other non-registry entity. An ambiguous target remains unchanged and is covered by identity-collision validation when the ambiguity is an alias collision. ### Validation And Diagnostics All new production validators are deterministic. Use these exact keys and reason codes: | Validator key | Rejection/warning reason | | --- | --- | | `extract/dnd/npcs/shape` | `invalid_npc_shape` | | `extract/dnd/npcs/source_refs` | `invalid_npc_source_refs` | | `extract/dnd/npcs/source_relatedness` | warning `npc_not_near_source` | | `normalize/dnd/npcs/identity` | `invalid_npc_identity` | Shape validation owns required fields and non-empty values. Source-reference validation owns document identity, unit existence, and range ordering through `source.ValidateRef`. Identity validation owns ID syntax and recomputation, duplicate canonical identities, duplicate IDs, duplicate aliases, aliases equal to their own canonical name, cross-record alias/canonical collisions, and cross-record alias ownership. Relatedness approves the artifact and emits at most one warning per NPC when neither its canonical name nor any alias appears in the combined cited source text after the same Unicode comparison normalization. Opaque campaign references may legitimately explain such a warning; do not treat them as source evidence or attempt to parse them deterministically. Bound diagnostics using the established spell-validator policy: display at most 20 issues, truncate displayed identity values to 128 Unicode code points with an ellipsis, keep encoded rejection messages at or below 4,096 bytes, and report the total omitted issue count. Preserve valid UTF-8 and use Go quoting for control characters. Warnings must likewise use bounded displayed values. Each new validator implements `CheckpointFingerprintProvider` with one local `policy` fingerprint. Use these exact values: - shape: `dnd.npcs.validator.shape.v1`; - source references: `dnd.npcs.validator.source_refs.v1`; - source relatedness: `dnd.npcs.validator.source_relatedness.v1`; and - identity: `dnd.npcs.identity.v1`. Bump only the affected value when validator semantics change. ### Testing Rules Follow `docs/policy/testing.md`. Tests must be offline and deterministic. Use a fake structured LLM only at the external completion boundary. Protect durable artifact contracts, identity and normalization invariants, validation outcomes, registration, checkpoint identity, and representative assembled workflows. Do not add tests that require specific words or phrases to remain in prompt prose. Prompt-asset tests may verify that assets register, required inputs are wired, the selected prompt and schema identities are correct, and raw material does not leak into diagnostics. Human review, not exact model-output fixtures, owns semantic extraction quality. ## Stage 1: NPC Domain Contract, Identity Policy, And Codec ### Goal Establish the typed artifact, deterministic identity primitives, and durable serialization contract without registering a selectable production module. ### Changes - Add `NPCListKind`, `NPCList`, `NPC`, and `NPCRelationship` to the canonical D&D model using the exact shape above. - Add `internal/modules/dnd/npcs/identity` with defensive, concurrency-safe pure functions for display normalization, comparison keys, ID derivation, ID syntax checks, and whole-registry identity validation. - Identity validation must return structured or otherwise inspectable issues so the normalize validator can build bounded aggregate diagnostics. Do not make it depend on pipeline validator types. - Add `internal/modules/dnd/codec/npcs`, following the spell codec's candidate versus approved encode/decode boundary: - strict single-value JSON decoding with unknown-field rejection; - `EncodeCandidate` and `DecodeCandidate` preserve validator-visible invalid values; - approved `Encode` and `Decode` enforce structural validity; - schema and metadata are defensive copies; and - metadata reports `npc_count` only. - Add the durable JSON Schema at `internal/modules/dnd/codec/npcs/assets/schemas/dnd_npcs.v1.json` with the exact required fields, ID pattern, array rules, source-reference shape, and `additionalProperties: false` at every object level. - Create `docs/integrations/dnd-npc-artifacts.md` as the canonical durable contract. At this stage describe the implemented artifact and codec only; do not yet claim that production pipelines can select it. ### Tests - Test identity equivalence across case, Unicode compatibility forms, whitespace, and supported apostrophes; different names must remain distinct. - Test the exact ID format, deterministic repeatability, empty-name behavior, and registry identity issues for every collision category. - Test codec candidate preservation, approved round trips, schema identity and defensive copies, nil-versus-empty arrays, malformed/trailing/unknown JSON, required strings, relationship shape, ID pattern, and source-reference structure. - Keep one small intentional durable JSON fixture for round-trip compatibility; do not snapshot incidental error text. ### Completion Check Run `go test ./internal/modules/dnd/npcs/... ./internal/modules/dnd/codec/npcs` and `git diff --check`. ## Stage 2: NPC Extractor And Extraction Validators ### Goal Add a package-complete LLM-backed NPC extractor and deterministic extraction validation, still without composing it into the production D&D registrar. ### Changes - Add `internal/modules/dnd/extract/npcs`, mirroring the established spell extractor boundaries without sharing spell-specific code. - The module spec requires `chunks` and `source.transcript`, provides `dnd.npcs`, uses artifact kind `dnd/npc-list`, accepts no options, and declares the existing optional `players`, `party`, `glossary`, and deprecated `roster` campaign slots with the existing shared media types. - Embed and register package-owned prompt assets and a private LLM JSON Schema. Reuse the shared D&D system, transcript, and campaign-reference prompt fragments through `shared.ModulePromptFS`. - The prompt implements the feature-roadmap inclusion and exclusion policy, requires canonical in-world names rather than speakers, keeps descriptions concise, requires every claim to be supported by transcript citations, and treats auxiliary references only as disambiguation. - The private response has top-level `npcs`; each item contains `name`, `aliases`, `description`, `relationships`, and source ranges without `source_id`. Require `aliases` and `relationships` arrays even when empty. - Preserve malformed model values for validators. Canonicalize and de-duplicate exact source ranges, order response records by earliest valid cited unit, map every range to the current source ID, and assign IDs with the identity package. Do not merge NPC records in the extractor. - Validate constructor dependencies and embedded prompt/schema metadata during preparation. Expose manifest-safe prompt and private-schema identities plus `identity_policy`; expose `prompt`, `response_schema`, and `identity_policy` checkpoint fingerprints. - Add typed validators under `internal/modules/dnd/validate/npcs/shape`, `source_refs`, and `source_relatedness` using the contracts above. Validators defer appropriately after shape failure rather than emitting misleading secondary diagnostics. - Give each validator a strict empty-options decoder, immutable spec, correct execution class, typed builder, policy fingerprint, and nil/dependency error behavior consistent with existing production validators. ### Tests - Through a fake LLM, test request identity, profile/session propagation, chunk-scoped transcript bytes, campaign reference inputs, response mapping, deterministic IDs, source-ID assignment, evidence ordering, and preservation of invalid candidates for retries. - Test cancellation and contextual provider errors without asserting entire messages. - Test prompt and schema asset registration, input wiring, metadata redaction, and checkpoint fingerprints without asserting prompt prose. - Test each validator's approval, rejection, warning, malformed-shape deferral, strict options, typed registration, diagnostic limits, Unicode truncation, and immutability. - Test source relatedness using canonical names, aliases, case/whitespace/ apostrophe variants, invalid ranges, and names resolved only through opaque references. ### Completion Check Run `go test ./internal/modules/dnd/extract/npcs ./internal/modules/dnd/validate/npcs/...` and `git diff --check`. ## Stage 3: Deterministic NPC Normalization ### Goal Implement conservative cross-chunk identity consolidation and final identity validation without changing framework normalization contracts. ### Changes - Add `internal/modules/dnd/normalize/npcs` with key `dnd/npcs`, artifact kind `dnd/npc-list`, no options, no references, requirements `merged`, and capability `normalized`. - Deep-clone all nested slices before mutation. A result must never alias the merged input or another retained record. - Per record, normalize display whitespace, trim description and relationship strings, de-duplicate aliases and relationships by comparison keys, remove aliases equal to the canonical name, sort and exactly de-duplicate source references, and recompute the ID. - Build and merge identity components with the exact cross-stage algorithm. Preserve component and member encounter order. Add removed canonical names as aliases, union provenance, retain only the first description, and canonicalize unambiguous relationship targets after all components exist. - Use these stable warning reason codes: - `npc_fields_normalized`; - `npc_id_recomputed`; - `source_references_normalized`; - `duplicate_npc_collapsed`; and - `relationship_target_canonicalized`. - Warning scopes use merged input indexes such as `npcs[3]`. A collapsed-group warning is scoped to the retained input index and reports bounded removed indexes. Accepted-only warning promotion remains framework policy. - Expose manifest metadata `identity_policy` and `normalization_policy: dnd.npcs.normalize.v1`. Provide those same two local checkpoint fingerprints. Bump the normalization value whenever consolidation or retained-field semantics change. - Add `internal/modules/dnd/validate/npcs/identity`. It delegates identity issue detection to the domain identity package, builds bounded diagnostics, and is registered only in the normalize default chain during Stage 4. ### Tests - Use table-driven domain cases for per-record normalization and alias, relationship, source-reference, and ID behavior. - Use a compact accepted-output fixture for representative multi-record consolidation: exact canonical match, canonical-to-alias match, transitive unambiguous match, shared-alias non-merge, first-description retention, relationship target rewriting, and stable ordering. - Prove ambiguous shared aliases remain separate for identity-validator rejection and that invalid evidence is not made valid by normalization. - Prove input immutability and independent nested output storage. - Test warning scopes/reason codes, normalization and identity fingerprints, strict options, registration, cancellation, nil input, and bounded identity rejection diagnostics. - Do not separately test private union-find or graph helpers when the public normalizer cases already protect the behavior. ### Completion Check Run `go test ./internal/modules/dnd/normalize/npcs ./internal/modules/dnd/validate/npcs/identity` and `git diff --check`. ## Stage 4: Production Composition And NPC Pipeline ### Goal Make the complete NPC lane selectable and verify the assembled framework path before adding it as a reference consumer elsewhere. ### Changes - Extend `internal/modules/dnd/register` to register: - the NPC codec; - NPC extractor and prompt assets; - typed `appendorder` merger specialization for `dnd.NPCList`; - NPC normalizer and typed `noop` specialization; - all four NPC validators; - typed always-accept and always-reject validator variants; and - default validator-chain mappings. - Add a small `appendNPCLists` function that preserves chunk and record order and distinguishes nil from present-empty output consistently with the spell merger. - Register the extract default chain in this order: `generic/valid_json`, `generic/valid_json_schema`, NPC shape, NPC source refs, NPC source relatedness. - Register the normalize default chain in this order: `generic/valid_json`, `generic/valid_json_schema`, NPC shape, NPC identity, NPC source refs, NPC source relatedness. - Do not register an NPC merge default chain and do not change global framework defaults. - Add a synthetic, non-sensitive Seriatim integration fixture with a PC, a repeated named NPC, an alias, an unnamed but distinguishable NPC, and an interchangeable group that the fake response omits. - Add `examples/dnd-npcs.config.yml` as a complete version-3 configuration. Use a generic chunker, an `npcs` artifact lane, `dnd/npcs` extraction with `retries: 2`, `dnd/npcs` normalization, explicit checkpoint `enabled: false`, and safe relative output/debug paths. ### Tests - Extend family registration tests for keys, artifact variants, default chain order, prompt/schema assets, nil registry failures, and duplicate registration errors. Test the public catalog outcome, not private registrar call order. - Add config-resolution coverage for the assembled NPC lane, capabilities, typed codec/merger/normalizer compatibility, strict options, references, and invalid validator placement. - Add one runner integration test using a fake LLM that exercises Seriatim -> chunks -> NPC extract -> validators -> append merge -> NPC normalize -> validators -> JSON output. Assert normalized identities, provenance, warnings, manifest metadata, and durable lane schema. - Add one representative retry/rejection case at the assembled boundary only if existing generic runner tests do not already protect the same mechanism; do not duplicate the framework retry matrix. - Verify the maintained example through the existing config/example contract tests. ### Completion Check Run `go test ./internal/modules/dnd/register ./internal/modules/integration ./internal/core/config` and `git diff --check`. ## Stage 5: NPC Registry Reference For Spell Extraction ### Goal Complete the operator-driven sequential workflow by making normalized NPC JSON an explicit, validated spell-extractor reference. ### Changes - Add an optional `npcs` reference slot to the spell extractor only. Accept exactly one `application/json` item with a maximum size of 1,048,576 bytes. Do not add it to the shared scene or NPC-extractor campaign slots. - During spell-extractor preparation, when `npcs` is bound: - require exactly one item; - decode it with the approved NPC codec; - validate its IDs and alias ownership through the NPC identity package; - re-encode canonical durable JSON for the prompt; and - compute a semantic SHA-256 digest over those canonical bytes. - Permit source references inside the registry to identify another session; they are registry provenance, not spell evidence. Do not validate them against the current transcript. - When the slot is unbound, supply the prompt with exactly `{"npcs":[]}` as the empty structured value. Do not create reference manifest provenance or an `npc_registry` fingerprint for the absent slot. - Declare an optional `application/json` `npcs` prompt input and render it in a spell-owned prompt message. Do not modify the shared D&D reference fragment, because the scene and NPC prompts do not consume this registry. - Instruct the spell model to prefer exact canonical NPC names, recognize aliases, and never treat registry content as proof that a spell was cast or as source evidence. Preserve the existing party/player policy for PCs. - When bound, add manifest metadata `npc_registry_digest` and `npc_count`, and add local checkpoint fingerprint `npc_registry` with the semantic digest. Never expose names, aliases, reference content, paths, or raw bytes in metadata or fingerprints. Existing raw reference provenance continues to participate independently in checkpoint identity. - Add `examples/dnd-npc-spell-sequential.config.yml` with separate `dnd-npcs` and `dnd-spells` pipeline definitions over the same Seriatim input shape. The NPC output remains intentionally unbound in the static spell definition because its run directory is dynamic; demonstrate the explicit runtime binding in documentation. - Update current-behavior documentation in canonical locations: - `docs/config.md`: module and validator catalogs, NPC reference slot, limits, default chains, and maintained examples; - `docs/cli.md`: one explicit selector example using `spells.extract.npcs=/lanes/npcs.json`; - `docs/operations.md`: the two-invocation sequential workflow and state sensitivity; - `docs/integrations/dnd-npc-artifacts.md`: production identity, final normalization, warnings, manifest metadata, and reference-consumer semantics; - `docs/integrations/dnd-spell-artifacts.md`: optional NPC grounding and its non-evidence rule; - `docs/integrations/json-output.md`: link the NPC lane payload contract; - `docs/internal/overview.md`, `docs/internal/modules.md`, and `docs/internal/llm.md`: implemented package inventory and internal prompt, preparation, fingerprint, and registration behavior. - At feature completion, mark the feature roadmap and this implementation plan complete. Remove the now-implemented NPC item from `future.md`; keep combat turns as the next future sequential artifact. Do not leave future behavior in current-behavior documentation. ### Tests - Test spell preparation with an absent registry, a valid normalized registry, malformed JSON, unknown fields, invalid IDs, alias collisions, multiple items, wrong media type, and the configured byte limit. Materialization-limit failures must occur before pipeline execution and checkpoint construction. - Through the fake spell client, assert canonical NPC JSON input, input metadata, manifest-safe count/digest, and absence of content leakage. Do not assert prompt wording. - Test that identical semantic registries produce the same component fingerprint, semantic changes change it, and returned fingerprints cannot be mutated through caller-owned slices. Do not repeat generic checkpoint ordering tests already owned by the framework. - Add one sequential integration case: run the assembled NPC pipeline with a fake NPC response, serialize its normalized lane payload, materialize that payload as the spell extractor's `npcs` reference, and run the spell pipeline with a fake spell response. Verify reference provenance, prompt input, typed output, and that no NPC source reference becomes spell evidence. - Validate all new documentation links and maintained example configurations. ### Completion Check Run the final verification suite below. ## Final Verification Run: ```sh git diff --check go test ./... go vet ./... go build ./cmd/notarius go test -race ./internal/modules/dnd/... ./internal/framework/pipeline \ ./internal/cli ./internal/modules/integration ``` Review the final diff for: - accidental framework or provider-specific D&D behavior; - mutation of caller-owned artifacts, schemas, references, metadata, or fingerprints; - prompt/schema/content leakage into manifests, errors, checkpoint identity, or redacted summaries; - current documentation claiming behavior before its implementing stage exists; - redundant tests or prompt-prose change detectors; and - unrelated changes in the worktree. ## Open Questions None. The artifact shape, identity algorithm, merge policy, validator placement, reference parsing, checkpoint semantics, and documentation ownership are fixed by this plan.