diff --git a/docs/roadmap/combat.md b/docs/roadmap/combat.md deleted file mode 100644 index 88ce396..0000000 --- a/docs/roadmap/combat.md +++ /dev/null @@ -1,363 +0,0 @@ -# Combat Enemy Event Extraction - -## Purpose - -Define a minimal, evidence-grounded D&D artifact that records enemies engaged -and their explicitly observed combat outcomes. The artifact is the durable -primitive from which an end-of-session encounter ledger can be derived; this -scope does not ask an LLM to synthesize a final ledger or infer an outcome from -silence. - -This roadmap follows -[ADR-0009](../adr/0009-minimal-evidence-grounded-extraction-artifacts.md): -the extractor answers one narrow question, every reported fact has direct -transcript evidence, and generated artifacts provide grounding rather than -evidence. - -## Context - -Notarius can currently identify NPCs, classify NPC interactions, record combat -turns, and classify scenes. Those artifacts answer useful but different -questions: - -- the NPC registry establishes individually identifiable names; -- NPC interactions identify such occurrences as `combat_opponent` but do not - record outcomes; -- combat turns establish actors and action boundaries but deliberately omit - action contents and state; and -- scene descriptions determine which chunks are combat scenes. - -None can establish by itself that an opponent was killed, fled, captured, or -incapacitated. Deriving those outcomes from an absent later turn, the end of a -scene, or a combat-opponent classification would create unsupported claims. -The new extractor therefore reads the combat transcript directly while using -the four normalized artifacts above as explicit auxiliary grounding. - -## Goals - -- Produce an ordered list of directly evidenced enemy engagement and outcome - events. -- Keep the durable event shape to `name`, `kind`, and `source_refs`. -- Run the LLM only for chunks with an exact `combat` scene classification. -- Ground named subjects in the normalized NPC registry and use prior combat - artifacts to constrain participation and allegiance without treating them as - evidence. -- Support unnamed individuals and groups without inventing synthetic member - identities. -- Preserve repeated engagements and state changes rather than collapsing them - into one synthesized terminal record. -- Integrate the lane into the maintained complete D&D pipeline as a later - ordered step. -- Follow the existing D&D codec, prompt, normalization, validation, - registration, evidence-publication, checkpoint, and documentation - conventions. - -## Target End State - -### Durable Artifact Contract - -The new production module key is `dnd/enemy-events`. Its typed artifact is: - -| Property | Value | -| --- | --- | -| Artifact kind | `dnd/enemy-event-list` | -| Schema ID | `notarius.dnd.enemy_events` | -| Schema name | `notarius_dnd_enemy_events_v1` | -| Schema version | `v1` | -| Media type | `application/json` | -| Root field | `events` | - -The root is a strict object with one required `events` array. The array may be -empty. Each event is a strict object with exactly these required fields: - -| Field | Contract | -| --- | --- | -| `name` | Non-empty display name or directly grounded collective subject label. | -| `kind` | `engaged`, `killed`, `fled`, `captured`, or `incapacitated`. | -| `source_refs` | One or more inclusive current-transcript evidence ranges. | - -Each source reference has exactly `source_id`, `start_unit_id`, and -`end_unit_id`, following the existing generic source-reference contract. The -durable schema rejects unknown fields and incompatible types. - -Example: - -```json -{ - "events": [ - { - "name": "Ashfang", - "kind": "engaged", - "source_refs": [ - {"source_id": "session-7", "start_unit_id": 41, "end_unit_id": 42} - ] - }, - { - "name": "Ashfang", - "kind": "fled", - "source_refs": [ - {"source_id": "session-7", "start_unit_id": 57, "end_unit_id": 58} - ] - } - ] -} -``` - -The artifact does not contain an NPC ID, scene ID, quantity, confidence, -description, rationale, summary, current state, or inferred terminal outcome. -Scene membership remains deterministically derivable by intersecting event -evidence with scene ranges. Cross-artifact durable NPC IDs remain governed by -the separate future identity-link design consideration. - -### Event Semantics - -| Kind | Required evidence | -| --- | --- | -| `engaged` | The subject is directly established as actively opposing the party in combat. Emit at most one engagement for the same subject in one combat scene. | -| `killed` | The transcript explicitly establishes that the subject died or was killed. Damage, defeat, disappearance, or the end of combat is insufficient. | -| `fled` | The subject explicitly escapes, retreats, or otherwise leaves the combat to avoid continued engagement. Mere movement or absence from later turns is insufficient. | -| `captured` | The subject is explicitly taken prisoner or secured under the party's control. A grapple or temporary restraint alone is insufficient. | -| `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. Do not infer this from a missed turn. | - -An outcome may share evidence with an engagement, in which case both events -are emitted. A later engagement for the same named subject is preserved, even -after an earlier outcome, because enemies may escape, recover, return, or be -revived. The event list records observations rather than asserting irreversible -state transitions. - -`active` and `unresolved` are not model-produced event kinds. A downstream -ledger may report the most recent explicit outcome for a subject and use -`unresolved` when an engagement has no later evidenced outcome. It must not use -`active` merely because no terminal event was extracted. A distinct future -contract would be required if operators need an evidenced ongoing-combat state. - -### Subject Identity And Groups - -- An individually named subject that matches the normalized NPC registry uses - the registry's canonical display name. -- A hostile creature or summoned entity is included when it directly opposes - the party, whether or not it has an NPC-registry entry. -- An unnamed homogeneous group is represented collectively with the narrowest - transcript-grounded label, such as `Orcs`. The extractor does not invent - `Orc 1`, `Orc 2`, or other synthetic identities. -- When evidence applies only to a subgroup or one unidentified member, use the - narrowest supported phrase, such as `One orc` or `Remaining orcs`. Do not - claim a deterministic identity link between that phrase and another group - event. -- Named subjects may be compared across scenes through the canonical NPC name. - Generic or collective subjects are scoped to their combat scene by their - evidence range and are not assumed to be the same group in another scene. -- Party members, combat allies, neutral observers, mentioned-but-absent - enemies, hazards, traps, and environmental effects are excluded. Uncertain - allegiance is not enough to emit an event. - -These conservative rules prefer separate, explicitly evidenced observations -over a falsely precise encounter roster. - -### Ordered Pipeline And References - -The extractor is an LLM-backed lane in a third maintained D&D pipeline step. -It requires four JSON artifact reference slots at extraction: - -| Slot | Artifact kind | Purpose | -| --- | --- | --- | -| `npcs` | `dnd/npc-list` | Canonical grounding for individually named subjects. | -| `scene_descriptions` | `dnd/scene-description-list` | Eligibility gate for exact combat scenes. | -| `combat_turns` | `dnd/combat-turn-list` | Grounding for observed combat actors and action boundaries. | -| `npc_interactions` | `dnd/npc-interaction-list` | Grounding for named `combat_opponent` occurrences. | - -Each slot is required, accepts `application/json`, accepts only its listed -artifact kind, and has a 1,048,576-byte limit consistent with the current D&D -registry references. The framework may satisfy the contract with a compatible -external artifact, but the maintained complete pipeline uses generated -references from earlier steps. - -The deterministic normalizer requires only the `npcs` slot. The other three -artifacts inform extraction and scene routing but do not participate in final -name canonicalization. - -The maintained complete pipeline has this topology: - -```text -describe-session - -> npcs - -> scene-descriptions - -extract-events - -> combat-turns - -> npc-interactions - -track-enemies - -> enemy-events -``` - -`track-enemies` consumes `npcs` and `scene-descriptions` from -`describe-session`, and `combat-turns` and `npc-interactions` from -`extract-events`. An invalid, rejected, absent, ambiguous, or incompatible -upstream generated artifact prevents the dependent step from starting under -the existing single-run failure semantics. No new DAG or workflow mechanism is -introduced. - -### Eligibility And Grounding Projections - -The extractor reuses the scene-description registry's exact chunk matching: - -- exact `combat` match: prepare grounding inputs and call the LLM; -- exact non-combat match: return an accepted non-nil empty event list without - an LLM call; and -- missing or mismatched classification: return an accepted non-nil empty event - list with the bounded `scene_classification_unavailable` warning and no LLM - call. - -Auxiliary artifacts are decoded and projected into compact, canonical prompt -inputs: - -- NPCs retain only canonical `name` values; -- combat turns retain only `actor` and `turn_kind`; -- NPC interactions retain only `name` and `kind`, and only - `combat_opponent` records are included; and -- scene descriptions are used only for eligibility and are not rendered into - the extraction prompt. - -All artifact IDs, source references, summaries, producer provenance, and -origin URIs are excluded from the projections. Projection content receives its -own digest. Full generated-reference content and producer provenance continue -to participate in the framework's existing handoff and checkpoint identity. -Neither a projection nor its source references may be copied into event -evidence. - -### Prompt And Private Response Contract - -The extractor uses prompt ID `dnd.enemy_events`, PromptKit default profile -`dnd-extraction`, and a strict private response schema named -`notarius_dnd_enemy_events_llm_v1`. The private schema contains required -`name`, `kind`, and segment-range candidates; deterministic mapping attaches -the current source ID and preserves semantic candidates for validators. - -Prompt construction follows the shared D&D extraction convention. The first -four rendered messages remain exactly the shared system, identity, campaign -reference, and chunk-transcript messages in their existing cache-control -arrangement. Evidence policy, the NPC registry, module-owned compact combat -grounding, task, and final instructions follow the transcript. The new module -reuses shared assets instead of copying their text, and its final instructions -message carries ephemeral cache control. - -The prompt makes clear that grounding artifacts are hints, not proof; the -current combat transcript is the only evidence; every event requires its own -chunk-confined ranges; and absence from a turn list or opponent list does not -establish an outcome. - -### Deterministic Normalization - -`dnd/enemy-events` also identifies a deterministic normalizer for the same -typed artifact. It: - -1. clones caller-owned data; -2. collapses display whitespace in every subject name; -3. replaces a recognized subject with the canonical NPC-registry display - name, while retaining normalized unmatched group labels; -4. canonically orders and removes exact duplicate source ranges; -5. orders events by earliest valid source position, then normalized comparison - name, display name, the explicit kind order `engaged`, `incapacitated`, - `captured`, `fled`, `killed`, and complete reference sequence; invalid - evidence sorts after valid evidence for deterministic validation; and -6. collapses only records with the same normalized subject identity, kind, and - complete canonical evidence sequence, retaining the earliest display value. - -Distinct event kinds, distinct evidence, repeated engagements in different -scenes, and later outcome changes remain separate. Normalization never -manufactures an engagement or outcome and never aggregates the list into a -ledger. It is idempotent and emits bounded warnings for name changes, evidence -normalization, reordering, and exact duplicate collapse. - -### Validation And Production Policy - -The D&D registrar provides these validators: - -- `extract/dnd/enemy-events/shape`; -- `extract/dnd/enemy-events/engagements`; -- `extract/dnd/enemy-events/source_refs`; -- `extract/dnd/enemy-events/source_relatedness`; and -- `normalize/dnd/enemy-events/invariants`. - -The extraction default chain is: - -```text -generic/valid_json -extract/dnd/enemy-events/shape -extract/dnd/enemy-events/engagements -extract/dnd/enemy-events/source_refs -generic/valid_json_schema -extract/dnd/enemy-events/source_relatedness -``` - -The normalization default chain inserts -`normalize/dnd/enemy-events/invariants` after the shape validator. Shape owns -non-empty names, the closed event-kind vocabulary, and non-empty evidence. -Source-reference validation owns current-document range validity. Durable -schema validation owns the public JSON shape. Relatedness remains advisory. -The invariant validator owns canonical display values, registry-aware -canonical names where applicable, canonical evidence, deterministic ordering, -and absence of exact duplicates. - -No production LLM-backed validator is added. Semantic quality should first be -evaluated through the extractor prompt and human review rather than adding a -second model judgment without a concrete policy. - -The registrar also supplies the strict codec, typed append-order merger, -deterministic normalizer, typed no-op normalizer compatibility, typed evidence -projection, prompt and schema assets, safe manifest metadata, and stable -checkpoint fingerprints. Operation-varying generated reference content stays -in framework-owned reference/checkpoint identity rather than static module -metadata. - -### Output, Examples, And Documentation - -The JSON output encoder publishes normalized enemy events like every other -typed lane. The maintained complete example adds `enemy-events` to its evidence -context allowlist and demonstrates the third generated-reference step. The -minimal example remains unchanged. - -When implemented, canonical documentation adds a D&D enemy-event integration -contract, adds the production keys, slots, and default chains to -`docs/config.md`, and updates `docs/internal/dnd.md` for the seventh lane, -grounding projections, prompt ordering, eligibility, and normalization. -Current-behavior documents do not describe the feature before its code lands. - -## Out Of Scope - -- A separately published aggregated enemy-ledger artifact or inventory-like - state store. -- Model-produced `active`, `unresolved`, confidence, rationale, description, or - summary fields. -- Synthetic identities or quantities for indistinguishable enemies. -- Durable NPC-ID links, alias resolution beyond the normalized registry, or a - general cross-artifact entity graph. -- Inferring outcomes from turn absence, scene termination, hit-point guesses, - initiative order, or other artifacts. -- Deriving enemy events solely from combat turns or NPC interactions. -- Changing existing combat-turn, NPC, NPC-interaction, or scene-description - schemas. -- A general workflow DAG, general reference-query system, or generic semantic - deduplication framework. -- Paid or live-model tests in the default suite. - -## Acceptance Criteria - -- `dnd/enemy-events` produces the strict minimal `v1` event artifact and - preserves direct transcript evidence for every record. -- Only exact combat scenes invoke the LLM; non-combat and unclassified chunks - follow the defined empty-result behavior. -- The extractor consumes validated NPC, scene-description, combat-turn, and - NPC-interaction references through source-free grounding projections. -- Named enemies canonicalize through the NPC registry while unmatched - individuals and collective labels remain supported without invented IDs. -- The normalizer is deterministic, idempotent, ownership-safe, warning-bounded, - and collapses exact duplicates only. -- The codec, validators, default chains, merger, no-op compatibility, evidence - projector, prompt assets, fingerprints, and module specs are registered by - the D&D family. -- The maintained complete pipeline resolves and runs the new third step with - compatible generated references from both earlier steps. -- Canonical documentation and maintained examples describe only implemented - behavior, remain secret-free, and pass their existing offline contract tests. diff --git a/internal/modules/dnd/validate/enemyevents/engagements/validator_test.go b/internal/modules/dnd/validate/enemyevents/engagements/validator_test.go index 733daf9..6855f24 100644 --- a/internal/modules/dnd/validate/enemyevents/engagements/validator_test.go +++ b/internal/modules/dnd/validate/enemyevents/engagements/validator_test.go @@ -2,6 +2,7 @@ package engagements import ( "context" + "reflect" "strings" "testing" @@ -35,11 +36,17 @@ func TestValidatorEnforcesOneEngagementPerSubject(t *testing.T) { } }) } + validator := New(Options{}) for _, unitID := range []int{1, 2} { - result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: events(event("Ashfang", dnd.EnemyEventKindEngaged, unitID))}) + value := events(event("Ashfang", dnd.EnemyEventKindEngaged, unitID)) + before := events(event("Ashfang", dnd.EnemyEventKindEngaged, unitID)) + result, err := validator.Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Value: value}) if err != nil || !result.Approved { t.Fatalf("separate scene validation = %#v, %v; want approved", result, err) } + if !reflect.DeepEqual(value, before) { + t.Fatalf("Validate() mutated separate scene value: got %#v, want %#v", value, before) + } } }