From c6f330eb065af1e9be91532c0b60529570e9c685 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 20 Jul 2026 23:10:25 -0500 Subject: [PATCH] Implement NPC extraction follow-up fixes --- docs/roadmap/dnd-npc-extraction.md | 209 ------- docs/roadmap/implementation.md | 528 ------------------ .../modules/dnd/extract/npcs/canonicalize.go | 15 +- .../dnd/extract/npcs/extractor_test.go | 32 ++ .../dnd/extract/spells/npc_registry.go | 15 +- .../dnd/extract/spells/npc_registry_test.go | 51 +- .../modules/dnd/normalize/npcs/normalizer.go | 2 +- .../npcs/diagnostics/diagnostics.go | 2 +- .../dnd/npcs/diagnostics/diagnostics_test.go | 28 + .../dnd/validate/npcs/identity/validator.go | 2 +- .../dnd/validate/npcs/shape/validator.go | 2 +- .../validate/npcs/source_refs/validator.go | 2 +- .../npcs/source_relatedness/validator.go | 2 +- 13 files changed, 133 insertions(+), 757 deletions(-) delete mode 100644 docs/roadmap/dnd-npc-extraction.md delete mode 100644 docs/roadmap/implementation.md rename internal/modules/dnd/{validate => }/npcs/diagnostics/diagnostics.go (97%) create mode 100644 internal/modules/dnd/npcs/diagnostics/diagnostics_test.go diff --git a/docs/roadmap/dnd-npc-extraction.md b/docs/roadmap/dnd-npc-extraction.md deleted file mode 100644 index 189825a..0000000 --- a/docs/roadmap/dnd-npc-extraction.md +++ /dev/null @@ -1,209 +0,0 @@ -# D&D NPC Extraction And Registry - -Status: Complete. - -## Purpose - -Add a production D&D NPC pipeline that turns a session transcript into a -canonical, evidence-backed NPC registry. The registry should be useful on its -own and should serve as reference material for later sequential spell and -combat-turn pipelines. - -This is the next recommended increment because participant identity is already -a demonstrated source of extraction error, while combat-turn extraction will -depend on consistent identities across many more events. Establishing the NPC -artifact first gives later pipelines a durable vocabulary without introducing -a DAG or cross-lane orchestration. - -## Target Outcome - -An operator can: - -1. run an NPC pipeline against a D&D transcript; -2. receive a validated, normalized JSON NPC registry; -3. provide that registry as an explicit reference to a later spell pipeline; - and -4. receive spell artifacts that prefer the registry's canonical NPC identities - when resolving casters, targets, and other named participants. - -The pipelines remain independent CLI invocations over the same input. Notarius -does not automatically schedule them, discover prior output, or reconcile -their results concurrently. - -## NPC Artifact Contract - -The new typed artifact kind represents a list of NPC records. Each record has: - -- a stable, deterministic ID derived by Notarius from the normalized NPC - identity rather than authored freely by the model; -- one canonical in-world name or, for an unnamed but individually - distinguishable NPC, one stable descriptive label; -- zero or more observed aliases; -- a concise transcript-grounded description; -- zero or more explicitly supported relationships to PCs, NPCs, groups, or - locations; and -- one or more source references that collectively support the record's - identity and every reported description, alias, and relationship. - -The external contract defines the exact JSON shape, ID syntax, ordering, -normalization, and compatibility policy. IDs are stable for equivalent -normalized identities under the same policy; they do not promise permanence -when human review or new evidence changes the canonical identity. - -Descriptions report only facts established by the session transcript. General -D&D knowledge, campaign reference material, and model inference do not supply -biography, statistics, alignment, motivations, relationships, or outcomes. -Reference material may disambiguate an identity but is not source evidence. - -## Inclusion Policy - -Include an in-world non-PC participant when the transcript establishes that it -appears, acts, speaks, or is materially discussed and either: - -- gives it a proper name; -- gives it a stable alias or title; or -- distinguishes it individually in a way useful to later extraction, such as - a titled attendant or a distinct item-bearing guard. - -Exclude: - -- human players, transcript speakers, and the GM as out-of-world people; -- player characters identified by the party or player references; -- incidental name drops, hypothetical examples, corrected transcription - mistakes, and characters mentioned only by reference material; -- interchangeable unnamed crowds or groups whose members cannot be - distinguished; and -- temporary summoned creatures or spell effects unless the transcript gives - one an individual, persistent identity relevant beyond the summoning event. - -An NPC may be friendly, hostile, neutral, allied with the party, controlled by -a player temporarily, or absent from the current combat. Classification rests -on whether the entity is an in-world non-PC participant, not on disposition or -who rolls its dice. - -## Extraction Semantics - -The extractor uses the existing shared D&D transcript, player, party, and -glossary prompt inputs. It returns canonical in-world identities rather than -speaker names and cites only transcript units. - -Each record is intentionally concise. It captures identity and stable facts -useful for recognizing the NPC elsewhere; it does not summarize every action, -follow the NPC through the entire session, or duplicate combat-turn and -narrative artifacts. Relationships must be stated or directly demonstrated by -the cited transcript rather than inferred from D&D lore. - -The model may propose canonical names, aliases, descriptions, relationships, -and evidence. Deterministic code owns artifact IDs, structural integrity, -ordering, safe consolidation, and validation of all source ranges. - -## Validation And Normalization - -The production default validator chain remains deterministic. It must reject: - -- malformed or empty required fields; -- missing, invalid, or foreign source references; -- duplicate or colliding IDs; -- an alias that resolves to more than one retained NPC; -- unsupported structural combinations defined by the artifact contract. - -Relatedness checks may emit warnings when a proposed canonical name or alias is -absent from its cited source text. A name legitimately disambiguated through -opaque player, party, or glossary reference material may still produce that -warning; the warning does not turn the reference into event evidence. Exclusion -of PCs is prompt policy and a human-evaluation dimension until a future -structured roster contract makes deterministic membership checks reliable. - -Normalization is deterministic and conservative. It: - -- normalizes whitespace, case-insensitive comparison keys, apostrophe variants, - and exact source-reference order; -- consolidates records whose normalized canonical names match or whose - canonical-name and alias sets establish one unambiguous identity; -- unions unique aliases, relationships, and source references in stable order; -- retains one supported description without synthesizing new prose; and -- emits scoped warnings for every canonicalization or collapsed record. - -Ambiguous identities remain separate or cause a bounded validation rejection; -the normalizer does not guess. Broader semantic reconciliation belongs to the -future generic LLM-assisted deduplication work after real NPC output shows that -deterministic identity overlap is insufficient. - -## Sequential Reference Use - -The normalized NPC registry is accepted as an optional structured JSON -reference by the D&D spell extractor. The spell prompt uses it to select exact -canonical NPC names and aliases, just as party and player references ground PC -identities. The registry does not establish that an NPC cast a spell and never -becomes spell source evidence. - -Reference binding stays explicit in configuration or through the existing CLI -reference override. The maintained examples and operator documentation should -demonstrate separate NPC and spell invocations over the same transcript, with -the first output bound to the second invocation. - -The reference contract should be designed for reuse by the future combat-turn -extractor without adding combat-specific fields to the NPC artifact. - -## Provenance, Checkpoints, And Diagnostics - -The NPC extractor and normalizer report their prompt, private schema, and -normalization-policy identities through the existing manifest and prepared -checkpoint-fingerprint contracts as applicable. Changes to any semantic input -that can change NPC identity or consolidation intentionally produce cold -checkpoint misses. - -Raw transcript, reference, prompt, schema, and NPC content remain outside -manifest metadata and checkpoint fingerprints. Existing debug and rejected- -attempt behavior applies without a domain-specific filesystem surface. - -## Evaluation - -Evaluation is human-reviewed and model-aware rather than a deterministic golden -output gate. The initial corpus should include the existing D&D sessions that -exposed player-versus-character attribution and repeated NPC appearances. - -Review should score separately: - -- NPC detection precision and recall; -- exclusion of PCs and out-of-world people; -- canonical identity and alias quality; -- duplicate consolidation; -- description and relationship fidelity; -- source-reference completeness; and -- usefulness as grounding for a subsequent spell extraction run. - -Small-model development runs may expose more semantic errors than production -frontier models. Deterministic contracts and validators should protect -structure and provenance without attempting to turn subjective extraction -quality into brittle fixture equality. - -## Out Of Scope - -- Combat turns, initiative, actions, damage, conditions, or encounter state. -- General narrative or scene summaries. -- PC extraction or replacement of the existing party and player references. -- Automatic multi-pipeline scheduling, prior-output discovery, or DAG - execution. -- An LLM-backed NPC validator or generic semantic deduplication normalizer. -- Campaign-wide identity persistence, a database, cross-session entity merges, - or manual identity-editing UI. -- Retrieval, embeddings, reference summarization, or token-budget management. - -## Acceptance Criteria - -- A configured D&D NPC lane produces a typed, durable JSON registry with the - identity, evidence, and inclusion semantics above. -- Production shape, source-reference, identity-collision, and relatedness - validators are registered with a deterministic default chain. -- The NPC normalizer assigns stable IDs and safely consolidates only - unambiguous identities while preserving provenance and warnings. -- The registry can be bound directly to the spell extractor as an optional NPC - reference, and spell extraction prefers its canonical NPC names without - treating it as source evidence. -- Prompt, schema, reference, and normalization-policy changes participate in - checkpoint identity where they can affect output. -- External, configuration, internal, operational, and maintained-example - documentation is updated in its canonical location when the feature lands. -- Human review on representative transcripts demonstrates useful NPC grounding - for a subsequent spell run without requiring exact model-output fixtures. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md deleted file mode 100644 index 4ce09e0..0000000 --- a/docs/roadmap/implementation.md +++ /dev/null @@ -1,528 +0,0 @@ -# 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. diff --git a/internal/modules/dnd/extract/npcs/canonicalize.go b/internal/modules/dnd/extract/npcs/canonicalize.go index 8263bc5..4c5bd3a 100644 --- a/internal/modules/dnd/extract/npcs/canonicalize.go +++ b/internal/modules/dnd/extract/npcs/canonicalize.go @@ -17,8 +17,8 @@ func canonicalizeResponse(response *extractionResponse, doc *source.SourceDocume canonicalizeNPC(&response.NPCs[index]) } sort.SliceStable(response.NPCs, func(i, j int) bool { - left, leftOK := earliestSourceUnit(doc, response.NPCs[i]) - right, rightOK := earliestSourceUnit(doc, response.NPCs[j]) + left, leftOK := earliestSourceIndex(doc, response.NPCs[i]) + right, rightOK := earliestSourceIndex(doc, response.NPCs[j]) if leftOK != rightOK { return leftOK } @@ -77,7 +77,9 @@ func sameSourceRef(left npcSourceRefResponse, right npcSourceRefResponse) bool { left.EndUnitID.Int() == right.EndUnitID.Int() } -func earliestSourceUnit(doc *source.SourceDocument, npc npcResponse) (int, bool) { +func earliestSourceIndex(doc *source.SourceDocument, npc npcResponse) (int, bool) { + earliest := 0 + found := false for _, ref := range npc.SourceRefs { start := ref.StartUnitID.Int() end := ref.EndUnitID.Int() @@ -87,10 +89,13 @@ func earliestSourceUnit(doc *source.SourceDocument, npc npcResponse) (int, bool) if !startOK || !endOK || startIndex > endIndex { continue } - return start, true + if !found || startIndex < earliest { + earliest = startIndex + found = true + } } } - return 0, false + return earliest, found } func unitSortValue(ref shared.UnitRef) int { diff --git a/internal/modules/dnd/extract/npcs/extractor_test.go b/internal/modules/dnd/extract/npcs/extractor_test.go index e4bc24b..05c4ef9 100644 --- a/internal/modules/dnd/extract/npcs/extractor_test.go +++ b/internal/modules/dnd/extract/npcs/extractor_test.go @@ -56,6 +56,38 @@ func TestExtractReturnsCanonicalNPCListFromPrivateResponse(t *testing.T) { } } +func TestExtractOrdersNPCsBySourcePositionRatherThanUnitID(t *testing.T) { + client := &fakeNPCsLLMClient{response: extractionResponse{NPCs: []npcResponse{ + { + Name: "Later NPC", Aliases: []string{}, Description: "Appears later.", Relationships: []npcRelationshipResponse{}, + SourceRefs: responseSourceRefs(10, 10), + }, + { + Name: "Earlier NPC", Aliases: []string{}, Description: "Appears first.", Relationships: []npcRelationshipResponse{}, + SourceRefs: []npcSourceRefResponse{ + {StartUnitID: sharedUnitRef(50), EndUnitID: sharedUnitRef(50)}, + {StartUnitID: sharedUnitRef(100), EndUnitID: sharedUnitRef(100)}, + }, + }, + }}} + req := extractionRequest() + req.Source.Units = []source.SourceUnit{ + {ID: 100, Kind: "transcript_segment", Text: "Earlier NPC appears."}, + {ID: 10, Kind: "transcript_segment", Text: "Later NPC appears."}, + {ID: 50, Kind: "transcript_segment", Text: "Earlier NPC appears again."}, + } + req.Chunk.Units = append([]source.SourceUnit(nil), req.Source.Units...) + req.Chunk.Ref = source.SourceRef{SourceID: req.Source.ID, StartUnitID: 100, EndUnitID: 50} + + result, err := newExtractor(t, client).Extract(context.Background(), req) + if err != nil { + t.Fatalf("Extract() error = %v, want nil", err) + } + if len(result.Value.NPCs) != 2 || result.Value.NPCs[0].Name != "Earlier NPC" || result.Value.NPCs[1].Name != "Later NPC" { + t.Fatalf("NPC order = %#v, want source-document order", result.Value.NPCs) + } +} + func TestExtractPassesCampaignReferencesAsPromptInputs(t *testing.T) { client := &fakeNPCsLLMClient{response: extractionResponse{NPCs: []npcResponse{}}} req := extractionRequest() diff --git a/internal/modules/dnd/extract/spells/npc_registry.go b/internal/modules/dnd/extract/spells/npc_registry.go index 00215ed..de9afb2 100644 --- a/internal/modules/dnd/extract/spells/npc_registry.go +++ b/internal/modules/dnd/extract/spells/npc_registry.go @@ -9,6 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" ) @@ -56,14 +57,14 @@ func resolveNPCRegistry(references contracts.ReferenceSet) (npcRegistryPromptInp codec := npccodec.New() value, err := codec.Decode(item.Content) if err != nil { - return npcRegistryPromptInput{}, fmt.Errorf("decode NPC registry: %w", err) + return npcRegistryPromptInput{}, fmt.Errorf("decode NPC registry: invalid approved NPC JSON") } if issues := identity.ValidateList(value); len(issues) > 0 { - return npcRegistryPromptInput{}, fmt.Errorf("validate NPC registry identity: %s", formatNPCIdentityIssues(issues)) + return npcRegistryPromptInput{}, fmt.Errorf("%s", formatNPCIdentityIssues(issues)) } content, err := codec.Encode(value) if err != nil { - return npcRegistryPromptInput{}, fmt.Errorf("encode canonical NPC registry: %w", err) + return npcRegistryPromptInput{}, fmt.Errorf("encode canonical NPC registry: approved NPC value could not be encoded") } digest := semanticNPCRegistryDigest(content) @@ -83,7 +84,11 @@ func semanticNPCRegistryDigest(content []byte) string { func formatNPCIdentityIssues(issues []identity.Issue) string { parts := make([]string, len(issues)) for index, issue := range issues { - parts[index] = fmt.Sprintf("%s at record %d", issue.Code, issue.RecordIndex) + location := fmt.Sprintf("record %d", issue.RecordIndex) + if issue.AliasIndex >= 0 { + location += fmt.Sprintf(" alias %d", issue.AliasIndex) + } + parts[index] = fmt.Sprintf("%s at %s", issue.Code, location) } - return strings.Join(parts, ", ") + return diagnostics.Aggregate("validate NPC registry identity", parts) } diff --git a/internal/modules/dnd/extract/spells/npc_registry_test.go b/internal/modules/dnd/extract/spells/npc_registry_test.go index 0bfc731..cabaadc 100644 --- a/internal/modules/dnd/extract/spells/npc_registry_test.go +++ b/internal/modules/dnd/extract/spells/npc_registry_test.go @@ -4,14 +4,17 @@ import ( "bytes" "context" "encoding/json" + "fmt" "strings" "testing" + "unicode/utf8" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" ) @@ -90,12 +93,13 @@ func TestResolveNPCRegistryRejectsInvalidBoundaryValues(t *testing.T) { name string reference contracts.ReferenceSet wantError string + forbidden []string }{ {name: "zero items", reference: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{}}}}, wantError: "exactly one"}, {name: "multiple", reference: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{{Content: []byte(`{"npcs":[]}`)}, {Content: []byte(`{"npcs":[]}`)}}}}}, wantError: "exactly one"}, {name: "wrong media type", reference: npcRegistryReferenceWithMedia([]byte(`{"npcs":[]}`), "text/plain"), wantError: "must be application/json"}, - {name: "malformed JSON", reference: npcRegistryReference([]byte(`{"npcs":[`), "file:///private.json"), wantError: "decode NPC registry"}, - {name: "unknown field", reference: npcRegistryReference([]byte(`{"npcs":[],"unexpected":true}`), "file:///private.json"), wantError: "unknown field"}, + {name: "malformed JSON", reference: npcRegistryReference([]byte(`{"npcs":[],"MALFORMED_REGISTRY_SECRET":`), "file:///private.json"), wantError: "invalid approved NPC JSON", forbidden: []string{"MALFORMED_REGISTRY_SECRET"}}, + {name: "unknown field", reference: npcRegistryReference([]byte(`{"npcs":[],"UNKNOWN_FIELD_SECRET":true}`), "file:///private.json"), wantError: "invalid approved NPC JSON", forbidden: []string{"UNKNOWN_FIELD_SECRET"}}, {name: "invalid ID", reference: npcRegistryReference(marshalNPCRegistry(t, invalidID), "file:///private.json"), wantError: "decode NPC registry"}, {name: "alias collision", reference: npcRegistryReference(encodeNPCRegistry(t, valueWithAliasCollision), "file:///private.json"), wantError: string(identity.IssueAliasOwnershipCollision)}, {name: "byte limit", reference: npcRegistryReference(bytes.Repeat([]byte("x"), NPCRegistryMaxBytes+1), "file:///private.json"), wantError: "limit"}, @@ -106,13 +110,52 @@ func TestResolveNPCRegistryRejectsInvalidBoundaryValues(t *testing.T) { if err == nil || !strings.Contains(err.Error(), test.wantError) { t.Fatalf("resolveNPCRegistry() error = %v, want %q", err, test.wantError) } - if strings.Contains(err.Error(), "Mira Thorn") || strings.Contains(err.Error(), "The Greencloak") || strings.Contains(err.Error(), "private.json") { - t.Fatalf("error leaked registry content or provenance: %v", err) + for _, forbidden := range append(test.forbidden, "Mira Thorn", "The Greencloak", "private.json") { + if strings.Contains(err.Error(), forbidden) { + t.Fatalf("error leaked registry content or provenance %q: %v", forbidden, err) + } } }) } } +func TestResolveNPCRegistryBoundsIdentityDiagnosticsWithoutContent(t *testing.T) { + const recordCount = 30 + value := dnd.NPCList{NPCs: make([]dnd.NPC, recordCount)} + for index := range value.NPCs { + value.NPCs[index] = dnd.NPC{ + ID: "npc:sha256:0000000000000000000000000000000000000000000000000000000000000000", + Name: fmt.Sprintf("PRIVATE NPC %d", index), + Aliases: []string{"PRIVATE SHARED ALIAS"}, + Description: "PRIVATE DESCRIPTION", + Relationships: []dnd.NPCRelationship{}, + SourceRefs: []source.SourceRef{{SourceID: "private-source", StartUnitID: 1, EndUnitID: 1}}, + } + } + issues := identity.ValidateList(value) + if len(issues) <= diagnostics.MaxIssues { + t.Fatalf("identity issues = %d, want more than display limit", len(issues)) + } + + _, err := resolveNPCRegistry(npcRegistryReference(marshalNPCRegistry(t, value), "file:///private-registry.json")) + if err == nil { + t.Fatal("resolveNPCRegistry() error = nil, want bounded identity rejection") + } + message := err.Error() + if !utf8.ValidString(message) || len([]byte(message)) > diagnostics.MaxMessageBytes { + t.Fatalf("identity error has invalid encoding or size: bytes=%d message=%q", len([]byte(message)), message) + } + wantOmitted := fmt.Sprintf("%d additional issue(s) omitted", len(issues)-diagnostics.MaxIssues) + if !strings.Contains(message, wantOmitted) { + t.Fatalf("identity error = %q, want %q", message, wantOmitted) + } + for _, forbidden := range []string{"PRIVATE NPC", "PRIVATE SHARED ALIAS", "PRIVATE DESCRIPTION", "private-source", "private-registry.json"} { + if strings.Contains(message, forbidden) { + t.Fatalf("identity error leaked %q: %s", forbidden, message) + } + } +} + func TestNPCRegistryFingerprintIsSemanticAndDefensive(t *testing.T) { value := validNPCRegistryList() canonical := encodeNPCRegistry(t, value) diff --git a/internal/modules/dnd/normalize/npcs/normalizer.go b/internal/modules/dnd/normalize/npcs/normalizer.go index ab24a33..3966c26 100644 --- a/internal/modules/dnd/normalize/npcs/normalizer.go +++ b/internal/modules/dnd/normalize/npcs/normalizer.go @@ -13,8 +13,8 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/diagnostics" ) const ( diff --git a/internal/modules/dnd/validate/npcs/diagnostics/diagnostics.go b/internal/modules/dnd/npcs/diagnostics/diagnostics.go similarity index 97% rename from internal/modules/dnd/validate/npcs/diagnostics/diagnostics.go rename to internal/modules/dnd/npcs/diagnostics/diagnostics.go index e044212..d19a77d 100644 --- a/internal/modules/dnd/validate/npcs/diagnostics/diagnostics.go +++ b/internal/modules/dnd/npcs/diagnostics/diagnostics.go @@ -1,5 +1,5 @@ // Package diagnostics provides bounded, safe text for deterministic NPC -// validator decisions and warnings. +// decisions and warnings. package diagnostics import ( diff --git a/internal/modules/dnd/npcs/diagnostics/diagnostics_test.go b/internal/modules/dnd/npcs/diagnostics/diagnostics_test.go new file mode 100644 index 0000000..86969d1 --- /dev/null +++ b/internal/modules/dnd/npcs/diagnostics/diagnostics_test.go @@ -0,0 +1,28 @@ +package diagnostics + +import ( + "fmt" + "strings" + "testing" + "unicode/utf8" +) + +func TestAggregateEnforcesByteBudgetAndReportsOmissions(t *testing.T) { + issues := make([]string, MaxIssues) + for index := range issues { + issues[index] = fmt.Sprintf("issue-%d-%s", index, strings.Repeat("火", MaxDisplayedRunes)) + } + + message := Aggregate("invalid NPC data", issues) + if !utf8.ValidString(message) || len([]byte(message)) > MaxMessageBytes { + t.Fatalf("Aggregate() returned invalid or oversized message: bytes=%d message=%q", len([]byte(message)), message) + } + displayed := strings.Count(message, "issue-") + if displayed == 0 || displayed >= len(issues) { + t.Fatalf("Aggregate() displayed %d issues, want byte-budget omission", displayed) + } + wantOmitted := fmt.Sprintf("%d additional issue(s) omitted", len(issues)-displayed) + if !strings.Contains(message, wantOmitted) { + t.Fatalf("Aggregate() = %q, want %q", message, wantOmitted) + } +} diff --git a/internal/modules/dnd/validate/npcs/identity/validator.go b/internal/modules/dnd/validate/npcs/identity/validator.go index 8241e83..4150c66 100644 --- a/internal/modules/dnd/validate/npcs/identity/validator.go +++ b/internal/modules/dnd/validate/npcs/identity/validator.go @@ -8,8 +8,8 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" domainidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/diagnostics" npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/shape" ) diff --git a/internal/modules/dnd/validate/npcs/shape/validator.go b/internal/modules/dnd/validate/npcs/shape/validator.go index 62e9b7e..af23515 100644 --- a/internal/modules/dnd/validate/npcs/shape/validator.go +++ b/internal/modules/dnd/validate/npcs/shape/validator.go @@ -8,7 +8,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/diagnostics" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" ) const ( diff --git a/internal/modules/dnd/validate/npcs/source_refs/validator.go b/internal/modules/dnd/validate/npcs/source_refs/validator.go index 725c4aa..ae6f34a 100644 --- a/internal/modules/dnd/validate/npcs/source_refs/validator.go +++ b/internal/modules/dnd/validate/npcs/source_refs/validator.go @@ -8,7 +8,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/diagnostics" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/shape" ) diff --git a/internal/modules/dnd/validate/npcs/source_relatedness/validator.go b/internal/modules/dnd/validate/npcs/source_relatedness/validator.go index 40182ac..958bc36 100644 --- a/internal/modules/dnd/validate/npcs/source_relatedness/validator.go +++ b/internal/modules/dnd/validate/npcs/source_relatedness/validator.go @@ -9,8 +9,8 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/diagnostics" npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/shape" )