From 2ee6b495e1f8e12ff192e29f273f9bde27dd7356 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 6 Aug 2026 13:28:12 +0000 Subject: [PATCH] Consolidate duplicate item registry records --- docs/roadmap/implementation.md | 1096 +++++------------ .../dnd/normalize/itemregistry/normalizer.go | 9 +- .../normalize/itemregistry/normalizer_test.go | 52 +- 3 files changed, 355 insertions(+), 802 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 64b114c..adc718e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,858 +1,366 @@ -# Canonical D&D Entity Registries And Occurrences Implementation Plan +# Canonical D&D Entity Contracts Remediation Plan ## Objective -Implement the target state defined in -[Canonical D&D Entity Registries And Occurrences](entity-registries.md). NPCs, -locations, and items will each have one canonical registry lane and one -registry-backed occurrence lane, with consistent module, artifact, schema, -prompt, reference, validation, package, asset, example, and documentation -vocabulary. +Address the remaining correctness and maintainability findings discovered after +implementing [Canonical D&D Entity Registries And Occurrences](entity-registries.md). +The work must preserve the completed six-lane registry/occurrence architecture +while fixing deterministic item identity consolidation, hardening currency +reconciliation, making item-occurrence grounding fail closed, removing an +unnecessary NPC-identity dependency from item behavior, and aligning NPC +registry eligibility with the downstream `mentioned` occurrence contract. -This is a breaking pre-release migration. The completed implementation must not -retain compatibility aliases for old module keys, artifact kinds, types, -schemas, prompts, validators, reference slots, or asset paths. The stages below -deliberately change durable contracts before moving the corresponding module -namespace so that each prompt remains bounded for gpt-5.6-terra and the -repository can compile and pass focused tests after every stage. +Complete Stages 15 through 18 in numerical order. Stages 1 through 14 are +already complete and are summarized below rather than retained as historical +implementation instructions. Each remaining stage is intentionally bounded for +one gpt-5.6-terra implementation prompt. Do not combine stages unless a later +review explicitly revises this plan. -Complete the stages in numerical order. Each stage is sized for one -implementation prompt. Do not combine stages unless a later review explicitly -revises this plan. +## Completed Work Summary: Stages 1 Through 14 -## Final Contract Matrix +The completed stages established the target state in the feature roadmap: -Use these exact final identities. Do not invent variants during implementation. +- NPCs, locations, and items now each have a canonical registry lane and a + registry-backed occurrence lane under consistent `dnd/-registry` and + `dnd/-occurrences` module keys. +- Durable artifact types, kinds, `v1` schemas, codecs, evidence projections, + mergers, validators, prompts, assets, capabilities, and package namespaces + use registry/occurrence vocabulary without compatibility aliases. +- Registry records contain deterministic IDs, canonical names, and transcript + evidence. Occurrence records carry exact registry ID/name pairs and their own + current-transcript evidence. +- The complete D&D example produces all three registries in an earlier step and + supplies them through explicit generated references to later occurrence and + action lanes. Enemy events consume the NPC registry and combat-opponent NPC + occurrences in a later derived step. +- Production registration, default validator chains, integration contracts, + current-behavior documentation, maintained examples, and representative + ordered-pipeline tests were migrated to the new contracts. +- A final repository audit confirmed that the retired pre-release module, + artifact, schema, prompt, reference-slot, and package names no longer remain + as production compatibility paths. -| Entity role | Module key | Go artifact type | Artifact kind | Durable schema ID | Durable root | -| --- | --- | --- | --- | --- | --- | -| NPC registry | `dnd/npc-registry` | `dnd.NPCRegistry` | `dnd/npc-registry` | `notarius.dnd.npc_registry` | `npcs` | -| NPC occurrences | `dnd/npc-occurrences` | `dnd.NPCOccurrenceList` | `dnd/npc-occurrence-list` | `notarius.dnd.npc_occurrences` | `occurrences` | -| Location registry | `dnd/location-registry` | `dnd.LocationRegistry` | `dnd/location-registry` | `notarius.dnd.location_registry` | `locations` | -| Location occurrences | `dnd/location-occurrences` | `dnd.LocationOccurrenceList` | `dnd/location-occurrence-list` | `notarius.dnd.location_occurrences` | `occurrences` | -| Item registry | `dnd/item-registry` | `dnd.ItemRegistry` | `dnd/item-registry` | `notarius.dnd.item_registry` | `items` | -| Item occurrences | `dnd/item-occurrences` | `dnd.ItemOccurrenceList` | `dnd/item-occurrence-list` | `notarius.dnd.item_occurrences` | `occurrences` | +These completed contracts are the implementation baseline. The remaining +stages are corrective work, not a second namespace or schema migration. -All durable contracts use schema version `v1` and media type -`application/json`. Registry records have required `id`, `name`, and -`source_refs`. NPC, location, and item occurrences have required `npc_id`, -`location_id`, or `item_id` respectively, plus required `name`, `kind`, and -`source_refs`. Item occurrence `quantity`, `from`, and `to` remain semantically -conditional durable fields. +## Baseline Contract Decisions -Use these exact identity policies and digest inputs: +The implementation agent must preserve these decisions throughout the four +remaining stages: -- NPC: `dnd.npc_registry.identity.v1`, SHA-256 of compact JSON - `[policy, comparison_name]`, prefix `npc:sha256:`; -- location: `dnd.location_registry.identity.v1`, SHA-256 of compact JSON - `[policy, comparison_name, source_id, start_unit_id, end_unit_id]` using the - earliest canonical evidence anchor, prefix `location:sha256:`; and -- item: `dnd.item_registry.identity.v1`, SHA-256 of compact JSON - `[policy, comparison_name]`, prefix `item:sha256:`. +- Item identity is name-based: `item:sha256:` plus the lowercase SHA-256 of + compact JSON `["dnd.item_registry.identity.v1", comparison_name]`. + Consequently, two item-registry records with the same comparison name cannot + remain separate merely because their evidence differs. +- NPC identity is likewise name-based; location identity additionally includes + its earliest evidence anchor. Do not change any durable ID algorithm in this + work set. +- Exact comparison-name duplicates are resolved deterministically before any + semantic LLM reconciliation. The model is used only for supported alias or + semantic-equivalence proposals that deterministic identity cannot decide. +- Currency denominations are separate item identities. Singular, plural, and + standard abbreviations for the same denomination may be reconciled, but a + currency name may never be consolidated with another denomination or with a + non-currency item. +- Every occurrence must contain an exact registry ID/name pair. An extractor + response with an unknown ID or mismatched name is invalid output and must + cause the extraction attempt to fail; silently dropping a record is not an + allowed repair. +- A named or stably designated NPC may enter the NPC registry when established + only through a supported third-party mention. Hypothetical, speculative, + generic, or unestablished identities remain ineligible. +- Registry references provide source-free identity grounding and never become + occurrence evidence. No stage may weaken this boundary. -Registry reference slots and corresponding PromptKit inputs are exactly -`npc_registry`, `location_registry`, and `item_registry`. Enemy-event grounding -uses `npc_occurrences` for the renamed NPC occurrence artifact. - -## Rules For Every Stage +## Rules For Every Remaining Stage - Read and follow `docs/policy/architecture.md`, `docs/policy/documentation.md`, and `docs/policy/testing.md` before editing. -- Preserve the fixed pipeline architecture, typed artifact zone, ordered-step - barrier, generated-reference provenance, whole-run failure behavior, - scheduler, checkpoint, and output boundaries. This feature requires no new - framework workflow primitive and no new ADR. -- Keep `assets/assets.go` as the root assets package's only Go file. It remains - a read-only content boundary with no business logic, PromptKit dependency, - registry behavior, or module registration. -- Keep D&D semantics inside `internal/modules/dnd` and `assets/dnd`. Do not - implement the future generic LLM-assisted deduplicator in this work set. -- Do not create compatibility type aliases, duplicate module registrations, - legacy schema decoders, fallback reference-slot names, copied prompt assets, - or deprecated-key shims. Intermediate stages may retain a current module key - until its explicit namespace-migration stage, but must not register both old - and new keys. -- Preserve minimal evidence-grounded contracts. Registry references are - source-free grounding and never occurrence evidence. Every occurrence must - cite current-transcript ranges independently. -- Private structured-response schemas must reject unknown fields and include - every declared property in `required`. Use nullable private fields for - semantically absent item values and map them deterministically to omitted - durable fields. -- Preserve prompt-cache construction: extraction prompts share the byte-identical - system/identity/reference/chunk prefix; evidence and registry projections - follow that prefix; module instructions remain final. Registry normalizers - retain system, instructions, shared reconciliation policy, candidates, and - transcript-window order. -- Update current-behavior documentation when a stage changes an implemented - contract. Do not describe a not-yet-registered item module outside - `docs/roadmap/` before its production-registration stage. -- Keep default tests offline, deterministic, and credential-free. Use package - behavior tests for codecs, identity, registry resolution, normalization, and - validators; integration tests for generated handoffs; and CLI tests for - assembled examples. Do not add live-LLM tests, exact prompt snapshots, private - asset inventories, or duplicate change-detector tests. -- Preserve unrelated user changes. Use `gofmt`, `git diff --check`, and focused - tests in every stage. Do not retire this roadmap or the feature roadmap as - part of implementation. +- Preserve the fixed pipeline, typed artifact, generated-reference, scheduler, + validation, checkpoint, output, and whole-run failure boundaries. +- Keep D&D policy in `internal/modules/dnd` and `assets/dnd`. Shared text policy + belongs in a D&D shared helper, not a generic framework package. +- Keep `assets/assets.go` as the root assets package's only Go file and keep the + root package free of business logic and PromptKit dependencies. +- Preserve strict private structured-response schemas: reject unknown fields + and list every declared property in `required`. +- Keep prompt-cache ordering and the byte-identical shared D&D prefix intact. +- Update the canonical current-behavior integration or internal documentation + in the same stage as any implemented contract or failure-semantics change. + Link to canonical owners instead of duplicating volatile facts. +- Keep tests deterministic, offline, and credential-free. Add regression tests + at the narrowest stable behavior boundary. Do not add live-LLM tests, exact + prompt snapshots, prompt-prefix length detectors, private asset inventories, + or tests that merely freeze helper placement. +- Preserve unrelated user changes. Run `gofmt` on changed Go files and + `git diff --check` in every stage. Do not retire this plan or the feature + roadmap during implementation. -## Stage 1: Migrate The NPC Registry Durable Contract And Identity +## Stage 15: Consolidate Deterministically Identical Item Registry Records ### Goal -Adopt the final NPC registry type, artifact kind, schema identity, and identity -algorithm while the selectable module temporarily remains `dnd/npcs`. +Make item-registry normalization produce exactly one record for every item +comparison name, regardless of whether duplicate candidates cite identical or +different transcript ranges. ### Required changes -1. In `internal/modules/dnd/types.go`, replace `NPCListKind` and `NPCList` with - `NPCRegistryKind` and `NPCRegistry`. Retain `NPC` with required `ID`, `Name`, - and `SourceRefs`, and retain the durable root field `npcs`. -2. Update every typed consumer, merger, evidence projection, validator, - normalizer, extractor, test fixture, integration helper, and registry - registration to use `dnd.NPCRegistry` and `dnd.NPCRegistryKind`. Do not leave - a Go alias for the retired names. -3. In the existing NPC codec package, replace the durable schema with - `dnd_npc_registry.v1.json`, schema ID `notarius.dnd.npc_registry`, schema name - `notarius_dnd_npc_registry_v1`, and version `v1`. Keep the strict `npcs` - envelope and `application/json` media type. -4. Change the NPC identity policy to `dnd.npc_registry.identity.v1`. Derive IDs - from compact JSON `[policy, comparison_name]`, not the current bare comparison - string. Preserve Unicode NFKC, apostrophe normalization, whitespace collapse, - case folding, empty-input rejection, lowercase SHA-256, and the existing ID - prefix. -5. Update registry validation and tests to prove the documented bytes, policy - namespace, deterministic IDs, uniqueness, concurrency safety, malformed-ID - rejection, and non-mutation. Update dependent fixtures to use newly derived - IDs rather than copied old digests. -6. Update the current `docs/integrations/dnd-npc-artifacts.md` in place so its - artifact kind, schema identity, Go-independent ID algorithm, and wire example - match this stage. It must continue to name `dnd/npcs` as the current producer - until Stage 2. +1. Revise deterministic preprocessing in + `internal/modules/dnd/normalize/itemregistry` so grouping is keyed by the + item identity comparison name alone. Remove source-reference equality as a + condition for grouping records whose deterministic IDs are necessarily + identical. +2. For each comparison-name group: + - retain the earliest input record as the deterministic display-name and + ordering owner; + - union source references from every member; + - canonicalize and deduplicate the union with the existing + `shared.SourceRefOrder` behavior; + - retain the sorted unique set of original input indexes for diagnostics; + and + - derive the final ID from the retained canonical display name through the + existing item identity package. +3. Keep this consolidation entirely deterministic. Do not send equal-key + candidates to the LLM reconciliation prompt, and do not change semantic + alias reconciliation for records with different comparison names. +4. Preserve stable output order, warning bounds, content-safe diagnostics, + nil/empty collection conventions, and input non-mutation. Remove imports or + helpers made obsolete by dropping source-reference equality. +5. Replace the existing test expectation that equal-name items with different + evidence survive as two records. Add focused behavior coverage proving that + such records become one valid item with unioned canonical evidence, one + deterministic ID, earliest-display-name retention, stable placement, and no + input mutation. +6. Include a normalizer/default-chain regression case showing that normalized + output with cross-chunk equal-name candidates passes item-registry identity + validation rather than producing duplicate canonical-name or duplicate-ID + rejection. ### Acceptance criteria -- No production Go code refers to `NPCList` or `NPCListKind`. -- The registered codec round-trips the strict `NPCRegistry` `v1` contract and - reports the target kind and schema identity. -- All current NPC-grounded consumers compile against `NPCRegistry` without - changing their reference-slot names yet. -- There is no compatibility alias or decoder for `dnd/npc-list` or - `notarius.dnd.npcs`. - -### Validation - -Run `gofmt` on every Go file changed in this stage, then run: - -```sh -go test ./internal/modules/dnd/... ./internal/modules/integration/... -git diff --check -``` - -## Stage 2: Move The NPC Registry Module Into Its Canonical Namespace - -### Goal - -Rename the NPC registry's selectable modules, packages, prompts, assets, -validators, capabilities, and reference slots without changing the durable -record shape completed in Stage 1. - -### Required changes - -1. Move the stage packages to Go-compatible canonical paths: - `codec/npcregistry`, `extract/npcregistry`, `normalize/npcregistry`, and - `validate/npcregistry/...`. Domain helpers under `internal/modules/dnd/npcs` - may remain because they own NPC identity rather than a selectable module. -2. Set extract and normalize module keys to `dnd/npc-registry`. Rename all NPC - validator keys from `.../dnd/npcs/...` to `.../dnd/npc-registry/...`, and - rename the provided capability to `dnd.npc_registry`. -3. Move `assets/dnd/npcs/{extract,normalize}` to - `assets/dnd/npc-registry/{extract,normalize}`. Use prompt IDs - `dnd.npc_registry` and `dnd.npc_registry.normalize`; use private schema ID - `notarius.dnd.npc_registry.llm`, schema name - `notarius_dnd_npc_registry_llm_v1`, and filename - `dnd_npc_registry_llm.v1.json`. -4. Rename `common-dnd-npcs.md` to `common-dnd-npc-registry.md`. Change the - rendered PromptKit input and every module-facing reference slot from `npcs` - to `npc_registry` in spells, combat turns, the still-current NPC-interaction - modules, and enemy events. Preserve requiredness: optional for spell and - combat grounding, required where the current consumer already requires an - NPC registry. -5. Update immutable NPC registry projections to render ordered `{id, name}` - pairs where identity is required and the narrowest source-free name - projection where a consumer intentionally needs names only. Do not expose - registry source references to prompts. -6. Update central module, codec, merger, evidence, validator, default-chain, - prompt, and profile registration. Update the cross-lane prompt composition - test for the new prompt ID and `npc_registry` input without duplicating its - ordering assertions in module-local tests. -7. Rename the integration contract to - `docs/integrations/dnd-npc-registry-artifacts.md`, update inbound links, and - update the implemented module catalog, configuration reference, current - internal D&D guide, and maintained examples for the new key and slot. -8. Remove the old package trees, asset tree, prompt IDs, module/validator keys, - capability, shared filename, and reference-slot name. The durable JSON field - `npcs`, the record type `NPC`, and domain helper package name `npcs` are not - legacy module identifiers and should remain. - -### Acceptance criteria - -- `dnd/npc-registry` is the only selectable NPC registry extract/normalize key. -- All NPC-registry consumers expose `npc_registry` and accept only - `dnd/npc-registry` artifacts. -- Prompt and schema assets load only from `assets/dnd/npc-registry` and retain - the documented cache order. -- Current docs and examples contain no claim that `dnd/npcs` is selectable. +- Item normalization cannot emit two records with the same item comparison + name or deterministic ID solely because their evidence differs. +- The retained record contains the canonical union of every duplicate member's + evidence and remains stable across repeated normalization. +- Different comparison names remain eligible for the existing bounded semantic + reconciliation path. +- No durable schema, item ID algorithm, module key, or artifact kind changes. ### Validation ```sh -go test ./internal/modules/dnd/... ./internal/modules/integration/... ./internal/cli/... -go run ./cmd/notarius config validate --config examples/dnd-minimal.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 3: Redesign The NPC Occurrence Durable Contract - -### Goal - -Replace the NPC-interaction value contract with the final registry-ID-backed -NPC occurrence contract while the selectable module temporarily remains -`dnd/npc-interactions`. - -### Required changes - -1. Replace `NPCInteractionListKind`, `NPCInteractionList`, - `NPCInteractionKind`, and `NPCInteraction` with `NPCOccurrenceListKind`, - `NPCOccurrenceList`, `NPCOccurrenceKind`, and `NPCOccurrence`. The envelope - field is `occurrences`; each record has required `npc_id`, `name`, `kind`, - and `source_refs`. -2. Retain the six existing category values and their precedence and splitting - semantics. Change only the contract vocabulary and registry identity link; - do not add relationship, sentiment, biography, or persistent-state fields. -3. In the current codec package, adopt `dnd/npc-occurrence-list`, durable schema - ID `notarius.dnd.npc_occurrences`, schema name - `notarius_dnd_npc_occurrences_v1`, and strict file - `dnd_npc_occurrences.v1.json`. -4. Change the current NPC-interaction private response contract to require - `npc_id` as well as `name`, `kind`, and `source_refs`. Use private schema ID - `notarius.dnd.npc_occurrences.llm`, schema name - `notarius_dnd_npc_occurrences_llm_v1`, and a renamed schema file even though - the prompt ID moves in Stage 4. -5. Project ordered `{id, name}` NPC registry entries into extraction. Require - the model to return an exact supplied pair. Extraction, normalization, and - registry validators must reject unknown IDs and mismatched ID/name pairs - rather than repairing by name similarity. -6. Update deterministic ordering, exact-duplicate identity, clone/evidence - helpers, merge functions, normalizers, validators, and diagnostics to include - `npc_id`. Preserve source chronology and distinct kinds/evidence. -7. Update enemy-event grounding to consume `NPCOccurrenceList`, filtering - `combat_opponent` occurrences while carrying only source-free grounding - values. Do not add NPC IDs to the enemy-event durable contract. -8. Update `docs/integrations/dnd-npc-interaction-artifacts.md` in place to the - new durable schema, root, and fields while accurately retaining the current - producer key until Stage 4. - -### Acceptance criteria - -- No production Go code refers to an `NPCInteraction*` artifact type. -- The durable artifact uses `occurrences` and validates exact NPC ID/name pairs. -- Unknown IDs and mismatches are detected independently at extraction, - normalization, and validation boundaries without mutation. -- Enemy-event grounding behavior remains source-free and category-filtered. - -### Validation - -```sh -go test ./internal/modules/dnd/codec/npcinteractions ./internal/modules/dnd/extract/npcinteractions ./internal/modules/dnd/normalize/npcinteractions ./internal/modules/dnd/validate/npcinteractions/... ./internal/modules/dnd/extract/enemyevents ./internal/modules/dnd/normalize/enemyevents -go test ./internal/modules/dnd/... ./internal/modules/integration/... -git diff --check -``` - -## Stage 4: Move NPC Occurrences Into Their Canonical Namespace - -### Goal - -Complete the NPC migration by replacing interaction module/package vocabulary -and updating the enemy-event dependency. - -### Required changes - -1. Move the codec, extract, normalize, validate, and D&D helper packages from - `npcinteractions` to `npcoccurrences` where they own the occurrence artifact. -2. Set extract and normalize keys to `dnd/npc-occurrences`; rename validator - keys and the provided capability to the `npc-occurrences`/ - `dnd.npc_occurrences` vocabulary. -3. Move `assets/dnd/npc-interactions` to `assets/dnd/npc-occurrences`. Change the - extraction prompt ID to `dnd.npc_occurrences`, update instructions from - “interactions” to “occurrences” without changing category semantics, and - keep the target private schema identity established in Stage 3. -4. Rename the generated reference and PromptKit grounding input consumed by - enemy events from `npc_interactions` to `npc_occurrences`. Update combat - grounding assets, reference specs, fingerprint metadata, checkpoint inputs, - and tests consistently. -5. Update central registration, default validator chains, artifact evidence and - merge registration, prompt-cache tests, configuration keys, examples, and - current documentation. -6. Rename the integration contract to - `docs/integrations/dnd-npc-occurrence-artifacts.md` and update every link from - NPC registry, enemy event, combat, internal D&D, configuration, and JSON - output documentation. -7. Delete every old package, key, schema, prompt, capability, asset, and current- - behavior reference for `dnd/npc-interactions` and `npc_interactions`. Do not - retain a module alias. - -### Acceptance criteria - -- `dnd/npc-occurrences` is the only selectable NPC occurrence key. -- Enemy events require `npc_occurrences` of kind - `dnd/npc-occurrence-list`. -- Current catalogs, examples, and documentation use occurrence vocabulary. -- No production package or asset directory named `npcinteractions` or - `npc-interactions` remains. - -### Validation - -```sh -go test ./internal/modules/dnd/... ./internal/modules/integration/... ./internal/cli/... -OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 5: Migrate The Location Registry Durable Contract And Identity - -### Goal - -Adopt the final location registry type, artifact kind, schema identity, and -identity-policy namespace while the selectable module remains `dnd/locations`. - -### Required changes - -1. Replace `LocationListKind` and `LocationList` with `LocationRegistryKind` and - `LocationRegistry`. Retain `Location` and the durable `locations` root. -2. Update all typed location-registry consumers, especially location-occurrence - extraction, normalization, validation, reference resolution, merge, evidence, - registration, and tests. Do not leave aliases. -3. In the current location codec package, use kind `dnd/location-registry`, - schema ID `notarius.dnd.location_registry`, schema name - `notarius_dnd_location_registry_v1`, and strict file - `dnd_location_registry.v1.json`. -4. Change the identity policy to `dnd.location_registry.identity.v1` and retain - the exact compact JSON input `[policy, comparison_name, source_id, - start_unit_id, end_unit_id]`. Continue to select the earliest canonical - evidence anchor without mutating caller data. Accept the resulting pre-release - ID invalidation. -5. Update identity, registry, codec, same-name-location, normalization, and - dependent occurrence fixtures. Tests must prove same-name distinct anchors, - deterministic ordering, invalid component rejection, and exact documented - bytes. -6. Update `docs/integrations/dnd-location-artifacts.md` in place for the target - kind, schema, and identity policy while retaining `dnd/locations` as the - current module key until Stage 6. - -### Acceptance criteria - -- No production Go code refers to `LocationList` or `LocationListKind`. -- Same-name locations with distinct evidence anchors remain distinguishable. -- The codec and generated-reference compatibility use - `dnd/location-registry`. -- No old kind, schema identity, or identity-policy compatibility path remains. - -### Validation - -```sh -go test ./internal/modules/dnd/locations/... ./internal/modules/dnd/codec/locations ./internal/modules/dnd/extract/locations ./internal/modules/dnd/normalize/locations ./internal/modules/dnd/extract/locationoccurrences ./internal/modules/dnd/normalize/locationoccurrences ./internal/modules/dnd/validate/locations/... ./internal/modules/dnd/validate/locationoccurrences/... -go test ./internal/modules/dnd/... ./internal/modules/integration/... -git diff --check -``` - -## Stage 6: Move And Tighten The Location Registry Module - -### Goal - -Move location registry code and assets into the canonical namespace and enforce -the roadmap's named-or-uniquely-designated eligibility policy. - -### Required changes - -1. Move the stage packages to `codec/locationregistry`, - `extract/locationregistry`, `normalize/locationregistry`, and - `validate/locationregistry/...`. Entity identity and immutable registry - helpers may remain under `internal/modules/dnd/locations`. -2. Set extract and normalize keys to `dnd/location-registry`; rename validator - keys and the provided capability to `dnd.location_registry`. -3. Move `assets/dnd/locations/{extract,normalize}` to - `assets/dnd/location-registry/{extract,normalize}`. Use prompt IDs - `dnd.location_registry` and `dnd.location_registry.normalize`; use private - schema ID `notarius.dnd.location_registry.llm`, schema name - `notarius_dnd_location_registry_llm_v1`, and filename - `dnd_location_registry_llm.v1.json`. -4. Change the location-registry extraction instructions to accept only a stable - proper name or unique in-world designation. Explicitly exclude generic, - temporary, relative, and descriptive phrases such as `the room`, `the bar`, - `the hallway`, `outside`, and `upstairs`. Do not use capitalization as the - eligibility test and do not add a brittle deterministic word blacklist. -5. Rename the required location-occurrence reference slot and PromptKit input - from `locations` to `location_registry`. Preserve the ordered source-free - `{id, name}` projection and exact ID/name validation at extraction and - normalization. -6. Update location-occurrence instructions so a generic current-chunk phrase - may resolve to an existing named registry location only when context supports - that coreference; it may not create a registry entity or substitute registry - provenance for evidence. -7. Update central registration, prompt composition, validator chains, - configuration, examples, and current docs. Rename the integration contract - to `docs/integrations/dnd-location-registry-artifacts.md` and update links. -8. Remove old module/package/asset/prompt/validator/capability identities and - old reference-slot names. The durable `locations` field and domain helper - package remain valid. - -### Acceptance criteria - -- `dnd/location-registry` is the only selectable location registry key. -- Registry prompts exclude generic locations, while occurrence prompts permit - supported coreference to an existing registry entity. -- Location occurrences require `location_registry` of kind - `dnd/location-registry` at extract and normalize stages. -- No production compatibility alias for `dnd/locations` remains. - -### Validation - -```sh -go test ./internal/modules/dnd/... ./internal/modules/integration/... ./internal/cli/... -OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 7: Add The Item Registry Domain Foundation - -### Goal - -Implement the item registry's typed durable contract, deterministic identity, -codec, and immutable reference projection without registering a selectable -pipeline module yet. - -### Required changes - -1. Add `ItemRegistryKind`, `ItemRegistry`, and `Item` to the D&D domain types. - The strict envelope is `{"items":[...]}`; each item has required `id`, - `name`, and `source_refs`. -2. Add an item identity package under `internal/modules/dnd/items/identity`. - Implement comparison normalization consistent with the NPC identity text - family, policy `dnd.item_registry.identity.v1`, compact JSON - `[policy, comparison_name]`, prefix `item:sha256:`, syntax checks, registry - uniqueness validation, and deterministic errors. -3. Add immutable item registry resolution under - `internal/modules/dnd/items/registry`. It must decode through the canonical - codec, validate IDs, preserve same-name/type policy, return defensive copies, - and produce an ordered source-free `{id, name}` projection suitable for a - later reference slot. -4. Add `internal/modules/dnd/codec/itemregistry` with strict durable schema - `dnd_item_registry.v1.json`, ID `notarius.dnd.item_registry`, name - `notarius_dnd_item_registry_v1`, version `v1`, and media type - `application/json`. -5. Test codec round trips, strict unknown-field rejection, candidate encoding, - metadata, identity bytes, empty/malformed identities, duplicate IDs, - defensive copies, projection order, and non-mutation. -6. Do not register the codec or describe the item registry as selectable in - current configuration or integration docs yet. The feature roadmap remains - the canonical description until Stage 9. - -### Acceptance criteria - -- The new type, identity, codec, and registry packages pass independently. -- Item IDs are name/type identities rather than per-instance identities. -- The projection exposes IDs and names but no source references. -- No generic framework package depends on D&D item types. - -### Validation - -Run `gofmt` on every Go file changed in this stage, then run: - -```sh -go test ./internal/modules/dnd/items/... ./internal/modules/dnd/codec/itemregistry +go test ./internal/modules/dnd/normalize/itemregistry ./internal/modules/dnd/items/identity ./internal/modules/dnd/validate/itemregistry/identity ./internal/modules/dnd/register go test ./internal/modules/dnd/... git diff --check ``` -## Stage 8: Implement Item Registry Extraction And Validation +## Stage 16: Make Currency Reconciliation Fail Closed ### Goal -Add the item-registry extraction module, private response contract, prompt -assets, canonical mapping, and extraction validators without production -registration. +Prevent an LLM reconciliation proposal from combining currency with a +non-currency item or combining distinct currency denominations. ### Required changes -1. Add `internal/modules/dnd/extract/itemregistry` with key - `dnd/item-registry`, artifact kind `dnd/item-registry`, LLM-backed execution, - the ordinary campaign reference slots, and prompt ID `dnd.item_registry`. -2. Add `assets/dnd/item-registry/extract` containing `prompt.yaml`, one coherent - `instructions.md`, and strict private schema `dnd_item_registry_llm.v1.json` - with ID `notarius.dnd.item_registry.llm` and name - `notarius_dnd_item_registry_llm_v1`. The private response contains required - item `name` and `source_refs`; deterministic code supplies source identity - and derives the durable item ID. -3. Encode the roadmap eligibility policy precisely: accept named unique items, - concrete reusable item types, stable unique designations, and separate - currency denominations; reject vague `loot`, `treasure`, `some gear`, - generic weapons, inferred properties, quantities, and uniqueness. -4. Reuse the shared extraction prompt prefix and evidence policy. Keep the item - registry instructions final and ephemeral. Do not add item occurrence, - holder, inventory, or ledger rules to this prompt. -5. Canonicalize display whitespace and evidence, derive IDs through the item - identity package, preserve candidate evidence for validation, and order - candidates deterministically. Do not semantically collapse aliases in the - extractor. -6. Add `validate/itemregistry/shape`, `source_refs`, and - `source_relatedness` packages and keys using the final module vocabulary. - Keep relatedness advisory according to existing D&D conventions. -7. Add focused behavior, schema, prompt preparation, metadata-redaction, - cancellation/error, and validator tests. Do not call a live model and do not - snapshot full prompt text. -8. Do not add the module to the production D&D registrar until its normalizer - and complete default chains exist in Stage 9. +1. Refactor the item-registry semantic-consolidation guard in + `internal/modules/dnd/normalize/itemregistry/reconciliation.go` to classify + every proposed member before applying a group. Preserve the existing five + supported D&D denominations and their singular, plural, and standard + abbreviation aliases. +2. Apply this exact decision table: + + | Proposed group contents | Result | + | --- | --- | + | no recognized currency members | allow the existing item reconciliation policy to decide/apply the already validated proposal | + | only recognized currency members of one denomination | allow consolidation | + | recognized currency members of different denominations | reject the proposal group | + | one or more recognized currency members plus any non-currency member | reject the proposal group | + + An unrecognized name must not be treated as the same denomination merely + because another member is recognized currency. +3. On rejection, preserve every deterministic input record exactly as the safe + fallback, keep its canonicalized evidence, count the proposal as rejected + for the existing retry/fallback behavior, and emit one bounded content-safe + warning for the group. Do not partially consolidate a rejected group. +4. Generalize the current warning text so it accurately covers both mixed + currency/non-currency groups and conflicting denominations without exposing + transcript content unnecessarily. +5. Add table-driven regression coverage for same-denomination aliases, + different denominations, currency plus an ordinary item, multiple ordinary + items, and a mixed group whose canonical member is the non-currency item. + Test observable normalized results and warnings rather than the private + helper's implementation. +6. Preserve proposal validation, retry counts, safe fallback, warning limits, + deterministic ordering, and non-mutation outside this additional safety + rule. ### Acceptance criteria -- Direct module construction prepares and maps a strict item-registry response. -- Currency denominations and reusable item types are eligible; vague categories - are excluded by prompt policy without a capitalization heuristic. -- Extracted records contain deterministic IDs and owned evidence slices. -- No current module catalog advertises an incomplete lane. +- `Gold Pieces` cannot be consolidated with `Longsword`, regardless of which + member the model selects as canonical. +- Currency aliases such as `GP`, `Gold Piece`, and `Gold Pieces` may still + resolve to one gold-denomination item when proposed safely. +- Different denominations always remain distinct. +- Invalid groups preserve the deterministic candidates and follow the current + retry/fallback contract without a partial merge. ### Validation ```sh -go test ./internal/modules/dnd/extract/itemregistry ./internal/modules/dnd/validate/itemregistry/... +go test ./internal/modules/dnd/normalize/itemregistry +go test ./internal/modules/dnd/validate/itemregistry/... ./internal/modules/dnd/register +git diff --check +``` + +## Stage 17: Enforce Item Occurrence Grounding And Own Shared Text Comparison + +### Goal + +Make item-occurrence extraction reject invalid registry pairs instead of +silently losing events, and remove its accidental semantic dependency on the +NPC identity package while preserving comparison behavior exactly. + +### Required changes + +1. Change response mapping in + `internal/modules/dnd/extract/itemoccurrences/canonicalize.go` so an unknown + `item_id` or a `name` that does not exactly match that ID's canonical + registry name returns a bounded, content-safe extraction error identifying + the occurrence index and violated field. Do not `continue`, omit the bad + record, repair it by name, or return a partial artifact. +2. Ensure the mapping error propagates through the existing extractor boundary + as a failed extraction attempt so the configured pipeline retry policy may + obtain a corrected structured response. Do not add a module-local retry + loop or convert this failure into a validator rejection after accepting + partial output. +3. Add extraction regression cases for an unknown ID, a mismatched name, and a + response containing a valid occurrence followed by an invalid one. All must + return an error and no accepted partial result. Mirror the established NPC + occurrence extractor behavior where practical without introducing a generic + framework abstraction. +4. Introduce one D&D-owned shared text comparison primitive under + `internal/modules/dnd/shared` implementing the current byte-for-byte + semantics: Unicode NFKC normalization, curly/modifier apostrophe mapping, + whitespace collapse, and Unicode case folding. Give the semantic policy a + stable explicit identity such as `dnd.text_comparison.v1`. +5. Remove the `shared` package's existing import of `npcs/identity` by making + shared token matching use the new primitive. Make NPC, location, and item + identity `ComparisonKey` functions delegate to the shared primitive while + retaining their public package functions and all existing entity-specific + ID policy constants and digest algorithms. +6. Change `internal/modules/dnd/itemoccurrences` to use the shared D&D + comparison primitive rather than `npcs/identity`. Do not change display + trimming, party-holder rules, exact-duplicate identity, or ordering. +7. Add the shared comparison policy identity to item-occurrence extractor and + normalizer manifest/checkpoint semantics wherever comparison behavior can + affect mapped or normalized output. Keep values content-safe. Document in + the nearest D&D internal owner that a semantic change to shared comparison + requires an explicit policy-version review for every affected identity, + mapping, normalization, and validator policy; do not calculate a brittle + fingerprint from private helper source. +8. Consolidate duplicated comparison test cases into focused shared-helper + behavior coverage, while retaining entity identity tests that protect each + durable ID contract. Prove existing representative NPC, location, item, and + item-occurrence outputs are unchanged by the ownership refactor. +9. Update `docs/integrations/dnd-item-occurrence-artifacts.md` if it currently + says extraction omits unknown or mismatched pairs. The canonical contract + must say the response is rejected as invalid model output and may be retried; + normalization and validation remain defense-in-depth for artifacts entering + through other boundaries. + +### Acceptance criteria + +- Item-occurrence extraction never succeeds with a silently shortened artifact + after receiving an invalid registry pair. +- The framework's existing retry behavior receives the mapping error without a + new hidden retry mechanism. +- No item-occurrence or shared D&D package imports NPC identity solely for text + comparison. +- All existing comparison keys and durable entity IDs remain unchanged for the + same inputs. +- Item-occurrence checkpoint reuse is invalidated when its declared comparison + policy identity changes. + +### Validation + +```sh +go test ./internal/modules/dnd/shared ./internal/modules/dnd/npcs/identity ./internal/modules/dnd/locations/identity ./internal/modules/dnd/items/identity +go test ./internal/modules/dnd/itemoccurrences ./internal/modules/dnd/extract/itemoccurrences ./internal/modules/dnd/normalize/itemoccurrences ./internal/modules/dnd/validate/itemoccurrences/... go test ./internal/modules/dnd/... git diff --check ``` -## Stage 9: Implement Item Registry Normalization And Production Registration +## Stage 18: Align NPC Mention Eligibility And Finish Vocabulary Cleanup ### Goal -Complete and register the item registry as the third production registry -family. +Allow the NPC registry to establish named NPCs that appear only in supported +third-party mentions, and remove the remaining occurrence/interactions naming +residue without broadening the registry to speculative or generic identities. ### Required changes -1. Add `internal/modules/dnd/normalize/itemregistry` with key - `dnd/item-registry`, LLM-backed execution, prompt ID - `dnd.item_registry.normalize`, and the same retry/fallback contract used by - NPC and location registry normalization. -2. Deterministically preprocess candidate names, evidence, IDs, and exact - duplicates. Then use the existing shared D&D entity-reconciliation proposal - mechanics with item-specific candidates and selected transcript windows. - Validated groups may choose one supplied canonical display name and union - evidence; unsafe, overlapping, unknown, or uncertain groups preserve the - deterministic result with bounded diagnostics. -3. Add `assets/dnd/item-registry/normalize` with `prompt.yaml`, - `instructions.md`, and `candidates.md`. Item instructions own only same-item- - kind/designation judgment and canonical display preference. The shared - reconciliation fragment alone owns opaque keys and safe group mechanics. -4. Explicitly prevent reconciliation across currency denominations, materially - different item types, or merely nearby objects. Do not create per-instance - identities or infer properties. -5. Add `validate/itemregistry/identity` and its default normalize-chain position. - Test retry, invalid structured output, operational error, warning bounds, - idempotence, order, non-mutation, and content-safe diagnostics. -6. Register the item registry codec, evidence projection, append-order merger, - extractor, normalizer, validators, default chains, prompt assets, and module - metadata in the D&D registrar. Add it to the centralized extraction prompt - composition test. -7. Create the current durable contract at - `docs/integrations/dnd-item-registry-artifacts.md`. Update configuration and - internal module catalogs enough to describe the now-selectable registry, - but defer the complete multi-step example until item occurrences consume it. +1. Revise `assets/dnd/npc-registry/extract/prompts/instructions.md` so a proper + name or stable unique NPC designation is eligible when the transcript + establishes it through a factual third-party mention, even if the NPC is not + physically present and does not speak or enter combat in that chunk. +2. Keep the exclusion boundary explicit: omit hypothetical or speculative + people, names used only in an imagined example, generic roles, anonymous + groups, player characters, and labels invented by the model. Do not require + dialogue, physical presence, or direct action as evidence of identity. +3. Preserve separation of concerns. The registry prompt must extract only NPC + identity and identity evidence; it must not classify an occurrence as + `mentioned` or copy occurrence semantics into the registry artifact. The + later NPC occurrence lane remains responsible for emitting the `mentioned` + fact with independently cited current-transcript evidence. +4. Review the shared D&D identity/evidence instructions and remove or revise + any contradictory wording using the canonical asset owner. Do not duplicate + the new rule across module-specific and shared assets merely for emphasis, + and do not disturb shared prompt ordering or cacheable prefix bytes unless a + genuinely shared contradiction must be corrected. +5. Update the canonical NPC registry and NPC occurrence integration documents, + and `docs/internal/dnd.md` only where needed, so they consistently distinguish + a transcript-established mentioned-only identity from an occurrence. Link + between canonical owners instead of copying category lists or schemas. +6. Correct the duplicated phrase `NPC occurrence occurrences` in the NPC + occurrence prompt. Rename stale local aliases such as `interactionmodel` in + NPC occurrence code and stale `interactionContent` test variables to + occurrence vocabulary. Correct similarly scoped registration labels that + inaccurately say `locations` when they describe another entity. Do not make + unrelated wording changes outside the entity modules. +7. Run existing prompt preparation and module behavior tests. Add a focused + deterministic regression test only where there is a stable behavioral seam; + do not snapshot the prose, assert an exact prompt fragment, or add a prompt + change detector. Use optional human prompt evaluation to assess model + interpretation, not as an automated acceptance gate. ### Acceptance criteria -- `dnd/item-registry` is selectable for extraction and normalization with - complete production validator chains. -- The normalizer uses proposal-only LLM semantics and deterministic application. -- Codec, merge, evidence, prompt, metadata, and registry registrations are all - type-compatible. -- The current docs accurately describe the implemented item registry without - claiming that item occurrences consume it yet. +- The NPC registry prompt permits a factual named third-party mention to + establish an NPC while continuing to reject hypothetical and generic names. +- The registry output remains the minimal `{id, name, source_refs}` contract; + mention classification remains in NPC occurrences. +- Current integration and internal documentation describe the same boundary + without duplicating volatile contract detail. +- NPC occurrence production code, prompt prose, and touched tests contain no + stale interaction-era local vocabulary. +- Prompt manifests still prepare successfully with the established shared + prefix and final module-instruction ordering. ### Validation ```sh -go test ./internal/modules/dnd/items/... ./internal/modules/dnd/codec/itemregistry ./internal/modules/dnd/extract/itemregistry ./internal/modules/dnd/normalize/itemregistry ./internal/modules/dnd/validate/itemregistry/... ./internal/modules/dnd/register -go test ./internal/modules/dnd/... ./internal/cli/... +go test ./internal/modules/dnd/extract/npcregistry ./internal/modules/dnd/extract/npcoccurrences ./internal/modules/dnd/normalize/npcoccurrences ./internal/modules/dnd/validate/npcoccurrences/... ./internal/modules/dnd/register +go test ./internal/modules/dnd/... ./internal/modules/integration/... git diff --check ``` -## Stage 10: Redesign Item Events As Registry-Backed Item Occurrences +## Final Verification -### Goal - -Adopt the final item occurrence durable contract and required item-registry -grounding while the selectable module temporarily remains `dnd/item-events`. - -### Required changes - -1. Replace `ItemEventListKind`, `ItemEventList`, `ItemEventKind`, and `ItemEvent` - with `ItemOccurrenceListKind`, `ItemOccurrenceList`, `ItemOccurrenceKind`, - and `ItemOccurrence`. Use durable root `occurrences`; each record has required - `item_id`, `name`, `kind`, and `source_refs`, plus conditional `quantity`, - `from`, and `to`. -2. Preserve the five existing kinds and all holder, quantity, currency, - ordering, and exact-duplicate semantics. Rename the D&D helper behavior to - occurrence vocabulary in code, but defer physical package moves to Stage 11. -3. In the current codec package, adopt kind `dnd/item-occurrence-list`, schema - ID `notarius.dnd.item_occurrences`, schema name - `notarius_dnd_item_occurrences_v1`, and strict file - `dnd_item_occurrences.v1.json`. -4. Change the private response schema to - `notarius.dnd.item_occurrences.llm`/ - `notarius_dnd_item_occurrences_llm_v1` in - `dnd_item_occurrences_llm.v1.json`. Require every property. Represent absent - `quantity`, `from`, and `to` as nullable private fields and map them to the - durable conditional fields without fabricating values. -5. Add required `item_registry` reference slots to both extraction and - normalization, accepting only `dnd/item-registry`, `application/json`, and an - explicit size bound. Resolve one immutable registry per operation and render - ordered source-free `{id, name}` entries after the shared evidence message. -6. Require the model to return an exact registry ID/name pair. Extraction must - reject or omit unknown/mismatched pairs without creating an item. The - deterministic normalizer must canonicalize recognized names by ID and retain - unknown values for the registry validator to reject according to the current - validation architecture. -7. Add a registry membership validator to the item occurrence extract and - normalize chains. Update shape, invariant, source-reference, relatedness, - merge, evidence, clone, metadata, warning, and ordering behavior for - `item_id` and the new envelope. -8. Update the maintained complete example immediately: produce - `dnd/item-registry` in the earlier registry step and bind its generated - artifact as `item_registry` to the still-current `dnd/item-events` extract - and normalize stages. Keep the example valid at the end of this stage. -9. Update `docs/integrations/dnd-item-event-artifacts.md` in place to the target - durable shape and required registry behavior while retaining the current - producer key until Stage 11. - -### Acceptance criteria - -- No production Go code uses an `ItemEvent*` artifact type. -- Every item occurrence carries an exact item-registry ID/name pair. -- Extraction and normalization both declare the identical required registry - slot; validation detects unknown and mismatched identities. -- The complete example resolves with an earlier item registry and no same-step - dependency. - -### Validation - -```sh -go test ./internal/modules/dnd/itemevents ./internal/modules/dnd/codec/itemevents ./internal/modules/dnd/extract/itemevents ./internal/modules/dnd/normalize/itemevents ./internal/modules/dnd/validate/itemevents/... ./internal/modules/dnd/register ./internal/modules/integration/... -OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 11: Move Item Occurrences Into Their Canonical Namespace - -### Goal - -Complete the item migration by replacing item-event module, package, prompt, -validator, asset, and documentation vocabulary. - -### Required changes - -1. Move `internal/modules/dnd/itemevents` to `itemoccurrences` and move codec, - extract, normalize, and validate packages to the corresponding - `itemoccurrences` paths. -2. Set extract and normalize keys to `dnd/item-occurrences`; rename validator - keys and the provided capability to `dnd.item_occurrences`. -3. Move `assets/dnd/item-events` to `assets/dnd/item-occurrences`. Use prompt ID - `dnd.item_occurrences`, the target private schema identity from Stage 10, and - occurrence terminology throughout module instructions. -4. Update all imports, central registrations, default chains, metadata, - fingerprints, prompt composition cases, configuration keys, example lanes, - generated references, output expectations, and tests. -5. Rename the integration contract to - `docs/integrations/dnd-item-occurrence-artifacts.md` and update links from - item registry, configuration, internal D&D, and JSON output documentation. -6. Delete every old `itemevents`, `item-events`, `dnd/item-events`, - `dnd.item_events`, `notarius.dnd.item_events`, `events`-envelope fixture, and - old schema/asset path that represented the retired artifact. Do not remove - unrelated uses of the ordinary word “event” in other D&D lanes. - -### Acceptance criteria - -- `dnd/item-occurrences` is the only selectable item occurrence key. -- All item occurrence packages, prompts, validators, schemas, and docs use the - canonical vocabulary. -- The complete example binds `item_registry` into both stages of the renamed - lane. -- No compatibility alias for item events remains. - -### Validation - -```sh -go test ./internal/modules/dnd/... ./internal/modules/integration/... ./internal/cli/... -OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 12: Verify The Complete Ordered Entity Pipeline - -### Goal - -Make the maintained complete configuration and representative integration tests -prove the final three-registry/three-occurrence architecture. - -### Required changes - -1. Finalize `examples/dnd-complete.config.yml` with an earlier registry step - containing `npc-registry`, `location-registry`, and `item-registry` lanes - alongside scene descriptions. The next step contains NPC, location, and item - occurrences plus spell and combat lanes. The derived enemy step consumes - `npc_registry`, `npc_occurrences`, combat turns, and scene descriptions. -2. Use generated-reference aliases `npc_registry`, `location_registry`, - `item_registry`, and `npc_occurrences`. Preserve explicit step-and-lane - selectors and stage-local spell-catalog bindings. Do not infer dependencies - from matching names. -3. Update scene-plan eligibility configuration for all renamed lane labels while - preserving the shared chunk plan and exact combat-scene routing semantics. -4. Add or revise one representative integration workflow that executes the - ordered handoffs with deterministic fake LLM responses and proves exact - artifact-kind compatibility, registry projection, independent occurrence - evidence, and final publication for all three entity families. -5. Test compatible external path references for each registry through existing - reference preparation. Do not duplicate the framework's exhaustive topology - and failure-propagation cases; add only D&D-specific compatibility coverage. -6. Test rejection of a wrong registry artifact kind and invalid same-step or - forward binding at the existing resolver/integration boundary if those risks - are not already credibly covered with the new kinds. -7. Update CLI maintained-example contract tests and production catalog tests so - both D&D examples load, validate, resolve, and list only implemented module - and validator identities. - -### Acceptance criteria - -- The complete example validates offline and resolves all generated handoffs. -- A representative assembled run proves all three registry-to-occurrence paths. -- External compatible registry artifacts remain supported. -- Cross-family integration tests protect compatibility without repeating - package-level identity and validator cases. - -### Validation - -```sh -go test ./internal/modules/integration/... ./internal/cli/... ./internal/modules/dnd/register -go run ./cmd/notarius config validate --config examples/dnd-minimal.config.yml --pipeline dnd-session -OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 13: Consolidate Current Documentation And Examples - -### Goal - -Bring every canonical current-behavior document into alignment with the final -implemented entity family without duplicating contracts across documents. - -### Required changes - -1. Update `docs/config.md` as the canonical owner of selectable module keys, - validator keys, default chains, reference slots, requiredness, size limits, - execution classes, profile behavior, and ordered binding examples. -2. Update `docs/internal/dnd.md` for ten final D&D artifact lanes, the three - registry normalizers, source-free ID/name projections, occurrence grounding, - package/asset ownership, and lane-specific differences. Link to integration - contracts instead of repeating their schemas or categories. -3. Update `docs/internal/modules.md`, `docs/internal/overview.md`, README, CLI, - operations, JSON output, and other current documents only where they actually - name affected modules, artifacts, examples, or links. Preserve each - document's canonical scope under the documentation policy. -4. Ensure the six target integration contracts are the only canonical durable - entity contracts: - `dnd-npc-registry-artifacts.md`, `dnd-npc-occurrence-artifacts.md`, - `dnd-location-registry-artifacts.md`, - `dnd-location-occurrence-artifacts.md`, - `dnd-item-registry-artifacts.md`, and - `dnd-item-occurrence-artifacts.md`. -5. Each integration contract must define its exact kind, schema ID/name/version, - media type, strict wire shape, identity or occurrence policy, evidence - boundary, normalization behavior, consumers, and compatibility statement. - Cross-link rather than copying common reference or output semantics. -6. Verify that only `examples/dnd-minimal.config.yml` and - `examples/dnd-complete.config.yml` remain as maintained D&D configurations, - that both are secret-free, and that documentation links to rather than - reproduces them. -7. Keep future or historical discussion in `docs/roadmap/` and ADRs. Do not - rewrite accepted ADRs as current reference documentation, add a changelog, - or delete the active roadmap files. - -### Acceptance criteria - -- Current docs contain only implemented final names and contracts. -- Each volatile fact has one canonical owner and all links resolve. -- Maintained examples are valid, copyable, and tested. -- No documentation claims compatibility with the retired pre-release names. - -### Validation - -```sh -go test ./internal/cli/... ./internal/modules/integration/... -go run ./cmd/notarius config validate --config examples/dnd-minimal.config.yml --pipeline dnd-session -OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session -git diff --check -``` - -## Stage 14: Final Contract Audit And Repository Verification - -### Goal - -Perform a final gap audit, remove stale implementation remnants, and prove the -completed feature at repository scope. - -### Required changes - -1. Search production code, assets, tests, examples, and current documentation - for retired exact identities, including: - `dnd/npcs`, `dnd/npc-interactions`, `dnd/locations`, `dnd/item-events`, - `dnd/npc-list`, `dnd/npc-interaction-list`, `dnd/location-list`, - `dnd/item-event-list`, old schema IDs/names, old prompt IDs, old capability - names, `common-dnd-npcs.md`, and old reference-slot names. Exclude - `docs/roadmap/`, where migration context intentionally names the old values. -2. Search for retired Go artifact identifiers `NPCList`, `NPCInteraction`, - `LocationList`, and `ItemEvent`. Distinguish legitimate words or unrelated - concepts from compatibility remnants; remove every actual old contract or - alias. -3. Verify the final package and asset trees contain only canonical registry and - occurrence stage packages. Confirm every D&D prompt subtree has - `prompt.yaml` and `instructions.md`, manifests select only existing assets, - and all prompt/schema fingerprints are content-safe. -4. Verify every private D&D response schema has `additionalProperties: false`, - includes every declared property in `required`, and uses the exact final - private identity. Verify durable schemas independently through their codecs; - do not conflate private and durable contracts. -5. Review default validator chains for all six entity modules. Each registry - must have shape, identity, source-reference, schema, and relatedness coverage; - each occurrence must additionally enforce exact registry membership and - domain invariants at the appropriate stages. -6. Review tests against `docs/policy/testing.md`. Consolidate redundant - cross-layer assertions, retain regression coverage for identities and - handoffs, and do not add asset inventories or exact prefix-length detectors. -7. Run repository-wide tests, vet, build, example validation, and whitespace - checks. Fix any defect discovered; do not merely document it as follow-up if - it is within this roadmap's scope. -8. Report any optional live-model observations separately. Live model quality - is useful human evaluation but is not a credentialed automated acceptance - gate and must not block deterministic completion. - -### Acceptance criteria - -- The target end state in `entity-registries.md` is fully implemented. -- No production compatibility alias or stale current-behavior name remains. -- All six entity modules register with exact typed contracts and default chains. -- Both maintained configurations validate offline. -- The full repository test, vet, and build checks pass with a clean diff check. - -### Validation +After Stage 18, run the repository-wide checks and validate both maintained D&D +configurations: ```sh go test -count=1 ./... @@ -862,3 +370,15 @@ go run ./cmd/notarius config validate --config examples/dnd-minimal.config.yml - OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session git diff --check ``` + +The implementation is complete only when all commands pass and a final review +confirms that the feature roadmap's durable schemas, module names, evidence +boundaries, and ordered registry handoffs remain unchanged except for the +explicitly corrected failure and eligibility behavior above. + +## Open Questions + +None. The stages above adopt the long-term-maintainable choices from the audit: +deterministic identity consolidation, fail-closed semantic safeguards, strict +extractor grounding, a D&D-owned shared comparison policy, and support for +factual mentioned-only NPC identities. diff --git a/internal/modules/dnd/normalize/itemregistry/normalizer.go b/internal/modules/dnd/normalize/itemregistry/normalizer.go index 1594546..a968921 100644 --- a/internal/modules/dnd/normalize/itemregistry/normalizer.go +++ b/internal/modules/dnd/normalize/itemregistry/normalizer.go @@ -212,13 +212,16 @@ func preprocessRecords(input dnd.ItemRegistry, order shared.SourceRefOrder) ([]n warnings = append(warnings, contracts.Warning{Scope: itemScope(index), ReasonCode: ReasonCodeItemIDRecomputed, Message: fmt.Sprintf("input index %d: item ID recomputed from %s", index, diagnostics.Quote(item.Name))}) } } - groups := exactDuplicateGroups(records) + groups := comparisonNameGroups(records) output := make([]normalizedRecord, 0, len(groups)) for _, members := range groups { retained := cloneRecord(records[members[0]]) for _, member := range members[1:] { + retained.item.SourceRefs = append(retained.item.SourceRefs, records[member].item.SourceRefs...) retained.inputIndexes = append(retained.inputIndexes, records[member].inputIndexes...) } + retained.item.SourceRefs = order.Canonicalize(retained.item.SourceRefs) + retained.item.ID = identity.DeriveID(retained.item.Name) retained.inputIndexes = sortedUniqueIndexes(retained.inputIndexes) output = append(output, retained) if len(members) > 1 { @@ -236,14 +239,14 @@ func normalizeRecord(input dnd.Item, order shared.SourceRefOrder) (dnd.Item, boo return output, input.Name != output.Name, !reflect.DeepEqual(input.SourceRefs, output.SourceRefs) } -func exactDuplicateGroups(records []normalizedRecord) [][]int { +func comparisonNameGroups(records []normalizedRecord) [][]int { groups := make([][]int, 0, len(records)) for index, record := range records { key := identity.ComparisonKey(record.item.Name) found := false for groupIndex, members := range groups { first := records[members[0]] - if identity.ComparisonKey(first.item.Name) == key && reflect.DeepEqual(first.item.SourceRefs, record.item.SourceRefs) { + if identity.ComparisonKey(first.item.Name) == key { groups[groupIndex] = append(groups[groupIndex], index) found = true break diff --git a/internal/modules/dnd/normalize/itemregistry/normalizer_test.go b/internal/modules/dnd/normalize/itemregistry/normalizer_test.go index 0c5caaa..bd82882 100644 --- a/internal/modules/dnd/normalize/itemregistry/normalizer_test.go +++ b/internal/modules/dnd/normalize/itemregistry/normalizer_test.go @@ -17,6 +17,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/entityreconcile" + identityvalidator "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/identity" "gitea.maximumdirect.net/eric/promptkit" ) @@ -38,19 +39,48 @@ func TestModuleContractAndMetadata(t *testing.T) { } } -func TestNormalizePreprocessesExactDuplicatesWithoutMutation(t *testing.T) { - input := dnd.ItemRegistry{Items: []dnd.Item{ - {Name: " Rope ", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, - {Name: "rope", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, - {Name: "Rope", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}, +func TestNormalizeConsolidatesEqualNamesAcrossEvidenceWithoutMutation(t *testing.T) { + doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{ + {ID: 1, Text: "The rope is secured."}, + {ID: 2, Text: "The party takes the rope."}, + {ID: 3, Text: "The rope is packed away."}, + {ID: 4, Text: "A lantern lights the path."}, }} - before := dnd.ItemRegistry{Items: append([]dnd.Item(nil), input.Items...)} - result, err := newNormalizer(t, &recordingNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input)) - if err != nil || len(result.Value.Items) != 2 || !reflect.DeepEqual(input, before) { - t.Fatalf("Normalize() = %#v, %v; want non-mutating exact deduplication", result, err) + input := dnd.ItemRegistry{Items: []dnd.Item{ + {Name: " Rope ", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 3, EndUnitID: 3}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, + {Name: "rope", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}, {SourceID: "session", StartUnitID: 3, EndUnitID: 3}}}, + {Name: "Lantern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 4, EndUnitID: 4}}}, + }} + before := dnd.ItemRegistry{Items: []dnd.Item{ + {Name: " Rope ", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 3, EndUnitID: 3}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, + {Name: "rope", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}, {SourceID: "session", StartUnitID: 3, EndUnitID: 3}}}, + {Name: "Lantern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 4, EndUnitID: 4}}}, + }} + client := &recordingNormalizerClient{} + result, err := newNormalizer(t, client).Normalize(context.Background(), normalizeRequestWithSource(input, doc)) + if err != nil || len(result.Value.Items) != 2 || !reflect.DeepEqual(input, before) || len(client.requests) != 1 { + t.Fatalf("Normalize() = %#v, %v; want deterministic non-mutating consolidation", result, err) } - if result.Value.Items[0].ID != result.Value.Items[1].ID || !hasWarning(result.Warnings, ReasonCodeDuplicateItemCollapsed) { - t.Fatalf("items = %#v, warnings = %#v; want type identity and duplicate warning", result.Value.Items, result.Warnings) + rope := result.Value.Items[0] + wantRefs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 2, EndUnitID: 2}, {SourceID: "session", StartUnitID: 3, EndUnitID: 3}} + if rope.Name != "Rope" || rope.ID != identity.DeriveID("Rope") || !reflect.DeepEqual(rope.SourceRefs, wantRefs) || result.Value.Items[1].Name != "Lantern" || !hasWarning(result.Warnings, ReasonCodeDuplicateItemCollapsed) { + t.Fatalf("items = %#v, warnings = %#v; want earliest display name, canonical evidence union, and stable placement", result.Value.Items, result.Warnings) + } + validation, validationErr := identityvalidator.New(identityvalidator.Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemRegistry]{Value: result.Value}) + if validationErr != nil || !validation.Approved { + t.Fatalf("default item-registry identity validation = %#v, %v; want normalized cross-chunk candidates accepted", validation, validationErr) + } + var candidates struct { + Candidates []struct { + Name string `json:"name"` + } `json:"candidates"` + } + if err := json.Unmarshal(client.requests[0].Inputs["candidates"].Content, &candidates); err != nil || len(candidates.Candidates) != 2 || candidates.Candidates[0].Name != "Rope" || candidates.Candidates[1].Name != "Lantern" { + t.Fatalf("semantic candidates = %#v, %v; want one candidate per comparison name", candidates, err) + } + repeated, repeatErr := newNormalizer(t, &recordingNormalizerClient{}).Normalize(context.Background(), normalizeRequestWithSource(result.Value, doc)) + if repeatErr != nil || !reflect.DeepEqual(repeated.Value, result.Value) { + t.Fatalf("repeated normalization = %#v, %v; want stable output", repeated, repeatErr) } }