Finish the enemy engagement cleanup
This commit is contained in:
@@ -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.
|
|
||||||
@@ -2,6 +2,7 @@ package engagements
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -35,11 +36,17 @@ func TestValidatorEnforcesOneEngagementPerSubject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
validator := New(Options{})
|
||||||
for _, unitID := range []int{1, 2} {
|
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 {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("separate scene validation = %#v, %v; want approved", result, err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user