Close out the D&D combat turn extraction roadmap

This commit is contained in:
2026-07-21 07:49:57 -05:00
parent a1f5dce405
commit 110593ece1
15 changed files with 318 additions and 804 deletions

View File

@@ -423,14 +423,14 @@ accepts no references. To pass an NPC result to a later spell run, bind the
normalized payload explicitly at runtime; the maintained sequential example
documents that operator workflow.
The `dnd/combat-turns` extractor and normalizer declare the same optional
campaign slots and the structured `npcs` slot. The combat extractor uses the
NPC payload only to ground actor and target identity; the normalizer uses its
prepared immutable registry for the same canonicalization. Both slots accept
exactly one UTF-8 `application/json` file no larger than 1 MiB. The registry's
source ranges remain provenance for the reference and never become combat
evidence. Binding `npcs` to extraction and normalization is stage-local, so an
operator-driven combat run uses two explicit selectors:
The `dnd/combat-turns` extractor declares the optional campaign slots and the
structured `npcs` slot. Campaign references guide only the LLM extraction
stage. The deterministic normalizer declares only `npcs`, whose prepared
immutable registry supports the same actor and target canonicalization. Each
`npcs` slot accepts exactly one UTF-8 `application/json` file no larger than 1
MiB. The registry's source ranges remain provenance for the reference and never
become combat evidence. Binding `npcs` to extraction and normalization is
stage-local, so an operator-driven combat run uses two explicit selectors:
```text
combat.extract.npcs=<npc-run>/lanes/npcs.json

View File

@@ -93,13 +93,16 @@ inputs, and optionally the deprecated `roster` reference through the shared
party mapping. The optional `npcs` reference is an approved normalized NPC
artifact used only for identity grounding; it never supplies combat evidence.
The private response shape is the same as the durable turn/action shape except
that source references contain only `start_unit_id` and `end_unit_id`. The
The private response envelope has the same fields and JSON types as the durable
turn/action shape except that source references contain only `start_unit_id`
and `end_unit_id`. It enforces required and nullable field presence, types, and
unknown-field rejection, while deterministic validators own enum membership,
non-empty values and collections, and positive-number requirements. The
extractor assigns the current source ID, removes exact duplicate ranges, and
stable-sorts turns by the earliest valid source-document position. Numeric unit
IDs are identifiers; source-document slice position determines chronology.
Malformed candidate fields remain in the typed result for deterministic
validators to report.
Semantically malformed candidate fields remain in the typed result for the
configured validation and retry boundary.
## Deterministic candidate validation
@@ -125,10 +128,10 @@ schema, combat shape, source references, then source relatedness.
## Normalization boundary
The standalone normalizer uses key `dnd/combat-turns`, requires `merged`,
provides `normalized`, accepts no options, and accepts the same optional
`players`, `party`, `glossary`, deprecated `roster`, and structured `npcs`
reference slots as extraction. The NPC registry is resolved during
preparation; runtime normalization uses that immutable prepared view.
provides `normalized`, accepts no options, and accepts only the optional
structured `npcs` reference. Campaign references are LLM extraction context and
are not normalizer inputs. The NPC registry is resolved during preparation;
runtime normalization uses that immutable prepared view.
Normalization policy is `dnd.combat_turns.normalize.v1`. It display-normalizes
actor, summary, declarations, targets, and non-null resolutions; canonicalizes

View File

@@ -104,9 +104,13 @@ The small framework registry contains only generic test schemas; production
schemas remain package-owned.
The spell and combat extractors' package-owned prompts declare their structured
JSON inputs and private response schemas. The spell extractor's prompt declares a required
`application/json` `spell_catalog` input and an optional `application/json`
`npcs` input. The extractor generates the catalog input from its prepared
JSON inputs and private response schemas. The combat private schema owns the
transport envelope—required fields, JSON types, nullability, and unknown-field
rejection—while its deterministic validators own semantic constraints such as
enum membership, non-empty values and collections, and positive numbers. The
spell extractor's prompt declares a required `application/json` `spell_catalog`
input and an optional `application/json` `npcs` input. The extractor generates
the catalog input from its prepared
effective catalog as `{"spell_names":[...]}` using sorted canonical names only.
The shared D&D prompt assets include a generic NPC grounding fragment directly
after the campaign reference message for both extractors. When an NPC registry

View File

@@ -232,12 +232,17 @@ the shared transcript, campaign-reference, and NPC-grounding prompt inputs. It
maps the private response to `dnd.CombatTurnList`, assigns the current source
identity, removes exact duplicate source ranges, and orders turns by valid
source-document position while preserving malformed candidate fields for
deterministic validators. Its prompt and private response schema are
package-owned, and its prepared metadata and checkpoint fingerprints contain
deterministic validators. Its package-owned private response schema enforces
only the structural JSON envelope; semantic artifact constraints remain with
the validator chain. Its prepared metadata and checkpoint fingerprints contain
only prompt/schema/mapping identities plus an optional NPC registry digest.
The package exposes typed registration and is included in the production D&D
registrar with the default combat extraction chain.
The combat normalizer accepts only the optional structured NPC registry.
Campaign references remain extractor-only LLM context and are not materialized
for deterministic normalization.
### `internal/modules/dnd/normalize/npcs`
The NPC normalizer performs deterministic identity-aware consolidation in

View File

@@ -1,201 +0,0 @@
# D&D Combat-Turn Extraction
Status: Complete.
## Purpose
Add a production D&D combat-turn pipeline that converts session transcripts
into an ordered, evidence-backed account of combat activity. This is the next
recommended increment because spell and NPC extraction now provide the two
most important grounding vocabularies, while combat turns are the next useful
artifact explicitly identified by the sequential-pipeline strategy.
The feature uses the existing fixed pipeline and scene chunks for a more
structurally demanding artifact without requiring automatic workflow
composition, richer scene routing, or generic semantic deduplication.
## Target Outcome
An operator can:
1. run the NPC pipeline against a transcript;
2. run a combat-turn pipeline over the same transcript, explicitly binding the
normalized NPC artifact as a reference;
3. receive an ordered JSON list of validated combat turns and interrupting
combat events; and
4. trace every reported declaration and immediate resolution back to the
transcript units that support it.
The runs remain independent CLI invocations. Notarius does not discover prior
outputs, schedule dependent pipelines, or reconcile spell and combat artifacts
automatically.
## Combat-Turn Artifact
The new typed artifact represents an ordered list of combat-turn records. Each
record contains:
- the canonical in-world actor;
- a turn kind distinguishing an ordinary turn from a reaction, legendary
action, lair action, or other interrupting combat event;
- the combat round as a positive integer when it is explicit or unambiguous,
and `null` otherwise;
- one or more ordered actions;
- a concise turn-level summary; and
- one or more transcript source references that collectively support every
reported field.
Each action contains a conservative category, a concise declaration, zero or
more targets, and its immediate observed resolution. Resolution is `null` when
the cited passage establishes the declaration but no immediate resolution.
Categories cover attacks, spells, movement, items, ability checks, saving
throws, condition or state changes, and an `other` fallback without requiring
the transcript to use formal rules terminology.
Reactions and similar out-of-turn events appear at the point where they occur
in transcript chronology rather than being moved to the reacting creature's
later turn. Output order is derived from cited source position; numeric source
unit IDs are identifiers, not chronology.
## Inclusion And Evidence Policy
Include a record when the transcript establishes that an in-world participant
takes a combat turn or performs a discrete interrupting combat event. Report
only declarations and their immediate resolutions, including directly
associated rolls, damage, healing, movement, conditions, or target outcomes.
For every detail reported, cite all supporting transcript units.
Exclude:
- initiative setup that contains no turn or combat event;
- tactical planning, table talk, rules lookup, and hypothetical actions;
- corrected or abandoned declarations that never become an attempted action,
except where the correction is necessary to describe the final declaration;
- recap of combat that occurred outside the current source passage; and
- downstream consequences that occur on later turns or elsewhere in the
scene.
Do not infer a round number, action-economy classification, target, roll,
amount, condition, or outcome merely from D&D rules knowledge. Preserve the
session as played, and attribute nonstandard rulings to the GM or table when
that detail is relevant to the immediate resolution.
## Identity And Reference Grounding
The extractor uses the existing D&D transcript, player, party, and glossary
prompt inputs. It also accepts the normalized NPC artifact through an optional
structured `npcs` reference slot with the same validation, size, provenance,
content-safety, and semantic-checkpoint rules used by spell extraction.
The NPC registry helps select canonical actors and targets and recognize
aliases. It does not establish that combat occurred and never becomes source
evidence. Unmatched actors and targets remain permitted because a session may
introduce combatants that were omitted from an earlier NPC run.
The deterministic normalizer also accepts the registry. Exact canonical-name
or alias matches are rewritten to the registry's canonical display name for
actors and targets; ambiguous aliases and unmatched values remain unchanged
for validation and human review. Opaque player and party references continue
to guide the LLM but are not parsed into a new roster contract in this scope.
## Extraction, Validation, And Normalization
The extractor uses one structured LLM call per supplied chunk and returns typed
combat-turn candidates. It must preserve malformed candidates for the normal
validation and retry boundary rather than silently repairing unsupported
content in mapping code.
The production default validator chain is deterministic and covers:
- required fields, arrays, nullable-round shape, and supported enum values;
- a required non-empty evidence collection, source identity, unit existence,
and range order;
- actor and declared-action relatedness to cited transcript text, expressed as
bounded warnings where deterministic substring checks are only advisory; and
- normalized identity and duplicate invariants.
Normalization is deterministic and conservative. It normalizes display
whitespace, canonicalizes exact NPC identity matches, orders and deduplicates
exact source references, and collapses only exact duplicate records with the
same normalized actor, turn kind, round value, and complete valid evidence set.
The first record is retained without synthesizing or merging prose. Every
mutation or collapse emits a scoped warning.
An LLM-backed validator and semantic reconciliation normalizer are outside the
production chain. Human evaluation owns judgments such as whether the
model grouped a long turn correctly or omitted a subtle reaction.
## Scene Strategy
The combat extractor processes every chunk delivered by the configured
chunker. Existing D&D scene annotations remain useful context, but a
`primary_mode` value does not suppress an LLM call. Avoiding a
call based on an imperfect non-combat classification could silently lose the
very turns this artifact is intended to recover.
Scene-classification and routing improvements remain separate future work. The
combat pipeline stays compatible with generic chunks that carry no D&D
annotation.
## Provenance And Checkpoints
The extractor, normalizer, and validators report stable semantic identities
through the existing manifest and prepared-component fingerprint contracts.
Prompt, private response schema, artifact policy, normalization policy, and a
bound NPC registry's semantic digest must invalidate incompatible checkpoints.
Metadata and fingerprints contain identities, counts, and digests only. They
must not contain transcript text, combat records, NPC names, reference paths,
or raw reference content. Preparation failures remain bounded and content-safe
and occur before checkpoint handlers or pipeline execution are constructed.
## Evaluation
Use human-reviewed development runs rather than exact model-output goldens.
Evaluate at least the existing transcripts used for spell and NPC development,
with separate attention to:
- combat-turn detection precision and recall;
- actor and target identity;
- turn boundaries and chronological order;
- reactions and other interrupting events;
- declaration and immediate-resolution fidelity;
- round-number restraint;
- completeness and precision of evidence; and
- duplicate behavior at chunk or scene boundaries.
Frontier and inexpensive development models may differ substantially in
semantic quality. Deterministic tests should protect structure, provenance,
identity, ordering, normalization, and orchestration rather than require exact
combat prose.
## Out Of Scope
- Initiative trackers, current hit points, complete encounter-state replay, or
rules-engine validation.
- Automatic comparison or reconciliation with spell artifacts.
- NPC discovery, campaign-wide entity persistence, or deterministic PC-roster
parsing.
- LLM-backed validation or generic LLM-assisted deduplication.
- Automatic pipeline scheduling, prior-output discovery, or DAG execution.
- Scene-classification changes or skipping provider calls for non-combat
chunks.
- Narrative summaries outside the immediate combat-turn scope.
## Acceptance Criteria
- A selectable D&D combat lane produces a typed, durable JSON artifact with the
turn, action, chronology, and evidence semantics above.
- The default deterministic validator chain rejects malformed or invalidly
sourced records and emits bounded advisory relatedness warnings.
- Deterministic normalization canonicalizes exact NPC identities and collapses
only safely identical combat records while preserving chronology and
evidence.
- A normalized NPC artifact can be bound explicitly to both extraction and
normalization without becoming combat evidence.
- Semantic contracts and structured references participate in checkpoint
identity without leaking application content.
- Maintained configuration and operational examples demonstrate independent
NPC and combat invocations over the same transcript.
- Human review on representative sessions demonstrates useful turn extraction
without requiring scene-based call suppression or semantic reconciliation.

View File

@@ -1,515 +0,0 @@
# D&D Combat-Turn Extraction Implementation Plan
Status: Complete.
Implement this plan in order. The feature policy and target state are defined
in [D&D Combat-Turn Extraction](dnd-combat-turn-extraction.md); this document
owns implementation sequencing and concrete technical decisions.
Do not introduce a DAG, prior-run discovery, encounter state engine, scene-based
provider-call suppression, LLM-backed validator, or semantic reconciliation.
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
### Production Identities
Use these exact identities:
- artifact kind: `dnd/combat-turn-list`;
- extractor key and normalizer key: `dnd/combat-turns`;
- extractor capability: `dnd.combat_turns`;
- prompt ID: `dnd.combat_turns`, version `v1`;
- private response-schema key: `dnd_combat_turns_llm`;
- private schema ID: `notarius.dnd.combat_turns.llm`;
- private schema name: `notarius_dnd_combat_turns_llm_v1`;
- durable schema ID: `notarius.dnd.combat_turns`;
- durable schema name: `notarius_dnd_combat_turns_v1`;
- durable schema version: `v1`; and
- durable media type: `application/json`.
The private response schema omits `source_id`; mapping code assigns the current
source identity. No combat-turn ID is added in v1.
### Canonical Go And JSON Shape
Add these types to the canonical D&D model:
```go
type CombatTurnList struct {
CombatTurns []CombatTurn `json:"combat_turns"`
}
type CombatTurn struct {
Actor string `json:"actor"`
TurnKind CombatTurnKind `json:"turn_kind"`
Round *int `json:"round"`
Actions []CombatAction `json:"actions"`
Summary string `json:"summary"`
SourceRefs []source.SourceRef `json:"source_refs"`
}
type CombatAction struct {
Category CombatActionCategory `json:"category"`
Declaration string `json:"declaration"`
Targets []string `json:"targets"`
Resolution *string `json:"resolution"`
}
```
Define string types and only these lowercase durable values:
- `CombatTurnKind`: `turn`, `reaction`, `legendary_action`, `lair_action`,
`other`;
- `CombatActionCategory`: `attack`, `spell`, `movement`, `item`,
`ability_check`, `saving_throw`, `condition`, `other`.
All object fields and the top-level `combat_turns` array are required.
`combat_turns` may be empty. Every turn requires a non-empty actor, supported
turn kind, at least one action, non-empty summary, and at least one source
reference. `round` is either `null` or a positive integer. Every action requires
a supported category, non-empty declaration, and present `targets` array;
targets may be empty but may not contain empty strings. `resolution` is either
`null` or non-empty after trimming. Unknown fields are rejected at every object
level.
Strict raw decoding must distinguish a missing required `round` or `resolution`
key from an explicitly present JSON `null`, even though the accepted Go value
represents `null` with a nil pointer. Use presence-aware wire decoding or raw
key validation at codec and private-response boundaries; do not silently turn a
missing nullable field into an accepted null.
One turn-level source-reference collection collectively supports the actor,
kind, round, actions, summary, targets, and resolutions. Do not add per-action
references in v1.
### Ordering And Duplicate Identity
Source-document slice position is chronology. Numeric unit IDs are identifiers
only. An item's earliest evidence position is the minimum valid start-unit
index across all its ranges. Stable ordering puts records with valid evidence
in ascending earliest-position order, preserves encounter order for ties, and
puts records without valid evidence after valid records without reordering
them.
The normalizer may collapse two records only when all of these match:
- actor under `npcs/identity.ComparisonKey`;
- exact `turn_kind`;
- both round values, including `nil` versus a value; and
- the complete, non-empty, canonically ordered set of exact source references.
Every reference in the duplicate key must pass `source.ValidateRef` against the
current document. Invalid or empty evidence never participates in duplicate
collapse. Actions and prose do not participate in identity because two model
representations of the same cited turn should collapse; retain the complete
first record without merging prose or action lists.
### NPC Registry Contract And Prompt Reuse
Promote NPC registry preparation from the spell extractor into
`internal/modules/dnd/npcs/registry`. It owns:
- slot name `npcs` and maximum size 1,048,576 bytes;
- exact-one-item and `application/json` validation when bound;
- approved NPC codec decoding, whole-registry identity validation, canonical
re-encoding, semantic SHA-256 digest, and content-safe bounded failures;
- the exact unbound prompt value `{"npcs":[]}`;
- immutable accessors for bound state, a defensive NPC list, canonical bytes,
digest, count, and prompt input; and
- exact canonical-name/alias lookup using the NPC identity comparison policy.
Registry source references may identify another session and are not checked
against the current source. The resolver never returns content, names, aliases,
paths, or raw decoder errors in failures. Identity failures use issue codes and
record/alias indexes under the established 20-issue, 128-rune, and 4,096-byte
limits.
Move the generic bounded diagnostic helper from `dnd/npcs/diagnostics` to
`dnd/shared/diagnostics`, updating existing NPC consumers. This is an internal
package move with no diagnostic-policy change.
Add `common-dnd-npcs.md` to the shared D&D prompt assets. Its generic policy
allows exact canonical participant names and alias recognition while stating
that registry content is context, not event evidence. Both spell and combat
prompts use this exact fragment immediately after the existing shared campaign
reference message. Remove the spell-owned NPC fragment and include the shared
fragment in both prompt hashes. This intentionally changes the spell prompt
fingerprint once but must not change its request inputs, metadata shape,
registry semantics, or output contract.
Both combat extraction and normalization declare the same optional registry
slot. A bound registry contributes manifest metadata `npc_registry_digest` and
`npc_count` and a local checkpoint fingerprint `npc_registry`. An absent slot
contributes neither metadata fields nor that fingerprint.
### Validation And Diagnostics
Add deterministic validators with these exact identities:
| Validator key | Result reason | Policy fingerprint |
| --- | --- | --- |
| `extract/dnd/combat-turns/shape` | rejection `invalid_combat_turn_shape` | `dnd.combat_turns.validator.shape.v1` |
| `extract/dnd/combat-turns/source_refs` | rejection `invalid_combat_turn_source_refs` | `dnd.combat_turns.validator.source_refs.v1` |
| `extract/dnd/combat-turns/source_relatedness` | warning `combat_turn_not_near_source` | `dnd.combat_turns.validator.source_relatedness.v1` |
| `normalize/dnd/combat-turns/invariants` | rejection `invalid_combat_turn_normalization` | `dnd.combat_turns.validator.normalized.v1` |
Shape owns required arrays, enum membership, nullable fields, positive rounds,
and non-empty strings. Source-reference validation owns source identity, unit
existence, and range order through `source.ValidateRef`. Later validators defer
when shape is invalid; relatedness also defers when any source range is invalid.
Relatedness combines the turn's cited units once in document order, removing
overlap. The actor is related when its NPC comparison key is a substring of the
comparison-normalized cited text. For each action declaration, split its
comparison-normalized form on runes that are not Unicode letters or digits,
retain tokens of at least four Unicode code points, and require at least one
retained token to occur as a complete cited-text token. No retained token means
the declaration is unrelated. Emit at most one approved warning per turn,
listing whether the actor and which action indexes were not related. Do not
check targets deterministically.
The normalize invariant validator requires display-normalized strings,
comparison-unique targets within each action, canonical exact source-reference
order without duplicates, chronological record order for valid evidence, and
absence of the duplicate identity defined above. It does not require every
actor or target to exist in the optional NPC registry.
All rejection and warning messages use the shared diagnostic helper: at most 20
displayed issues, at most 128 Unicode code points per displayed value, valid
UTF-8, at most 4,096 bytes, Go quoting for control characters, and the exact
total omitted count. Validators use strict empty-options decoders and provide
one local `policy` checkpoint fingerprint with the value in the table.
### Normalization Policy
Use normalization policy `dnd.combat_turns.normalize.v1` and these warning
reason codes:
- `combat_turn_fields_normalized`;
- `combat_actor_canonicalized`;
- `combat_target_canonicalized`;
- `source_references_normalized`;
- `combat_turns_reordered`; and
- `duplicate_combat_turn_collapsed`.
Deep-clone all nested slices and pointers. Normalize actor, summary,
declarations, targets, and non-null resolutions by collapsing Unicode
whitespace through `npcs/identity.NormalizeDisplay`. Preserve `nil` resolution.
Remove comparison-duplicate targets while retaining the first display value and
target order. Do not deduplicate or reorder actions.
When a registry is bound, rewrite an actor or target only if its comparison key
matches exactly one validated canonical name or alias. Preserve unmatched
values. Registry validation makes ambiguous lookup impossible at preparation;
do not guess or perform fuzzy matching.
Sort every source-reference list by exact `source_id`, `start_unit_id`, and
`end_unit_id`, then remove exact duplicates. Stable-sort records by the
chronology rule before duplicate detection. Collapse duplicates in that order
and retain the first record unchanged after its per-record normalization.
Warning scopes use merged input indexes such as `combat_turns[3]`, even after
sorting or collapse. Emit one bounded warning for each affected record or
collapsed group; group warnings identify the retained and removed input indexes.
Only warnings from a validator-approved attempt become durable, under existing
framework policy.
The normalizer exposes manifest metadata `normalization_policy`,
`identity_policy`, and optional registry digest/count. Its checkpoint
fingerprints are `normalization_policy`, `identity_policy`, and optional
`npc_registry`; bump the normalization policy when any transformation,
ordering, or duplicate rule changes.
### Testing Rules
Follow `docs/policy/testing.md`. Tests are offline and deterministic. Use a fake
structured LLM only at the completion boundary. Protect durable shapes, domain
invariants, preparation failures, reference sensitivity, registration, and one
representative assembled workflow.
Do not add prompt-prose change detectors or tests requiring particular words or
phrases. Prompt tests may verify registration, message/input wiring, prompt and
schema identities, stable shared-fragment use, and absence of raw content from
metadata or diagnostics. Human evaluation owns semantic output quality.
## Stage 1: Shared NPC Registry And Prompt Grounding
### Goal
Create one reusable, content-safe NPC registry boundary before adding a second
consumer, while preserving spell behavior.
### Changes
- Add the domain registry resolver and immutable lookup described above; migrate
spell extraction from its private resolver without changing the spell
extractor's public module contract.
- Relocate bounded D&D diagnostics to the shared domain package and update all
NPC imports and tests.
- Add the shared NPC prompt fragment, move the spell prompt to it, place it
immediately after shared campaign references, and remove the spell-owned
fragment.
- Keep raw reference provenance in framework identity independently from the
semantic registry fingerprint. Preserve the exact empty prompt input and all
existing content-safe error behavior.
- Update the internal overview, module, and LLM documentation in this stage so
the implemented registry owner and shared prompt-fragment ownership remain
accurate while the later combat work remains independently scoped.
### Tests
- Move resolver contract tests to the domain package: absent, valid, formatted-
equivalent, malformed, unknown-field, invalid-ID, collision, media type,
item count, byte limit, defensive copies, lookup, digest, and bounded
content-free diagnostics.
- Retain spell-level tests only for spell request wiring, metadata/fingerprint
behavior, and no-regression output; remove duplicated resolver cases.
- Verify the shared prompt asset registers for spells and hashes the shared
fragment without asserting its prose.
### Completion Check
Run `go test ./internal/modules/dnd/npcs/... ./internal/modules/dnd/extract/spells ./internal/modules/dnd/validate/npcs/...`
and `git diff --check`.
## Stage 2: Combat Domain Contract And Codec
### Goal
Establish the typed artifact and durable serialization boundary without making
the combat module selectable.
### Changes
- Add the canonical combat types, enums, constants, and artifact kind using the
exact shape and values above.
- Add `internal/modules/dnd/codec/combatturns`, following the existing candidate
versus approved codec boundary:
- strict single-value JSON with unknown-field rejection;
- candidate encode/decode preserving validator-visible invalid values;
- approved encode/decode enforcing structural validity only;
- defensive schema and metadata values; and
- metadata containing `combat_turn_count` only.
- Add the durable v1 JSON Schema with required nullable fields, enum values,
source-reference shape, array rules, and `additionalProperties: false`.
- Add `docs/integrations/dnd-combat-turn-artifacts.md`, describing only the
implemented artifact and codec at this stage.
### Tests
- Test approved round trips and candidate preservation for every nullable,
enum, required-array, required-string, target, and source-reference boundary.
- Test malformed, trailing, unknown-field, and invalid structural input without
snapshotting complete errors.
- Test defensive schema/metadata copies, nil versus present-empty arrays, and
exact codec identity.
- Keep one compact durable v1 fixture for intentional compatibility coverage.
### Completion Check
Run `go test ./internal/modules/dnd/codec/combatturns` and `git diff --check`.
## Stage 3: Combat Extractor And Extraction Validators
### Goal
Implement package-complete LLM extraction and deterministic candidate
validation without production composition.
### Changes
- Add `internal/modules/dnd/extract/combatturns` with the exact module, prompt,
schema, capability, artifact, and reference identities above. It accepts no
options and requires `chunks` and `source.transcript`. The prompt manifest is
`dnd.combat_turns.yaml`, uses default profile `gemini-2-flash`, JSON Schema
validation, and zero provider-side repair attempts so pipeline retries remain
the only extraction retry policy.
- Declare existing optional `players`, `party`, `glossary`, and deprecated
`roster` slots plus the optional structured NPC registry slot. Reuse shared
D&D prompt inputs and the shared NPC prompt fragment.
- Embed a private response schema matching the durable shape except that source
ranges omit `source_id`. Require present arrays and nullable round/resolution
values exactly as in the durable contract.
- Prompt for the inclusion, exclusion, chronology, identity, round restraint,
and immediate-resolution policy in the feature roadmap. Instruct the model
that campaign and NPC references disambiguate identities but are never combat
evidence.
- Preserve malformed response values for validators. Canonicalize and exactly
deduplicate source ranges, assign the current source ID, and stable-sort
records by earliest valid source-document position. Do not merge records,
canonicalize NPC names deterministically, or skip any chunk in the extractor.
- Expose prompt and private-schema manifest metadata plus optional registry
digest/count. Fingerprint local names `prompt`, `response_schema`,
`mapping_policy` with value `dnd.combat_turns.extract_mapping.v1`, and optional
`npc_registry`.
- Implement shape, source-reference, and source-relatedness validators with the
exact contracts and policies above.
### Tests
- Through a fake LLM, test request identity, profile/session propagation,
chunk-scoped source input, campaign and NPC inputs, response mapping, source
assignment, non-monotonic-unit chronology, invalid-candidate preservation,
cancellation, and contextual provider failures.
- Test prompt/schema registration, required input wiring, metadata and
fingerprint sensitivity, defensive copies, and content redaction without
asserting prompt prose.
- Test each validator's meaningful approval, rejection, deferral, and warning
categories, including Unicode comparison, overlapping evidence, declaration
tokenization, bounded diagnostics, strict options, and typed registration.
- Verify that a bound NPC registry affects grounding metadata and checkpoint
identity but never supplies combat source references.
### Completion Check
Run `go test ./internal/modules/dnd/extract/combatturns ./internal/modules/dnd/validate/combatturns/...`
and `git diff --check`.
## Stage 4: Combat Normalization And Final Invariants
### Goal
Implement conservative identity canonicalization, chronological ordering, and
exact-evidence duplicate collapse.
### Changes
- Add `internal/modules/dnd/normalize/combatturns` with key
`dnd/combat-turns`, requirements `merged`, capability `normalized`, no
options, and the optional NPC registry slot.
- Resolve and retain the immutable registry during preparation. Runtime uses
the prepared view; it does not parse references repeatedly.
- Apply the exact per-record, registry, source-reference, chronology, duplicate,
warning, metadata, and fingerprint policies above. Never mutate or retain
caller-owned slices, pointers, registry data, or source data.
- Add the normalized-invariants validator. It remains deterministic, accepts no
references of its own, and defers malformed shape or invalid source evidence
to their owning validators.
### Tests
- Use table-driven cases for whitespace, nullable resolution, target
deduplication, actor/target registry matches, unmatched names, reference
normalization, non-monotonic source IDs, stable ties, and warning scopes.
- Cover duplicate collapse and non-collapse for every identity dimension,
especially invalid evidence, distinct ranges, different turn kinds, and
`nil` versus numbered rounds.
- Prove actions/prose come only from the first retained record, merged input is
immutable, and all nested output storage is independent.
- Test absent and bound registries, preparation failures, metadata,
fingerprints, policy sensitivity, cancellation, strict options, and module
registration.
- Test normalized-invariant rejection for each owned invariant without
duplicating shape and source-validator case matrices.
### Completion Check
Run `go test ./internal/modules/dnd/normalize/combatturns ./internal/modules/dnd/validate/combatturns/...`
and `git diff --check`.
## Stage 5: Production Composition, Sequential Workflow, And Documentation
### Goal
Make the complete combat lane selectable, verify the assembled workflow, and
document current behavior in canonical locations.
### Changes
- Extend the D&D registrar with the combat codec, extractor and prompt assets,
typed append-order merger, combat normalizer, typed no-op normalizer, all four
validators, and typed always-accept/always-reject variants.
- Add append behavior that preserves present-empty versus nil output and chunk
record order before normalization.
- Register the extract default chain in this order:
`generic/valid_json`, `generic/valid_json_schema`, combat shape, combat source
references, combat source relatedness.
- Register the normalize default chain in this order:
`generic/valid_json`, `generic/valid_json_schema`, combat shape, normalized
invariants, combat source references, combat source relatedness.
- Do not add a merge validator chain or change framework defaults.
- Add `examples/dnd-combat-turns.config.yml` and
`examples/dnd-npc-combat-sequential.config.yml`. Use version 3, safe relative
state paths, explicit checkpoint `enabled: false`, a generic chunker, combat
extraction with `retries: 2`, and combat normalization. Keep the dynamic NPC
output unbound in the static sequential example.
- Demonstrate runtime binding to both stage-local slots with selectors
`combat.extract.npcs=<npc-run>/lanes/npcs.json` and
`combat.normalize.npcs=<npc-run>/lanes/npcs.json`.
- Update canonical current-behavior documentation:
- Configuration owns module/validator catalogs, reference slots, limits,
default chains, and maintained examples;
- CLI owns the explicit two-selector invocation syntax;
- Operations owns the independent NPC-then-combat workflow and state
sensitivity;
- the combat integration contract owns durable fields, enum values,
evidence, normalization, warnings, and manifest metadata;
- the NPC integration contract notes combat as a consumer without redefining
combat fields;
- JSON output links the new lane payload; and
- internal overview, module, and LLM docs describe concrete packages,
preparation, prompt reuse, and fingerprints.
- At completion, mark this plan and the feature roadmap complete. Remove the
implemented combat proposal from `future.md` while retaining scene-routing,
narrative, generic deduplication, and other unimplemented work.
### Tests
- Extend registrar tests for production keys, typed variants, prompt assets,
default chain order, nil dependencies, and duplicate registration behavior.
- Add config/example resolution coverage for stage-local NPC bindings,
capabilities, codec compatibility, strict options, and invalid validator
placement.
- Add one assembled pipeline integration covering extract, retry-capable
validation, append merge, registry-backed normalization, final validation,
JSON output, warnings, manifest metadata, and checkpoint fingerprints using a
fake LLM.
- Add one sequential integration that materializes normalized NPC output as
both combat references and verifies canonical actor/target output, reference
provenance, and that NPC source ranges never become combat evidence.
- Add preparation-boundary coverage showing malformed or oversized NPC input
fails before checkpoint construction or pipeline execution. Do not duplicate
the shared resolver's complete malformed-input matrix.
- Validate maintained examples and documentation links through existing test
mechanisms.
### Completion Check
Run the final verification suite.
## 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:
- D&D or provider behavior leaking into generic framework packages;
- accidental scene-based call suppression or automatic pipeline composition;
- mutation or aliasing of typed artifacts, schemas, references, metadata, or
fingerprints;
- transcript, registry, prompt, schema, path, or decoder content leaking into
errors, manifests, fingerprints, or redacted summaries;
- prompt-prose change-detector tests, redundant cross-layer cases, or exact LLM
output goldens;
- current-behavior documentation claiming features before the implementing
stage lands; and
- unrelated worktree changes.
## Open Questions
None. Artifact fields and enums, nullable behavior, chronology, duplicate
identity, reference reuse, prompt composition, validator placement,
normalization, checkpoint semantics, documentation ownership, and stage
boundaries are fixed by this plan.