Compare commits

130 Commits

Author SHA1 Message Date
bef8ca263b Tighten evidence context source excerpts 2026-08-09 20:04:46 +00:00
f6d037b613 Document evidence context source unit excerpts 2026-08-09 19:47:27 +00:00
449b506804 Update evidence context output coverage 2026-08-09 19:45:21 +00:00
071a78ae22 Replace evidence context with source unit excerpts 2026-08-09 19:42:33 +00:00
ad1cba41c2 Fix OpenAI reconciliation schema 2026-08-09 18:49:29 +00:00
67338798aa Complete the semantic reconciliation roadmap 2026-08-09 18:30:32 +00:00
e95e2f2220 Finalize semantic reconciliation documentation 2026-08-09 17:05:17 +00:00
628b8d1800 Remove legacy reconciliation path 2026-08-09 16:57:44 +00:00
d24d4609b6 Migrate location reconciliation to shared engine 2026-08-09 16:51:29 +00:00
c7f79fb38e Migrate item reconciliation to shared engine 2026-08-09 16:46:43 +00:00
8c071800cf Migrate NPC reconciliation to shared engine 2026-08-09 16:38:43 +00:00
569e12c6f4 Add generic reconciliation plan application 2026-08-09 16:27:36 +00:00
5b6eb591b2 Add shared semantic reconciliation engine 2026-08-09 16:19:54 +00:00
b630384aa0 Add generic semantic reconciliation prompt assets 2026-08-09 16:08:53 +00:00
297d58f090 Add semantic reconciliation proposal validation 2026-08-09 16:00:01 +00:00
ee71dc4937 Add bounded semantic candidate preparation 2026-08-09 15:52:55 +00:00
65e5d65d14 Record semantic reconciliation architecture decision 2026-08-09 15:43:05 +00:00
b40b40aaf3 Plan semantic reconciliation improvements 2026-08-09 15:38:10 +00:00
f120be1cb4 Add proposed changes to shared LLM prompts 2026-08-09 09:18:36 -05:00
17673d74ea Archive the completed codebase audit 2026-08-09 13:24:12 +00:00
ef19a03cbf Reconcile remediation documentation 2026-08-09 02:45:22 +00:00
0546f6eb4f Simplify module cleanup paths 2026-08-09 02:40:25 +00:00
d28d1062e0 Reuse canonical item occurrence evidence 2026-08-09 02:36:53 +00:00
b3ebfcef37 Index enemy event duplicate identities 2026-08-09 02:31:28 +00:00
b70d9f77e3 Improve D&D registry normalization efficiency 2026-08-09 02:26:49 +00:00
2a75f40871 Bound D&D normalization diagnostics 2026-08-09 02:22:23 +00:00
a705ba74a1 Project spell aliases into extraction prompts 2026-08-09 02:17:15 +00:00
8d9c9e7c87 Align item occurrence evidence fields 2026-08-09 02:11:27 +00:00
0b5cc4f251 Require chunk-local extraction evidence 2026-08-09 02:08:11 +00:00
82ffe85f2d Enforce durable enemy event validation 2026-08-09 02:03:27 +00:00
b3644abc0e Enforce durable D&D evidence ranges 2026-08-09 01:54:43 +00:00
d653bf1b90 Share immutable in-memory filesystems 2026-08-09 01:47:44 +00:00
3e66127b94 Write durable outputs through confined file writer 2026-08-09 01:41:37 +00:00
5d086c13ca Cache compiled JSON schemas per validator 2026-08-09 01:38:42 +00:00
d36d4e7689 Avoid redundant decoded graph clones 2026-08-09 01:34:26 +00:00
1456aa51cc Index generated reference handoffs 2026-08-09 01:30:10 +00:00
ffc179c822 Centralize builder request cloning 2026-08-09 01:27:28 +00:00
8e669a1f14 Preserve terminal rejection warnings 2026-08-09 01:19:55 +00:00
14bfae216d Isolate typed validator candidates 2026-08-09 01:11:03 +00:00
5a58d87995 Require candidate decoders for artifact codecs 2026-08-09 01:03:50 +00:00
557809f364 Add candidate artifact codec decoding 2026-08-09 00:57:03 +00:00
ee600975f0 Prevent scheduler callbacks after cancellation 2026-08-09 00:52:31 +00:00
0d8017e23f Gate runner dispatches on cancellation 2026-08-09 00:50:02 +00:00
37b18edf3d Contain provider errors at the LLM adapter 2026-08-09 00:44:38 +00:00
cda7a61b47 Encode checkpoint and debug path identities 2026-08-09 00:41:28 +00:00
2ad9283148 Bound external reference reads and fingerprints 2026-08-09 00:33:33 +00:00
41a8a80dda Reject ambiguous configuration and reference bindings 2026-08-09 00:29:04 +00:00
90c7fa6381 Finalize codebase audit synthesis 2026-08-08 23:00:28 +00:00
e3839f8620 Audit combat and enemy event processing 2026-08-08 22:54:00 +00:00
0fc2f9ee01 Audit spell and scene processing 2026-08-08 22:42:49 +00:00
5d6305f21a Audit NPC item and location occurrences 2026-08-08 22:30:23 +00:00
551e4daea2 Audit NPC item and location registries 2026-08-08 22:18:29 +00:00
3589d33468 Audit shared D&D family conventions 2026-08-08 22:08:24 +00:00
a22c1a7f59 Audit generic and Seriatim modules 2026-08-08 21:59:06 +00:00
ad85d71b0f Audit LLM runtime and prompt assets 2026-08-08 21:47:51 +00:00
f3506240c2 Audit state persistence and file safety 2026-08-08 21:37:57 +00:00
70c199aa31 Audit runtime execution and concurrency 2026-08-08 21:27:37 +00:00
e2b82746ab Audit reference materialization and ordered handoffs 2026-08-08 21:16:30 +00:00
4235507f7b Audit pipeline composition and typed registries 2026-08-08 21:06:28 +00:00
b346670cc7 Audit configuration and CLI composition 2026-08-08 20:56:50 +00:00
7868c26be7 Establish codebase audit baseline 2026-08-08 20:50:15 +00:00
92e89076a2 Add an audit plan and staged audit sequence to identify opportunities for code quality improvement 2026-08-08 20:42:35 +00:00
d9b87347b8 Simplify contextual entity grounding 2026-08-08 15:47:41 +00:00
20397ef710 Document deterministic entity identity resolution 2026-08-08 15:08:57 +00:00
fc449863f2 Use contextual descriptors for entity reconciliation 2026-08-08 15:05:35 +00:00
51d62de1f3 Ground location occurrences with contextual selectors 2026-08-08 14:56:43 +00:00
fc76805075 Add contextual location grounding 2026-08-08 14:49:08 +00:00
8e680cf96e Ground item occurrences by canonical names 2026-08-08 14:43:04 +00:00
ece1bca460 Ground NPC occurrences by canonical names 2026-08-08 14:37:54 +00:00
516af12916 Finish D&D entity contract cleanup 2026-08-08 13:14:42 +00:00
1015d61b2d Clarify NPC mention eligibility 2026-08-06 13:40:42 +00:00
4192aa8584 Harden item occurrence grounding 2026-08-06 13:37:21 +00:00
8cf03a2a44 Reject unsafe currency reconciliation proposals 2026-08-06 13:31:15 +00:00
2ee6b495e1 Consolidate duplicate item registry records 2026-08-06 13:28:12 +00:00
f1b120b590 Complete D&D entity contract audit 2026-08-05 20:17:20 +00:00
2ec17f5b4f Consolidate D&D entity documentation 2026-08-05 20:12:16 +00:00
6de470d541 Verify complete D&D entity handoffs 2026-08-05 20:07:31 +00:00
a6e176e160 Move item occurrences to canonical namespace 2026-08-05 20:00:20 +00:00
3dfefd0e14 Adopt registry-backed item occurrences 2026-08-05 19:55:34 +00:00
f91237e9c0 Register item registry normalization 2026-08-05 19:39:06 +00:00
ad9ee076b5 Add item registry extraction and validation 2026-08-05 19:27:49 +00:00
ce04387dbc Add item registry domain foundation 2026-08-05 19:19:58 +00:00
e8965ebbbb Move location registry modules to canonical namespace 2026-08-05 19:13:37 +00:00
c006b163d5 Adopt location registry durable contract 2026-08-05 19:05:42 +00:00
2f61118e78 Move NPC occurrences to their canonical namespace 2026-08-05 19:00:55 +00:00
5e2ccffc0f Adopt registry-backed NPC occurrence artifacts 2026-08-05 18:55:58 +00:00
3f4a1f2647 Move NPC registry to canonical namespace 2026-08-05 18:38:22 +00:00
9653e06297 Migrate NPC registry durable contract 2026-08-05 18:25:04 +00:00
916a32195b Plan canonical D&D entity registries and occurrences 2026-08-05 18:16:37 +00:00
299c110267 Tighten D&D prompt contracts and tests 2026-08-05 17:38:51 +00:00
4093ff2e8b Document D&D prompt conventions and verification 2026-08-05 15:05:24 +00:00
39563f3ea0 Normalize private D&D response schema identities 2026-08-05 14:59:03 +00:00
4b52cace76 Consolidate grounded event extraction instructions 2026-08-05 14:55:40 +00:00
16d1b29b14 Consolidate spell and combat turn instructions 2026-08-05 14:52:26 +00:00
856b26b718 Consolidate NPC location and item event instructions 2026-08-05 14:48:55 +00:00
ab70347c5d Consolidate scene and normalization prompt instructions 2026-08-05 14:42:54 +00:00
8ed99ceaee Standardize D&D prompt asset filenames 2026-08-05 14:37:18 +00:00
257bca0dc2 Define D&D prompt transcript scopes 2026-08-05 14:33:00 +00:00
bad5db3ef3 Plan the D&D prompt asset cleanup 2026-08-05 14:28:05 +00:00
406ad1d362 Retire the completed asset migration roadmaps 2026-08-05 13:23:00 +00:00
5f207b2ab1 Document centralized LLM asset ownership 2026-08-05 00:49:15 +00:00
c613b306ae Centralize item and enemy event assets 2026-08-05 00:45:04 +00:00
1c7291e17d Centralize spell and combat-turn assets 2026-08-05 00:43:39 +00:00
2345da106a Centralize location LLM assets 2026-08-05 00:42:22 +00:00
03761c97dd Centralize NPC LLM assets 2026-08-05 00:40:47 +00:00
9cb9ee48a7 Centralize scene planning and description assets 2026-08-05 00:38:55 +00:00
08954f17e2 Centralize shared D&D LLM assets 2026-08-05 00:37:19 +00:00
a57f83e30d Centralize generic LLM schema assets 2026-08-05 00:34:56 +00:00
f4c05c34ef Make item event responses compatible with strict schemas 2026-08-04 19:52:47 +00:00
29fcad6e9b Improve D&D registry caching and retire the completed roadmap 2026-08-04 18:29:17 +00:00
f5fd115046 Migrate location registry to shared resolver 2026-08-04 13:30:12 +00:00
7f28899730 Migrate NPC registry to shared resolver 2026-08-04 13:26:06 +00:00
84c0758455 Add shared D&D registry resolver 2026-08-04 13:20:20 +00:00
5002864e88 Narrow location occurrence normalizer references 2026-08-04 13:12:46 +00:00
55b188fd84 Clarify hypothetical location occurrence classification 2026-08-04 13:09:30 +00:00
d1f43df88e Restore NPC canonical name selection 2026-08-04 13:08:00 +00:00
d52387c1f7 Document D&D location tracking contracts 2026-08-04 00:51:43 +00:00
9c5e3cff14 Add D&D location tracking to complete example 2026-08-04 00:45:50 +00:00
811d5b8bd9 Compose D&D location tracking modules 2026-08-04 00:39:00 +00:00
a168c13b85 Add D&D location occurrence validators 2026-08-04 00:33:39 +00:00
dd61a4efda Add D&D location validators 2026-08-04 00:26:50 +00:00
228cc6ee83 Add D&D location occurrence normalizer 2026-08-04 00:22:25 +00:00
06170e1f65 Add D&D location occurrence extractor 2026-08-04 00:18:16 +00:00
7715baa1f6 Add immutable D&D location registry 2026-08-04 00:11:31 +00:00
98506db1a9 Add D&D location normalizer 2026-08-04 00:07:47 +00:00
bb4855f0c6 Add D&D location extractor 2026-08-04 00:02:18 +00:00
c51934d5c6 Migrate NPC normalization to shared reconciliation 2026-08-03 23:56:48 +00:00
c3513da880 Add shared D&D entity reconciliation support 2026-08-03 23:47:42 +00:00
c7d853ea52 Add D&D location artifact codecs 2026-08-03 23:40:35 +00:00
da7fcdaffd Add D&D location identity contracts 2026-08-03 23:35:50 +00:00
508 changed files with 28235 additions and 8560 deletions

View File

@@ -2,8 +2,9 @@
Notarius is a Go CLI for turning source material into structured artifacts with Notarius is a Go CLI for turning source material into structured artifacts with
configured extraction pipelines. The implemented D&D workflow reads Seriatim configured extraction pipelines. The implemented D&D workflow reads Seriatim
transcript JSON and can produce scene descriptions, item and currency events, transcript JSON and can produce NPC, location, and item registries; their
NPC identities, combat turns, NPC interactions, enemy events, and spell casts. source-grounded occurrences; scene descriptions, combat turns, enemy events,
and spell casts.
## Quickstart ## Quickstart

View File

@@ -0,0 +1,18 @@
Extract Dungeons & Dragons combat-turn artifacts from the supplied transcript.
Include a record only when the transcript establishes that an in-world
participant takes a combat turn or performs a discrete interrupting combat
event. Keep events in transcript chronology; place an interrupting event where
it occurs.
Exclude initiative setup without a turn or combat event, tactical planning,
table talk, rules lookup, hypothetical events, abandoned intentions, recaps
outside the current passage, and downstream consequences. Do not infer combat
events from Dungeons & Dragons rules knowledge. Preserve the session as played
and attribute relevant nonstandard rulings to the GM or table. Unmatched actors
remain permitted.
Treat each record as one turn-level event and keep its supporting transcript
evidence together. Use `turn` for a regular combat turn, `reaction` for an
off-turn reaction, `legendary_action` for a legendary action,
`lair_action` for a lair action, and `other` for another discrete combat
event that does not fit those categories.

View File

@@ -14,7 +14,7 @@ inputs:
- name: glossary - name: glossary
required: false required: false
content_type: text/plain content_type: text/plain
- name: npcs - name: npc_registry
required: false required: false
content_type: application/json content_type: application/json
messages: messages:
@@ -27,15 +27,13 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user - role: user
content_file: ./sharedassets/common-dnd-npcs.md content_file: ./sharedassets/common-dnd-npc-registry.md
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:

View File

@@ -9,4 +9,4 @@ Combat-turn grounding:
Named combat-opponent grounding: Named combat-opponent grounding:
{{ input "npc_interactions" }} {{ input "npc_occurrences" }}

View File

@@ -0,0 +1,25 @@
Extract Dungeons & Dragons enemy events from the supplied combat transcript.
An `engaged` event requires direct establishment that a subject is actively
opposing the party in combat. A `killed`, `fled`, `captured`, or
`incapacitated` event requires explicit establishment of that outcome. An
outcome may share evidence with an engagement, and a later engagement or
outcome for the same subject remains a separate observation. Emit at most one
`engaged` observation for the same subject in this combat scene.
For `killed`, direct death or killing is required. For `fled`, the subject
must explicitly escape, retreat, or leave combat to avoid continued engagement.
For `captured`, the subject must be explicitly taken prisoner or secured
under the party's control. For `incapacitated`, the subject must be explicitly
unable to continue acting without being established as killed or captured.
When the transcript identifies a named NPC, use its normalized registry
spelling. A hostile creature without a registry entry is allowed. For unnamed
individuals or groups, use only the narrowest transcript-grounded label, such
as `Orcs`, `One orc`, or `Remaining orcs`; never invent member names, IDs,
or quantities.
Exclude party members, allies, neutral observers, mentioned-but-absent enemies,
hazards, traps, environmental effects, uncertain allegiance, table talk,
planning, hypotheses, recaps outside this passage, and downstream inference.
Do not infer an engagement or outcome from initiative, turn absence, damage,
defeat, movement, or a scene ending.

View File

@@ -14,13 +14,13 @@ inputs:
- name: glossary - name: glossary
required: false required: false
content_type: text/plain content_type: text/plain
- name: npcs - name: npc_registry
required: true required: true
content_type: application/json content_type: application/json
- name: combat_turns - name: combat_turns
required: true required: true
content_type: application/json content_type: application/json
- name: npc_interactions - name: npc_occurrences
required: true required: true
content_type: application/json content_type: application/json
messages: messages:
@@ -33,17 +33,15 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user - role: user
content_file: ./sharedassets/common-dnd-npcs.md content_file: ./sharedassets/common-dnd-npc-registry.md
- role: user - role: user
content_file: ./grounding.md content_file: ./combat-grounding.md
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:

View File

@@ -0,0 +1,30 @@
Extract meaningful Dungeons & Dragons item and currency occurrences: discoveries and changes
in party possession established by the transcript. This is an occurrence history,
not an inventory or ledger: do not calculate balances, resolve item identity
across records, or infer ownership that the transcript does not establish.
For every occurrence, use the supplied canonical item `name`. Record a stated
quantity as an integer and leave it null when the transcript does not state
one. Preserve the stated currency denomination through the selected canonical
registry name.
Use `discovered` when the party learns of or encounters an item without
establishing possession. Use `acquired` when the party or a party member gains
possession. Use `lost` when party possession ends through a gift, sale, payment,
theft, abandonment, or destruction not caused by intended use. Use `consumed`
when intended use depletes an expendable item. Monetary spending, purchases, and
payments are always `lost`, not `consumed`. Classify currency as `consumed` only
when the transcript explicitly describes it being physically destroyed or
expended as a non-payment component. Use `transferred` only when possession
moves between two distinct named party members.
Return both `from` and `to` for every occurrence, using `null` when a holder does not
apply. For `discovered`, set both holders to `null`. For `acquired`, set `from`
to `null` and provide `to`; for `lost` and `consumed`, provide `from` and set
`to` to `null`; and for `transferred`, provide both holders. Use `party` only
for collective or unresolved party possession, never for either side of a
transfer. Do not emit a transfer for a gift, sale, or payment outside the party.
Ordinary non-depleting use is not an occurrence. Do not infer acquisition from a
discovery, or discovery from an acquisition: emit both only when each is
independently established.

View File

@@ -0,0 +1,6 @@
Use the supplied item registry only to ground each occurrence. Every record
must use one registry item's canonical `name`; do not invent, rename, merge,
or infer registry items. The registry is not transcript evidence: cite only the
current transcript chunk in `source_refs`.
{{ input "item_registry" }}

View File

@@ -1,4 +1,4 @@
id: dnd.npc_interactions id: dnd.item_occurrences
version: "v1" version: "v1"
default_profile: dnd-extraction default_profile: dnd-extraction
inputs: inputs:
@@ -14,7 +14,7 @@ inputs:
- name: glossary - name: glossary
required: false required: false
content_type: text/plain content_type: text/plain
- name: npcs - name: item_registry
required: true required: true
content_type: application/json content_type: application/json
messages: messages:
@@ -27,15 +27,13 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user - role: user
content_file: ./sharedassets/common-dnd-npcs.md content_file: ./item-registry.md
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:
@@ -43,5 +41,5 @@ messages:
output: output:
format: json format: json
validation_mode: json_schema validation_mode: json_schema
schema_path: dnd_npc_interactions_llm.v1.json schema_path: dnd_item_occurrences_llm.v1.json
repair_attempts: 0 repair_attempts: 0

View File

@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.item_occurrences.llm",
"type": "object",
"additionalProperties": false,
"required": ["occurrences"],
"properties": {
"occurrences": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "kind", "quantity", "from", "to", "source_refs"],
"properties": {
"name": {"type": "string"},
"kind": {"type": "string"},
"quantity": {"type": ["integer", "null"]},
"from": {"type": ["string", "null"]},
"to": {"type": ["string", "null"]},
"source_refs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["start_unit_id", "end_unit_id"],
"properties": {
"start_unit_id": {"type": "integer"},
"end_unit_id": {"type": "integer"}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
Extract only items established by the provided Dungeons & Dragons transcript.
Include named unique items, concrete reusable item types, and stable unique
designations. Record each currency denomination separately when it is
established, such as copper pieces, silver pieces, gold pieces, or platinum
pieces. Do not use capitalization as an eligibility test. Keep distinct names
and designations as separate candidates; do not merge aliases or invent
qualifiers.
Do not record vague categories such as "loot", "treasure", or "some gear";
generic weapons; inferred properties; quantities; or inferred uniqueness. Omit
uncertain or unsupported items.

View File

@@ -1,4 +1,4 @@
id: dnd.npcs id: dnd.item_registry
version: "v1" version: "v1"
default_profile: dnd-extraction default_profile: dnd-extraction
inputs: inputs:
@@ -24,13 +24,11 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:
@@ -38,5 +36,5 @@ messages:
output: output:
format: json format: json
validation_mode: json_schema validation_mode: json_schema
schema_path: dnd_npcs_llm.v1.json schema_path: dnd_item_registry_llm.v1.json
repair_attempts: 0 repair_attempts: 0

View File

@@ -1,31 +1,27 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.item_events.llm", "$id": "notarius.dnd.item_registry.llm",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["events"], "required": ["items"],
"properties": { "properties": {
"events": { "items": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["name", "kind", "source_refs"], "required": ["name", "source_refs"],
"properties": { "properties": {
"name": {"type": "string"}, "name": {"type": "string"},
"kind": {"type": "string"},
"quantity": {"type": "integer"},
"from": {"type": "string"},
"to": {"type": "string"},
"source_refs": { "source_refs": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["start_segment", "end_segment"], "required": ["start_unit_id", "end_unit_id"],
"properties": { "properties": {
"start_segment": {"type": "integer"}, "start_unit_id": {"type": "integer"},
"end_segment": {"type": "integer"} "end_unit_id": {"type": "integer"}
} }
} }
} }

View File

@@ -0,0 +1,9 @@
Determine whether candidates identify the same item type or unique designation
using their contextual labels and cited transcript windows. Do not treat nearby
evidence, similar objects, or a shared owner as sufficient.
Keep currency denominations and materially different item types separate. Keep
uncertain aliases separate. Do not infer an item property or uniqueness.
When selecting a canonical display name, choose one supplied candidate name
that is the clearest established designation.

View File

@@ -0,0 +1,30 @@
id: dnd.item_registry.normalize
version: "v1"
default_profile: dnd-extraction
inputs:
- name: candidates
required: true
content_type: application/json
- name: transcript
required: true
content_type: application/json
messages:
- role: system
content_file: ./sharedassets/common-dnd-system.md
- role: user
content_file: ./sharedassets/protocol.md
- role: user
content_file: ./instructions.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/candidates.md
- role: user
content_file: ./sharedassets/transcript-windows.md
cache_control:
type: ephemeral
output:
format: json
validation_mode: json_schema
schema_path: semantic_reconciliation_llm.v1.json
repair_attempts: 0

View File

@@ -0,0 +1,34 @@
Extract Dungeons & Dragons location occurrences from the supplied transcript.
Include an occurrence only when the transcript establishes one supplied
location, one occurrence kind, and a coherent passage supporting both.
Use exactly one kind per occurrence:
- visited: party members are physically present, arrive, remain, or depart;
- planned: the party explicitly proposes, intends, or agrees to future travel;
- recalled: the transcript explicitly recounts an earlier party visit; or
- mentioned: the location is explicitly referenced without stronger support,
including non-actionable speculation or a mere hypothetical reference.
A mere hypothetical or speculative reference is not planned unless the
transcript also establishes an actual proposal, intention, or agreement to
travel. When the hypothetical explicitly names a supplied location, it may be
mentioned.
A generic phrase in the current chunk may refer to a supplied named registry
location only when the chunk's context supports that coreference. It must not
create a registry location, and registry content or provenance must never
replace current-chunk evidence.
For every occurrence, return the exact selector from the location registry:
the canonical `name`, plus an empty `registry_refs` array for a unique name or
the complete ordered `registry_refs` array for a repeated name. Registry ranges
and context identify the location only; they are not occurrence evidence.
For overlapping support, visited outranks planned, recalled, and mentioned;
planned outranks recalled and mentioned; recalled outranks mentioned. A passage
may produce multiple records when it independently establishes separate facts,
such as recalling an earlier visit while planning a return. Omit inferred,
unstated, uncertain, or unsupported places and occurrences. Do not infer a
location or occurrence from surrounding events when the transcript does not
state it. Do not summarize location descriptions.

View File

@@ -0,0 +1,11 @@
A contextual location registry is provided below for identity grounding. It may
be empty. Every record supplies a canonical display name. A name that appears
once is selected with that name and an empty `registry_refs` array. A repeated
name is selected only by copying both its name and its complete, ordered
`registry_refs` array exactly as supplied.
Registry content is context, not occurrence evidence. Do not derive an
occurrence or `source_refs` range from the registry. Do not invent a location
or selector that is absent from it.
{{ input "location_registry" }}

View File

@@ -0,0 +1,45 @@
id: dnd.location_occurrences
version: "v1"
default_profile: dnd-extraction
inputs:
- name: transcript
required: true
content_type: application/json
- name: players
required: false
content_type: text/plain
- name: party
required: false
content_type: text/plain
- name: glossary
required: false
content_type: text/plain
- name: location_registry
required: true
content_type: application/json
messages:
- role: system
content_file: ./sharedassets/common-dnd-system.md
- role: user
content_file: ./sharedassets/common-dnd-identity.md
- role: user
content_file: ./sharedassets/common-dnd-references.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./location-registry.md
- role: user
content_file: ./instructions.md
cache_control:
type: ephemeral
output:
format: json
validation_mode: json_schema
schema_path: dnd_location_occurrences_llm.v1.json
repair_attempts: 0

View File

@@ -0,0 +1,45 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.location_occurrences.llm",
"type": "object",
"additionalProperties": false,
"required": ["occurrences"],
"properties": {
"occurrences": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "registry_refs", "kind", "source_refs"],
"properties": {
"name": {"type": "string"},
"registry_refs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["start_unit_id", "end_unit_id"],
"properties": {
"start_unit_id": {"type": "integer", "minimum": 1},
"end_unit_id": {"type": "integer", "minimum": 1}
}
}
},
"kind": {"enum": ["visited", "planned", "recalled", "mentioned"]},
"source_refs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["start_unit_id", "end_unit_id"],
"properties": {
"start_unit_id": {"type": "integer"},
"end_unit_id": {"type": "integer"}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
Extract only physical places established by the provided Dungeons & Dragons
transcript that have a stable proper name or unique in-world designation. This
includes named planes, regions, settlements, districts, buildings, rooms,
landmarks, routes, and geographic features.
Do not create a registry location for generic, temporary, relative, or merely
descriptive phrases, including "the room", "the bar", "the hallway",
"outside", and "upstairs". Do not use capitalization as an eligibility test.
Keep aliases and nested places when the transcript identifies them; do not merge
or invent qualifiers for similarly named places.
Exclude people, creatures, objects, organizations, abstract concepts, and
places merely inferred from an event. Omit uncertain or unsupported places.

View File

@@ -1,4 +1,4 @@
id: dnd.item_events id: dnd.location_registry
version: "v1" version: "v1"
default_profile: dnd-extraction default_profile: dnd-extraction
inputs: inputs:
@@ -24,13 +24,11 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:
@@ -38,5 +36,5 @@ messages:
output: output:
format: json format: json
validation_mode: json_schema validation_mode: json_schema
schema_path: dnd_item_events_llm.v1.json schema_path: dnd_location_registry_llm.v1.json
repair_attempts: 0 repair_attempts: 0

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.location_registry.llm",
"type": "object",
"additionalProperties": false,
"required": ["locations"],
"properties": {
"locations": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "source_refs"],
"properties": {
"name": {"type": "string"},
"source_refs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["start_unit_id", "end_unit_id"],
"properties": {
"start_unit_id": {"type": "integer"},
"end_unit_id": {"type": "integer"}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
Determine whether candidates identify the same physical place using their
contextual labels and cited transcript windows. Do not treat matching names,
nearby evidence, nested places, or generic labels as sufficient.
Keep parent and child places separate, as well as similarly named places and
uncertain aliases.
When selecting a canonical display name, prefer the clearest established name.

View File

@@ -0,0 +1,30 @@
id: dnd.location_registry.normalize
version: "v1"
default_profile: dnd-extraction
inputs:
- name: candidates
required: true
content_type: application/json
- name: transcript
required: true
content_type: application/json
messages:
- role: system
content_file: ./sharedassets/common-dnd-system.md
- role: user
content_file: ./sharedassets/protocol.md
- role: user
content_file: ./instructions.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/candidates.md
- role: user
content_file: ./sharedassets/transcript-windows.md
cache_control:
type: ephemeral
output:
format: json
validation_mode: json_schema
schema_path: semantic_reconciliation_llm.v1.json
repair_attempts: 0

View File

@@ -1,6 +1,14 @@
Return the interactions array even when no interaction is established. Every Extract Dungeons & Dragons NPC occurrences from the supplied
record must contain name, kind, and source_refs. Cite transcript ranges that transcript. Include an occurrence only when the transcript establishes one
support both the NPC identity and the interaction kind. supplied NPC, one occurrence kind, and a coherent passage supporting both.
Use the supplied canonical NPC `name`; never invent or substitute a similar
name. Cite current-transcript evidence for every occurrence.
Do not summarize, infer relationships, sentiment, factions, motives, aliases,
or persistent state. Do not identify player characters, anonymous groups, or
invented NPCs. Split records when an NPC's occurrence kind changes, when
combat alignment changes, or when an NPC is first mentioned and later becomes
present.
Use exactly one kind per occurrence: Use exactly one kind per occurrence:

View File

@@ -0,0 +1,45 @@
id: dnd.npc_occurrences
version: "v1"
default_profile: dnd-extraction
inputs:
- name: transcript
required: true
content_type: application/json
- name: players
required: false
content_type: text/plain
- name: party
required: false
content_type: text/plain
- name: glossary
required: false
content_type: text/plain
- name: npc_registry
required: true
content_type: application/json
messages:
- role: system
content_file: ./sharedassets/common-dnd-system.md
- role: user
content_file: ./sharedassets/common-dnd-identity.md
- role: user
content_file: ./sharedassets/common-dnd-references.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./sharedassets/common-dnd-npc-registry.md
- role: user
content_file: ./instructions.md
cache_control:
type: ephemeral
output:
format: json
validation_mode: json_schema
schema_path: dnd_npc_occurrences_llm.v1.json
repair_attempts: 0

View File

@@ -1,11 +1,11 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.npc_interactions.llm", "$id": "notarius.dnd.npc_occurrences.llm",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["interactions"], "required": ["occurrences"],
"properties": { "properties": {
"interactions": { "occurrences": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",

View File

@@ -0,0 +1,19 @@
Extract the individually identifiable Dungeons & Dragons non-player characters
established by the provided transcript.
Include an in-world non-PC only when the transcript factually establishes a
proper name or a stable, individually distinguishing title or alias. A factual
third-party mention establishes that identity even when the NPC is not
physically present, does not speak, and takes no direct action in this chunk.
Record only the NPC identity and the transcript evidence that establishes it;
do not infer or classify a separate occurrence.
Exclude human players, transcript speakers, and the GM as out-of-world people;
player characters identified by the player or party references; names used only
in hypothetical, speculative, or imagined examples; corrected transcription
mistakes; anonymous or generic roles; indistinguishable crowds or groups;
invented descriptive labels; and temporary summoned creatures or spell effects
without a persistent individual identity.
Preserve observed display spelling. Do not invent a label for an anonymous
creature, crowd, or generic role.

View File

@@ -0,0 +1,40 @@
id: dnd.npc_registry
version: "v1"
default_profile: dnd-extraction
inputs:
- name: transcript
required: true
content_type: application/json
- name: players
required: false
content_type: text/plain
- name: party
required: false
content_type: text/plain
- name: glossary
required: false
content_type: text/plain
messages:
- role: system
content_file: ./sharedassets/common-dnd-system.md
- role: user
content_file: ./sharedassets/common-dnd-identity.md
- role: user
content_file: ./sharedassets/common-dnd-references.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./instructions.md
cache_control:
type: ephemeral
output:
format: json
validation_mode: json_schema
schema_path: dnd_npc_registry_llm.v1.json
repair_attempts: 0

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.npcs.llm", "$id": "notarius.dnd.npc_registry.llm",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["npcs"], "required": ["npcs"],

View File

@@ -0,0 +1,11 @@
Determine whether candidates refer to the same individual using their
contextual labels and cited transcript windows. Preserve distinct individuals
even when their names are similar or their contextual descriptions are
identical.
When selecting a canonical display name, prefer a complete, stable proper name
over an abbreviation. Prefer an unadorned proper name over that name plus a
contextual class, role, title, or relationship descriptor unless the transcript
establishes the descriptor as part of the person's name. A longer display name
is not inherently more canonical; for example, do not prefer `Captain Aria`
over `Aria` solely because it includes the contextual title `Captain`.

View File

@@ -1,4 +1,4 @@
id: dnd.npcs.normalize id: dnd.npc_registry.normalize
version: "v1" version: "v1"
default_profile: dnd-extraction default_profile: dnd-extraction
inputs: inputs:
@@ -12,19 +12,19 @@ messages:
- role: system - role: system
content_file: ./sharedassets/common-dnd-system.md content_file: ./sharedassets/common-dnd-system.md
- role: user - role: user
content_file: ./task.md content_file: ./sharedassets/protocol.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./candidates.md content_file: ./sharedassets/candidates.md
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/transcript-windows.md
cache_control: cache_control:
type: ephemeral type: ephemeral
output: output:
format: json format: json
validation_mode: json_schema validation_mode: json_schema
schema_path: dnd_npcs_normalize_llm.v1.json schema_path: semantic_reconciliation_llm.v1.json
repair_attempts: 0 repair_attempts: 0

View File

@@ -1,4 +1,9 @@
Choose exactly one kind: Describe exactly one accepted Dungeons & Dragons scene from the supplied
transcript chunk. The complete chunk is the evidence boundary: do not split it
into multiple scenes or use facts that are not supported by it.
Return one kind, one concise title, and one concise summary. Choose exactly one
kind:
- combat: active combat materially organizes the scene, including - combat: active combat materially organizes the scene, including
initiative-like exchanges or sustained hostile action. Planning a fight or initiative-like exchanges or sustained hostile action. Planning a fight or
@@ -37,8 +42,4 @@ must not invent a proper noun.
The summary must briefly state the main activity and material transition or The summary must briefly state the main activity and material transition or
outcome established within the accepted chunk. Do not add analysis, inferred outcome established within the accepted chunk. Do not add analysis, inferred
motives, hidden state, future consequences, relationship claims, or facts from motives, hidden state, future consequences, relationship claims, or facts from
outside the chunk. Campaign references may disambiguate names but never add outside the chunk.
events or lore.
Do not return identifiers, source identifiers, source ranges, unit identifiers,
participants, confidence, or any fields besides kind, title, and summary.

View File

@@ -24,11 +24,9 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:

View File

@@ -1,5 +1,5 @@
Divide the provided transcript into coherent Dungeons & Dragons scenes for the Divide the complete provided transcript into coherent Dungeons & Dragons scenes
`dnd/scenes` chunk module. for the `dnd/scenes` chunk module.
A scene is a coherent unit of play. Start a new scene when the transcript A scene is a coherent unit of play. Start a new scene when the transcript
establishes a meaningful change in location, objective, threat, activity, establishes a meaningful change in location, objective, threat, activity,
@@ -13,6 +13,7 @@ Do not split a scene merely because a speaker or combat round changes, a
routine turn occurs, or the table briefly digresses. Prefer fewer coherent routine turn occurs, or the table briefly digresses. Prefer fewer coherent
scenes over speculative or fine-grained boundaries. scenes over speculative or fine-grained boundaries.
Return only inclusive `start_unit_id` and `end_unit_id` endpoints for each Cover the complete transcript from its first source unit to its last. Return
scene. Do not return titles, modes, participants, summaries, boundary notes, scenes in source-unit order with no gaps or overlaps. Use only positive integer
confidence, caveats, final chunk IDs, or chunk indexes. source-unit IDs from the transcript, and give every scene one inclusive
`start_unit_id` and one inclusive `end_unit_id`.

View File

@@ -21,16 +21,14 @@ messages:
content_file: ./sharedassets/common-dnd-references.md content_file: ./sharedassets/common-dnd-references.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-full.md
cache_control: cache_control:
type: ephemeral type: ephemeral
output: output:
format: json format: json
validation_mode: json_schema validation_mode: json_schema
schema_path: dnd_scenes.v1.json schema_path: dnd_scenes_llm.v1.json
repair_attempts: 0 repair_attempts: 0

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.scenes", "$id": "notarius.dnd.scenes.llm",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["scenes"], "required": ["scenes"],

View File

@@ -0,0 +1,6 @@
Transcript units are the only evidence for extracted events and factual claims.
Every reported factual claim must be supported by cited transcript units. Use
integer `start_unit_id` and `end_unit_id` values from the transcript.
When supporting evidence is non-contiguous, use multiple narrow ranges rather
than a broad range that bridges unrelated conversation.

View File

@@ -7,4 +7,4 @@ participants, effects, or source references from the registry. Registry source
references describe registry provenance and may belong to another session; they references describe registry provenance and may belong to another session; they
are never evidence for the current transcript. are never evidence for the current transcript.
{{ input "npcs" }} {{ input "npc_registry" }}

View File

@@ -0,0 +1,5 @@
You process Dungeons & Dragons gameplay transcripts.
As input, you will receive one or more portions of a transcript. The transcript may contain transcription errors, repeated lines, incomplete sentences, and misheard proper nouns.
Return exactly one JSON object that conforms to the configured response schema, with no explanatory prose.

View File

@@ -0,0 +1,3 @@
One extraction chunk from a Dungeons & Dragons gameplay transcript is provided below. Report and infer only what is within this chunk. Its unit IDs retain their source-wide meaning.
{{ input "transcript" }}

View File

@@ -0,0 +1,3 @@
The complete ordered transcript of this Dungeons & Dragons gameplay session is provided below.
{{ input "transcript" }}

View File

@@ -0,0 +1,14 @@
Extract Dungeons & Dragons spell-cast artifacts from the provided transcript.
Include an actual casting event or an unambiguous declared casting attempt.
Exclude spell mentions, hypothetical plans, rules discussion, and catalog
matches that do not establish a casting event in the transcript.
For every extracted cast, the transcript evidence must collectively support the
in-world caster, the spell, and the fact that the cast or declared attempt
occurred.
Attribute every cast to its in-world caster. Map first-person player speech to
the associated player character, and attribute a spell narrated by the GM to
the in-world creature that casts it. If the caster cannot be resolved, use only
the most specific in-world identity supported by the transcript; do not invent
a name.

View File

@@ -17,7 +17,7 @@ inputs:
- name: glossary - name: glossary
required: false required: false
content_type: text/plain content_type: text/plain
- name: npcs - name: npc_registry
required: false required: false
content_type: application/json content_type: application/json
messages: messages:
@@ -30,17 +30,15 @@ messages:
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-transcript.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user - role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user - role: user
content_file: ./sharedassets/common-dnd-npcs.md content_file: ./sharedassets/common-dnd-npc-registry.md
- role: user - role: user
content_file: ./catalog.md content_file: ./spell-catalog.md
- role: user
content_file: ./task.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:

View File

@@ -0,0 +1,6 @@
The spell catalog for this extraction is provided below as JSON. Each entry
lists a `canonical_name` and its recognized `aliases`. If the transcript uses
an alias, select that entry's `canonical_name`. Return spell names using the
canonical spelling exactly; never return an alias as a spell name.
{{ input "spell_catalog" }}

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.spells", "$id": "notarius.dnd.spells.llm",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["spell_casts"], "required": ["spell_casts"],

View File

@@ -0,0 +1,3 @@
Candidate material:
{{ input "candidates" }}

View File

@@ -0,0 +1,5 @@
Identify only high-confidence duplicate entities among the supplied candidates.
Preserve distinct entities even when their names are similar. Treat contextual descriptions and transcript evidence as supporting material, not as permission to merge ambiguous records.
When several records are duplicates, choose as canonical the candidate with the clearest stable identity. Prefer a complete proper name over an abbreviation, and prefer an unadorned proper name over one with incidental descriptors unless the evidence establishes those descriptors as part of the name. A longer name is not inherently more canonical.

View File

@@ -0,0 +1,27 @@
id: generic.semantic_reconciliation
version: "v1"
inputs:
- name: candidates
required: true
content_type: application/json
- name: transcript
required: true
content_type: application/json
messages:
- role: system
content_file: ./system.md
- role: user
content_file: ./protocol.md
- role: user
content_file: ./instructions.md
cache_control:
type: ephemeral
- role: user
content_file: ./candidates.md
- role: user
content_file: ./transcript-windows.md
output:
format: json
validation_mode: json_schema
schema_path: semantic_reconciliation_llm.v1.json
repair_attempts: 0

View File

@@ -0,0 +1,7 @@
Use only the positive integer `candidate_id` values supplied in the candidate material.
Return a duplicate group only when the evidence supports that every selected candidate describes the same underlying entity. Each group must contain at least two distinct candidate IDs, and its `canonical_candidate_id` must be one of those IDs. A candidate may appear in at most one group.
Omit uncertain matches and candidates that should remain distinct. Do not invent candidates or infer an ID from list position. An empty `duplicate_groups` array is valid.
The response must conform exactly to the selected JSON schema. Return IDs only: do not copy candidate names, evidence, transcript text, source identifiers, or source ranges into the response.

View File

@@ -0,0 +1,2 @@
You reconcile structured records that may describe the same underlying entity.
Follow the supplied protocol and return only the requested structured result.

View File

@@ -0,0 +1,3 @@
Transcript evidence windows:
{{ input "transcript" }}

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.generic.semantic_reconciliation.llm",
"title": "notarius_semantic_reconciliation_llm_v1",
"type": "object",
"additionalProperties": false,
"required": ["duplicate_groups"],
"properties": {
"duplicate_groups": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["candidate_ids", "canonical_candidate_id"],
"properties": {
"candidate_ids": {
"type": "array",
"minItems": 2,
"items": {
"type": "integer",
"minimum": 1
}
},
"canonical_candidate_id": {
"type": "integer",
"minimum": 1
}
}
}
}
}
}

15
assets/package.go Normal file
View File

@@ -0,0 +1,15 @@
// Package assets exposes embedded LLM-facing content.
package assets
import (
"embed"
"io/fs"
)
//go:embed dnd generic
var embedded embed.FS
// FS returns the embedded read-only asset filesystem.
func FS() fs.FS {
return embedded
}

View File

@@ -1,6 +1,6 @@
# ADR-0004: Package modules by domain, not by stage # ADR-0004: Package modules by domain, not by stage
**Status:** Accepted **Status:** Accepted — its asset-co-location rule is superseded by [ADR-0011](0011-centralize-llm-assets.md); its domain-first module packaging decision remains accepted.
**Date:** 2026-07-13 **Date:** 2026-07-13
## Context ## Context

View File

@@ -0,0 +1,69 @@
# ADR-0011: Centralize LLM-facing assets in a content-only package
**Status:** Accepted
**Date:** 2026-08-05
## Context
LLM prompts, private response schemas, generic schemas, and fallback profiles
are authored and reviewed as content, but package-local embedding scattered that
content across implementation trees. Finding all of the assets that contribute
to a prompt family required navigating code ownership boundaries rather than a
single discoverable content boundary.
The repository must retain module ownership of prompt semantics, schema
identities, registration, and prompt-cache behavior. Durable artifact schemas
and non-LLM domain data have different compatibility and ownership rules, so
they must not move merely because they are embedded files.
## Decision
LLM-facing content is embedded by the root `assets` package. It is a data-only
dependency leaf: its single `FS() fs.FS` API returns the read-only embedded
filesystem, and the package contains no business logic or internal or PromptKit
dependencies. The accepted import path is
`gitea.maximumdirect.net/eric/notarius/assets`; it makes repository-owned
content available to its consumers, not a public extension contract.
Consumers scope that filesystem to the subtree they own before reading or
registering content. Modules continue to own their manifests, prompt ordering,
private response-schema identity, and registration. Centralizing physical files
does not centralize domain semantics or transfer those responsibilities to the
root package.
The root package contains prompt content, private LLM response schemas, generic
LLM schemas, shared fragments, and fallback profiles. Durable artifact schemas
and non-LLM domain data remain with their current owners. A module fingerprint
is derived from its manifest-selected module and shared files, rather than from
an entire asset tree. The relocation is accepted to cause a one-time checkpoint
invalidation.
This decision supersedes only the physical asset-co-location portion of
ADR-0004's decision that places domain-specific prompt fragments and schemas
within the domain tree. ADR-0004's domain-first packaging and registrar
ownership decisions remain accepted.
## Alternatives Considered
- Keep package-local assets. This preserves physical co-location with code but
makes prompt-author discovery and cross-family review unnecessarily costly.
- Use `internal/llmassets`. This would hide content from legitimate owners
outside the `internal` subtree and would make the root asset boundary depend
on implementation-layer placement.
- Build a behavioral central registry. This would mix content discovery with
prompt selection and registration behavior, moving module semantics into a
shared registry.
- Use runtime filesystem overlays. This would add runtime configuration and
failure modes where compile-time embedded content is sufficient.
## Consequences
Prompt authors can find in-scope LLM content in one top-level tree while module
packages continue to define its meaning and registration. Consumers have an
explicit, narrow dependency on only the content they need. The root package is
intentionally importable but must remain a stable, content-only leaf rather
than becoming a general extension API.
The initial relocation invalidates existing checkpoints once. Later checkpoint
identity changes remain limited to the manifest-selected prompt and shared
content, so unrelated files do not trigger recomputation.

View File

@@ -0,0 +1,64 @@
# ADR-0012: Resolve opaque entity identifiers deterministically
**Status:** Accepted
**Date:** 2026-08-08
## Context
Entity IDs in durable Notarius artifacts are application-owned, deterministic
identifiers. They are useful to artifact consumers, but their hash-based form
does not help a model distinguish entities and would make the model reproduce
an opaque implementation detail. A plain name is likewise insufficient where
multiple supplied records share that name.
The LLM boundary must preserve the typed artifact and durable-schema ownership
of [ADR-0003](0003-typed-interfaces-with-two-zone-data-model.md) and the distinction
between disambiguating references and source evidence in
[ADR-0009](0009-minimal-evidence-grounded-extraction-artifacts.md).
## Decision
Callers present a model with semantic selections: a canonical name when it is
unique in the request, or a contextual descriptor containing the name and
source coordinates when that context is needed to distinguish supplied
records. The model returns only those supplied selections. The caller resolves
each accepted selection against the request-local supplied records and attaches
the opaque application ID deterministically.
Source coordinates are permitted in a selection solely as identity context.
They neither establish an occurrence fact nor replace that occurrence's
current-transcript evidence. A selector must resolve exactly; unknown,
ambiguous, partial, reordered, or otherwise unsafe selections are not mapped.
Where an operation requires a complete grounded artifact, that failure rejects
the complete artifact rather than accepting a partially mapped result.
An explicitly scoped request-local short label is permitted only when a
contextual descriptor would be impractical and the caller can deterministically
map the label within that one request. Such a label is not a durable ID, must
not escape the request boundary, and requires a concrete justification in its
own module contract.
## Alternatives considered
- Ask the model to return durable IDs. This exposes opaque implementation
state, does not improve semantic disambiguation, and makes model output
depend on hash formatting.
- Select by name alone. This cannot safely distinguish same-name records.
- Make request-local labels durable identifiers. This would turn prompt
presentation into a public identity contract and create avoidable migration
pressure.
- Let the model invent identifiers or resolve ambiguity. This makes identity
assignment non-deterministic and weakens validation.
## Consequences
Durable integration contracts retain their exact ID/name pairs while models
operate on readable contextual selections. Calling modules must own selector
construction, exact resolution, ambiguity handling, and conversion into their
durable artifact type; PromptKit and its adapter remain transport-only.
Some ambiguous or invalid proposals are deliberately omitted, retried, or
rejected according to the caller's existing failure policy. Internal candidate
keys may support deterministic request-local mapping, but they are not
model-visible selectors or durable data. This adds local validation work while
keeping identity assignment auditable and stable.

View File

@@ -0,0 +1,91 @@
# ADR-0013: Use request-local candidate handles for semantic reconciliation
**Status:** Accepted
**Date:** 2026-08-09
## Context
Several typed normalize stage modules need semantic reconciliation after
deterministic preprocessing: a model can judge whether source-backed candidates
refer to the same underlying entity, while application code remains responsible
for constructing the normalized artifact. Requiring the model to reproduce a
candidate's full contextual selector makes the response larger and introduces
avoidable formatting, ordering, and transcription failure modes.
Reconciliation must preserve the exact typed artifact boundary established by
[ADR-0003](0003-typed-interfaces-with-two-zone-data-model.md), the domain-neutral
framework and concrete-domain dependency direction established by
[ADR-0004](0004-package-modules-by-domain.md), and the distinction in
[ADR-0009](0009-minimal-evidence-grounded-extraction-artifacts.md) between source
evidence and auxiliary identity context. It also needs a concrete, narrowly
scoped application of the request-local-label exception allowed by
[ADR-0012](0012-resolve-opaque-entity-identifiers-deterministically.md).
## Decision
Semantic reconciliation will be a domain-neutral framework mechanism used by
typed normalize stage modules. A consuming artifact family will retain
ownership of its typed records, identity rules, consolidation policy, durable
IDs, and domain warnings; the framework mechanism will not infer those rules
from arbitrary data.
For each reconciliation request, deterministic code will assign every eligible
model-visible candidate a contiguous, one-based integer handle. The model may
receive the candidate's contextual label, source references, and bounded source
context needed to judge identity, but its structured response will identify
candidates only by those supplied handles. A handle is local to one request,
does not represent entity identity, and must never enter a durable artifact or
be used to derive a durable ID.
The model will propose duplicate groups and select one supplied member of each
group as canonical. Deterministic code will resolve the handles through the
retained request mapping, validate the complete proposal, discard unsafe
groups, and apply only validated groups through typed domain-owned policy. The
model will not synthesize replacement records or directly mutate an artifact.
Every reconciliation prompt will combine a mandatory framework-owned protocol
and safety policy with an explicitly selected semantic policy. The semantic
policy may be the conservative generic policy or a domain-owned policy, but it
cannot replace the shared response protocol or deterministic safety boundary.
## Alternatives considered
- Return durable application IDs. Opaque IDs do not help semantic judgment,
expose application identity mechanics, and make model output reproduce data
that deterministic code already owns.
- Return names alone or copied contextual selectors. Names can be ambiguous,
while reproducing labels and source ranges adds response complexity and
creates mismatches without adding semantic information. Request-local
handles preserve exact selection without either failure mode.
- Ask the model to return synthesized canonical replacement records. This
would transfer typed artifact construction, provenance consolidation, and
durable identity policy to a probabilistic boundary.
- Reconcile reflection-discovered fields or arbitrary JSON. This would weaken
the typed artifact contract and move domain semantics into generic code.
- Hide reconciliation inside extraction or another stage. This would obscure
stage ownership and create cross-stage behavior outside the fixed pipeline;
reconciliation remains explicit normalize-stage behavior.
- Let each domain replace the complete prompt protocol. This would duplicate
safety mechanics and allow domain policy to bypass the common response and
validation contract.
## Consequences
Model responses become smaller and easier to validate, while deterministic
application code retains authority over identity, provenance, ordering, and
typed artifact construction. The framework requires a request-local mapping,
bounded context preparation, a private integer response contract, proposal
assessment, and shared prompt assets. Each consuming artifact family still
requires a typed adapter for its irreducibly domain-specific rules.
Request-local handles are deliberately unsuitable for persistence, logging as
entity identity, checkpoint contracts, or cross-request correlation. Changes
to shared protocol and policy assets must participate in the normal prompt,
schema, and checkpoint fingerprint mechanisms.
Acceptance of this decision does not imply that the shared mechanism or its
consumer migrations are implemented. The
[feature roadmap](../roadmap/semantic-reconciliation.md) owns target behavior
and status, and the
[implementation plan](../roadmap/implementation.md) owns delivery sequence
until the work is complete.

View File

@@ -298,7 +298,7 @@ output:
enabled: true enabled: true
window_units: 3 window_units: 3
lanes: lanes:
- npcs - npc-registry
- spells - spells
~~~ ~~~
@@ -319,7 +319,8 @@ Unknown outer or nested option fields are rejected, as are incompatible YAML
types. The allowlist remains valid when a run uses lane filtering: a configured types. The allowlist remains valid when a run uses lane filtering: a configured
lane that is not active for that invocation simply contributes no evidence. lane that is not active for that invocation simply contributes no evidence.
Evidence publication is opt-in because it can persist source text and metadata. Evidence publication is opt-in because it can persist source text and metadata.
Its payload contract is [Published Evidence Context](integrations/evidence-context.md). When enabled, it publishes the selected source-unit excerpt defined by the
[Published Evidence Context contract](integrations/evidence-context.md).
## References And Ordered Handoffs ## References And Ordered Handoffs
@@ -332,15 +333,15 @@ step:
steps: steps:
- id: describe-session - id: describe-session
artifacts: artifacts:
npcs: npc-registry:
extract: dnd/npcs extract: dnd/npc-registry
normalize: dnd/npcs normalize: dnd/npc-registry
- id: extract-events - id: extract-events
references: references:
npcs: npc_registry:
artifact: artifact:
step: describe-session step: describe-session
lane: npcs lane: npc-registry
artifacts: artifacts:
spells: spells:
extract: dnd/spells extract: dnd/spells
@@ -370,28 +371,38 @@ selected target declares them:
| **players** | Optional text player context. | | **players** | Optional text player context. |
| **glossary** | Optional text campaign glossary. | | **glossary** | Optional text campaign glossary. |
| **spell_catalog** | Optional JSON spell-catalog overlay for spell extraction and normalization. See [spell-catalog overlays](integrations/dnd-spell-catalog-overlays.md). | | **spell_catalog** | Optional JSON spell-catalog overlay for spell extraction and normalization. See [spell-catalog overlays](integrations/dnd-spell-catalog-overlays.md). |
| **npcs** | Normalized NPC registry. Optional for spells and combat turns; required for NPC interactions and enemy-event extraction and normalization. | | **location_registry** | Required normalized location registry for location-occurrence extraction and normalization. |
| **item_registry** | Required normalized item registry for item-occurrence extraction and normalization. |
| **npc_registry** | Normalized NPC registry. Optional for spells and combat turns; required for NPC occurrences and enemy-event extraction and normalization. |
| **scene_descriptions** | Required normalized scene-description artifact for combat-turn and enemy-event extraction. | | **scene_descriptions** | Required normalized scene-description artifact for combat-turn and enemy-event extraction. |
| **combat_turns** | Required normalized combat-turn artifact for enemy-event extraction. | | **combat_turns** | Required normalized combat-turn artifact for enemy-event extraction. |
| **npc_interactions** | Required normalized NPC-interaction artifact for enemy-event extraction. | | **npc_occurrences** | Required normalized NPC-occurrence artifact for enemy-event extraction. |
Enemy-event artifact slots have the following exact binding contract. Durable Registry-backed occurrence and enemy-event artifact slots have the following
event semantics and wire shape remain in the exact binding contracts. Durable semantics and wire shapes remain in their
[enemy-event artifact contract](integrations/dnd-enemy-event-artifacts.md). [NPC occurrence](integrations/dnd-npc-occurrence-artifacts.md),
[location occurrence](integrations/dnd-location-occurrence-artifacts.md),
[item occurrence](integrations/dnd-item-occurrence-artifacts.md), and
[enemy-event](integrations/dnd-enemy-event-artifacts.md) contracts.
| Slot | Accepted artifact kind | Media type | Maximum size | Required stage | | Slot | Accepted artifact kind | Media type | Maximum size | Required stage |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `npcs` | `dnd/npc-list` | `application/json` | 1,048,576 bytes | extract and normalize | | `npc_registry` | `dnd/npc-registry` | `application/json` | 1,048,576 bytes | extract and normalize |
| `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only | | `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only |
| `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only | | `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only |
| `npc_interactions` | `dnd/npc-interaction-list` | `application/json` | 1,048,576 bytes | extract only | | `npc_occurrences` | `dnd/npc-occurrence-list` | `application/json` | 1,048,576 bytes | extract only |
| `location_registry` | `dnd/location-registry` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize |
| `item_registry` | `dnd/item-registry` | `application/json` | 1,048,576 bytes | item-occurrence extract and normalize |
Scene descriptions accept **party**, **players**, and **glossary**, but not Scene descriptions accept **party**, **players**, and **glossary**, but not
**roster**. NPC interactions require **npcs** for both extraction and **roster**. NPC occurrences require **npc_registry** for both extraction and
normalization. Combat turns require **scene_descriptions** for extraction; the normalization. Combat turns require **scene_descriptions** for extraction; the
normalized combat-turn module may use optional **npcs**. Enemy-event extraction normalized combat-turn module may use optional **npc_registry**. Location occurrences
requires all four JSON artifact slots; its normalizer requires **npcs**. The require **location_registry** for extraction and normalization. Item occurrences require
complete example shows the ordered generated bindings. **item_registry** for extraction and normalization. Enemy-event extraction requires all
four of its JSON artifact slots; its normalizer requires **npc_registry**.
The [complete example](../examples/dnd-complete.config.yml) shows the ordered
generated bindings.
## Production Module Keys ## Production Module Keys
@@ -399,19 +410,29 @@ complete example shows the ordered generated bindings.
| --- | --- | | --- | --- |
| Input | **seriatim** | | Input | **seriatim** |
| Chunk | **generic**, **dnd/scenes** | | Chunk | **generic**, **dnd/scenes** |
| Extract | **dnd/spells**, **dnd/npcs**, **dnd/combat-turns**, **dnd/item-events**, **dnd/npc-interactions**, **dnd/scene-descriptions**, **dnd/enemy-events** | | Extract | **dnd/spells**, **dnd/npc-registry**, **dnd/combat-turns**, **dnd/item-occurrences**, **dnd/item-registry**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/location-registry**, **dnd/location-occurrences** |
| Merge | **appendorder** | | Merge | **appendorder** |
| Normalize | **noop**, **dnd/spells**, **dnd/npcs**, **dnd/combat-turns**, **dnd/item-events**, **dnd/npc-interactions**, **dnd/scene-descriptions**, **dnd/enemy-events** | | Normalize | **noop**, **dnd/spells**, **dnd/npc-registry**, **dnd/combat-turns**, **dnd/item-occurrences**, **dnd/item-registry**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/location-registry**, **dnd/location-occurrences** |
| Output | **json** | | Output | **json** |
`dnd/scenes` and every D&D extractor are `llm_backed`. The
`dnd/npc-registry`, `dnd/location-registry`, and `dnd/item-registry`
normalizers are also `llm_backed` for bounded duplicate proposals; every other
D&D normalizer is `deterministic`. LLM-backed bindings use the effective
[PromptKit profile](#promptkit-profiles). The complete example binds each
registry in an earlier step before its occurrence consumer.
The D&D artifact contracts define each emitted schema: The D&D artifact contracts define each emitted schema:
[spells](integrations/dnd-spell-artifacts.md), [spells](integrations/dnd-spell-artifacts.md),
[NPCs](integrations/dnd-npc-artifacts.md), [NPC registry](integrations/dnd-npc-registry-artifacts.md),
[NPC interactions](integrations/dnd-npc-interaction-artifacts.md), [NPC occurrences](integrations/dnd-npc-occurrence-artifacts.md),
[combat turns](integrations/dnd-combat-turn-artifacts.md), [combat turns](integrations/dnd-combat-turn-artifacts.md),
[item events](integrations/dnd-item-event-artifacts.md), [item registry](integrations/dnd-item-registry-artifacts.md),
[scene descriptions](integrations/dnd-scene-description-artifacts.md), and [item occurrences](integrations/dnd-item-occurrence-artifacts.md),
[enemy events](integrations/dnd-enemy-event-artifacts.md). [scene descriptions](integrations/dnd-scene-description-artifacts.md),
[enemy events](integrations/dnd-enemy-event-artifacts.md),
[location registry](integrations/dnd-location-registry-artifacts.md), and
[location occurrences](integrations/dnd-location-occurrence-artifacts.md).
## Production Validator Keys And Default Chains ## Production Validator Keys And Default Chains
@@ -421,12 +442,15 @@ Available validator keys are:
| --- | --- | | --- | --- |
| Generic | **generic/always_accept**, **generic/always_reject**, **generic/valid_json**, **generic/valid_json_schema** | | Generic | **generic/always_accept**, **generic/always_reject**, **generic/valid_json**, **generic/valid_json_schema** |
| Spells | **extract/dnd/spells/shape**, **extract/dnd/spells/catalog**, **extract/dnd/spells/source_refs**, **extract/dnd/spells/source_relatedness** | | Spells | **extract/dnd/spells/shape**, **extract/dnd/spells/catalog**, **extract/dnd/spells/source_refs**, **extract/dnd/spells/source_relatedness** |
| NPCs | **extract/dnd/npcs/shape**, **extract/dnd/npcs/source_refs**, **extract/dnd/npcs/source_relatedness**, **normalize/dnd/npcs/identity** | | NPC registry | **extract/dnd/npc-registry/shape**, **extract/dnd/npc-registry/source_refs**, **extract/dnd/npc-registry/source_relatedness**, **normalize/dnd/npc-registry/identity** |
| Combat turns | **extract/dnd/combat-turns/shape**, **extract/dnd/combat-turns/source_refs**, **extract/dnd/combat-turns/source_relatedness**, **normalize/dnd/combat-turns/invariants** | | Combat turns | **extract/dnd/combat-turns/shape**, **extract/dnd/combat-turns/source_refs**, **extract/dnd/combat-turns/source_relatedness**, **normalize/dnd/combat-turns/invariants** |
| Item events | **extract/dnd/item-events/shape**, **extract/dnd/item-events/source_refs**, **extract/dnd/item-events/source_relatedness**, **normalize/dnd/item-events/invariants** | | Item occurrences | **extract/dnd/item-occurrences/shape**, **extract/dnd/item-occurrences/registry**, **extract/dnd/item-occurrences/source_refs**, **extract/dnd/item-occurrences/source_relatedness**, **normalize/dnd/item-occurrences/invariants** |
| NPC interactions | **extract/dnd/npc-interactions/shape**, **extract/dnd/npc-interactions/registry**, **extract/dnd/npc-interactions/source_refs**, **extract/dnd/npc-interactions/source_relatedness**, **normalize/dnd/npc-interactions/invariants** | | Item registry | **extract/dnd/item-registry/shape**, **extract/dnd/item-registry/source_refs**, **extract/dnd/item-registry/source_relatedness**, **normalize/dnd/item-registry/identity** |
| NPC occurrences | **extract/dnd/npc-occurrences/shape**, **extract/dnd/npc-occurrences/registry**, **extract/dnd/npc-occurrences/source_refs**, **extract/dnd/npc-occurrences/source_relatedness**, **normalize/dnd/npc-occurrences/invariants** |
| Scene descriptions | **extract/dnd/scene-descriptions/shape**, **extract/dnd/scene-descriptions/source_refs**, **extract/dnd/scene-descriptions/source_relatedness**, **normalize/dnd/scene-descriptions/invariants** | | Scene descriptions | **extract/dnd/scene-descriptions/shape**, **extract/dnd/scene-descriptions/source_refs**, **extract/dnd/scene-descriptions/source_relatedness**, **normalize/dnd/scene-descriptions/invariants** |
| Enemy events | **extract/dnd/enemy-events/shape**, **extract/dnd/enemy-events/engagements**, **extract/dnd/enemy-events/source_refs**, **extract/dnd/enemy-events/source_relatedness**, **normalize/dnd/enemy-events/invariants** | | Enemy events | **extract/dnd/enemy-events/shape**, **extract/dnd/enemy-events/engagements**, **extract/dnd/enemy-events/source_refs**, **extract/dnd/enemy-events/source_relatedness**, **normalize/dnd/enemy-events/invariants** |
| Location registry | **extract/dnd/location-registry/shape**, **extract/dnd/location-registry/source_refs**, **extract/dnd/location-registry/source_relatedness**, **normalize/dnd/location-registry/identity** |
| Location occurrences | **extract/dnd/location-occurrences/shape**, **extract/dnd/location-occurrences/registry**, **extract/dnd/location-occurrences/source_refs**, **extract/dnd/location-occurrences/source_relatedness**, **normalize/dnd/location-occurrences/invariants** |
When no override is configured, production D&D bindings use the following When no override is configured, production D&D bindings use the following
ordered chains. Each row lists extract then normalize; spell chains are the ordered chains. Each row lists extract then normalize; spell chains are the
@@ -435,12 +459,15 @@ same at both stages.
| Lane | Extract | Normalize | | Lane | Extract | Normalize |
| --- | --- | --- | | --- | --- | --- |
| Spells | generic/valid_json, extract/dnd/spells/shape, extract/dnd/spells/catalog, extract/dnd/spells/source_refs, generic/valid_json_schema, extract/dnd/spells/source_relatedness | Same as extract | | Spells | generic/valid_json, extract/dnd/spells/shape, extract/dnd/spells/catalog, extract/dnd/spells/source_refs, generic/valid_json_schema, extract/dnd/spells/source_relatedness | Same as extract |
| NPCs | generic/valid_json, extract/dnd/npcs/shape, extract/dnd/npcs/source_refs, generic/valid_json_schema, extract/dnd/npcs/source_relatedness | generic/valid_json, extract/dnd/npcs/shape, normalize/dnd/npcs/identity, extract/dnd/npcs/source_refs, generic/valid_json_schema, extract/dnd/npcs/source_relatedness | | NPC registry | generic/valid_json, extract/dnd/npc-registry/shape, extract/dnd/npc-registry/source_refs, generic/valid_json_schema, extract/dnd/npc-registry/source_relatedness | generic/valid_json, extract/dnd/npc-registry/shape, normalize/dnd/npc-registry/identity, extract/dnd/npc-registry/source_refs, generic/valid_json_schema, extract/dnd/npc-registry/source_relatedness |
| Combat turns | generic/valid_json, extract/dnd/combat-turns/shape, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | generic/valid_json, extract/dnd/combat-turns/shape, normalize/dnd/combat-turns/invariants, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | | Combat turns | generic/valid_json, extract/dnd/combat-turns/shape, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | generic/valid_json, extract/dnd/combat-turns/shape, normalize/dnd/combat-turns/invariants, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness |
| Item events | generic/valid_json, extract/dnd/item-events/shape, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | generic/valid_json, extract/dnd/item-events/shape, normalize/dnd/item-events/invariants, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | | Item occurrences | generic/valid_json, extract/dnd/item-occurrences/shape, extract/dnd/item-occurrences/registry, extract/dnd/item-occurrences/source_refs, generic/valid_json_schema, extract/dnd/item-occurrences/source_relatedness | generic/valid_json, extract/dnd/item-occurrences/shape, extract/dnd/item-occurrences/registry, normalize/dnd/item-occurrences/invariants, extract/dnd/item-occurrences/source_refs, generic/valid_json_schema, extract/dnd/item-occurrences/source_relatedness |
| NPC interactions | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, normalize/dnd/npc-interactions/invariants, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | | Item registry | generic/valid_json, extract/dnd/item-registry/shape, extract/dnd/item-registry/source_refs, generic/valid_json_schema, extract/dnd/item-registry/source_relatedness | generic/valid_json, extract/dnd/item-registry/shape, normalize/dnd/item-registry/identity, extract/dnd/item-registry/source_refs, generic/valid_json_schema, extract/dnd/item-registry/source_relatedness |
| NPC occurrences | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, normalize/dnd/npc-occurrences/invariants, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness |
| Scene descriptions | generic/valid_json, extract/dnd/scene-descriptions/shape, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | generic/valid_json, extract/dnd/scene-descriptions/shape, normalize/dnd/scene-descriptions/invariants, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | | Scene descriptions | generic/valid_json, extract/dnd/scene-descriptions/shape, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | generic/valid_json, extract/dnd/scene-descriptions/shape, normalize/dnd/scene-descriptions/invariants, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness |
| Enemy events | 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 | generic/valid_json, extract/dnd/enemy-events/shape, normalize/dnd/enemy-events/invariants, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness | | Enemy events | 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 | generic/valid_json, extract/dnd/enemy-events/shape, normalize/dnd/enemy-events/invariants, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness |
| Location registry | generic/valid_json, extract/dnd/location-registry/shape, extract/dnd/location-registry/source_refs, generic/valid_json_schema, extract/dnd/location-registry/source_relatedness | generic/valid_json, extract/dnd/location-registry/shape, normalize/dnd/location-registry/identity, extract/dnd/location-registry/source_refs, generic/valid_json_schema, extract/dnd/location-registry/source_relatedness |
| Location occurrences | generic/valid_json, extract/dnd/location-occurrences/shape, extract/dnd/location-occurrences/registry, extract/dnd/location-occurrences/source_refs, generic/valid_json_schema, extract/dnd/location-occurrences/source_relatedness | generic/valid_json, extract/dnd/location-occurrences/shape, extract/dnd/location-occurrences/registry, normalize/dnd/location-occurrences/invariants, extract/dnd/location-occurrences/source_refs, generic/valid_json_schema, extract/dnd/location-occurrences/source_relatedness |
Chains are only registered for the D&D extract and normalize modules shown Chains are only registered for the D&D extract and normalize modules shown
above; select an explicit override when a different compatible chain is above; select an explicit override when a different compatible chain is

View File

@@ -55,10 +55,10 @@ contract. The JSON bundle contract links to the available lane contracts.
If `index.json` has an `evidence_context` descriptor, treat it as a If `index.json` has an `evidence_context` descriptor, treat it as a
pipeline-wide artifact rather than a lane entry. Verify its six descriptor pipeline-wide artifact rather than a lane entry. Verify its six descriptor
fields before decoding the linked file according to the [Published Evidence fields before decoding the linked file according to the [Published Evidence
Context contract](../integrations/evidence-context.md). Use each Context contract](../integrations/evidence-context.md). Decode its top-level
`evidence_refs` entry as the citation to source material. Its surrounding source-unit array as a reading excerpt. Obtain authoritative citations and lane
context range and included units explain the citation, but do not widen or provenance from the normalized lane artifacts; the excerpt has neither and its
replace the cited source reference. nearby units do not widen a lane artifact's cited source reference.
A zero exit status may still report rejected outputs, warnings, or absent A zero exit status may still report rejected outputs, warnings, or absent
lanes. The caller decides which lane IDs are required for its own work and lanes. The caller decides which lane IDs are required for its own work and
@@ -73,5 +73,5 @@ them. Treat the input, output bundle, cache, debug bundle, and captured process
logs as potentially sensitive data. Apply the caller's access controls and logs as potentially sensitive data. Apply the caller's access controls and
retention policy, and avoid copying secrets into arguments, logs, or retention policy, and avoid copying secrets into arguments, logs, or
provenance records. An evidence-context artifact contains source-unit text and provenance records. An evidence-context artifact contains source-unit text and
metadata, and selected lanes can cover most of an input; preserve and share it metadata and can cover most of an input; preserve and share it only when that
only when that source content is authorized for the recipient. source content is authorized for the recipient.

View File

@@ -55,7 +55,7 @@ record controls eligibility only: its title, summary, and reference do not
become turn evidence. No exact matching scene also produces an empty list and become turn evidence. No exact matching scene also produces an empty list and
the `scene_classification_unavailable` warning. the `scene_classification_unavailable` warning.
An optional normalized [NPC artifact](dnd-npc-artifacts.md) can ground an An optional normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) can ground an
actor name. Its registry references are provenance, never combat evidence. actor name. Its registry references are provenance, never combat evidence.
Normalization trims and, where possible, canonicalizes actor names; orders and Normalization trims and, where possible, canonicalizes actor names; orders and
deduplicates exact source references; orders valid-evidence turns by source deduplicates exact source references; orders valid-evidence turns by source
@@ -63,7 +63,7 @@ chronology; and collapses only duplicates with the same actor identity, turn
kind, and complete valid evidence. It does not infer turns, initiative, or kind, and complete valid evidence. It does not infer turns, initiative, or
actions from registry or scene data. actions from registry or scene data.
The [NPC-interaction artifact](dnd-npc-interaction-artifacts.md) records The [NPC-occurrence artifact](dnd-npc-occurrence-artifacts.md) records
broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md)
uses combat turns as grounding only; turns do not establish an enemy event or uses combat turns as grounding only; turns do not establish an enemy event or
its outcome. The [JSON output contract](json-output.md) defines publication, its outcome. The [JSON output contract](json-output.md) defines publication,

View File

@@ -66,7 +66,7 @@ are positive and the start may not follow the end.
| `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. A missed turn is insufficient. | | `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. A missed turn is insufficient. |
The current transcript is the only event evidence. Campaign context and The current transcript is the only event evidence. Campaign context and
normalized NPC, scene-description, combat-turn, and NPC-interaction artifacts normalized NPC, scene-description, combat-turn, and NPC-occurrence artifacts
can ground names or control combat eligibility, but none may supply event can ground names or control combat eligibility, but none may supply event
evidence. An outcome may share evidence with an engagement, in which case both evidence. An outcome may share evidence with an engagement, in which case both
events are retained. events are retained.

View File

@@ -1,78 +0,0 @@
# D&D Item-Event Artifact
This contract defines the durable item and currency occurrence list produced by
`dnd/item-events`. It records source-grounded discoveries and possession
changes; it does not maintain an inventory, balance, or ledger.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/item-event-list` |
| Schema ID | `notarius.dnd.item_events` |
| Schema name | `notarius_dnd_item_events_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
`v1` is a strict JSON object with required `events`; the array may be empty.
Event and source-reference objects reject unknown fields. An incompatible
shape change requires a new schema version.
## Wire shape
Every event has required `name`, `kind`, and `source_refs`. `quantity`, `from`,
and `to` are optional where the event kind permits them.
| Field | Contract |
| --- | --- |
| `name` | Non-empty item or currency display name. |
| `kind` | `discovered`, `acquired`, `lost`, `consumed`, or `transferred`. |
| `quantity` | Optional positive integer; omit it when no count is established. |
| `from` | Optional non-empty losing holder, when allowed by `kind`. |
| `to` | Optional non-empty gaining holder, when allowed by `kind`. |
| `source_refs` | One or more transcript evidence ranges. |
Each source reference has exactly `source_id`, `start_unit_id`, and
`end_unit_id`. It identifies an inclusive current-transcript range; unit IDs
are positive and the start may not follow the end.
```json
{
"events": [
{
"name": "Silver Pieces",
"kind": "acquired",
"quantity": 20,
"to": "party",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 2, "end_unit_id": 2}
]
}
]
}
```
## Holder rules and minimal extraction
`discovered` has neither holder; `acquired` requires `to` and forbids `from`;
`lost` and `consumed` require `from` and forbid `to`; `transferred` requires
both holders. `party` denotes collective possession. A transfer cannot use
`party` for either holder and its two normalized holders must differ.
Only an evidenced discovery or possession change belongs in this artifact.
It does not infer quantities or holders, convert currency denominations,
calculate balances, or merge nearby events. Campaign references may
disambiguate names but are never event evidence. Currency uses the ordinary
`name` field and an explicit `quantity` only when the transcript establishes
one; each denomination remains a separate event.
Normalization trims display whitespace, orders and removes exact duplicate
source references, then orders events by valid source chronology, name identity
and display value, kind, holders, quantity, and reference sequence. It
collapses only entries with the same normalized durable fields and complete
valid evidence.
The [JSON output contract](json-output.md) defines publication. See
[D&D module internals](../internal/dnd.md) for implementation details and the
[NPC-interaction artifact](dnd-npc-interaction-artifacts.md) for a distinct
kind of occurrence.

View File

@@ -0,0 +1,72 @@
# D&D Item-Occurrence Artifact
`dnd/item-occurrences` currently produces this source-grounded item and currency
occurrence list. It records discoveries and possession changes, not an
inventory, balance, or ledger.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/item-occurrence-list` |
| Schema ID | `notarius.dnd.item_occurrences` |
| Schema name | `notarius_dnd_item_occurrences_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
`v1` accepts one strict JSON object with required `occurrences`; the array may
be empty. Each occurrence has required `item_id`, `name`, `kind`, and
`source_refs`, and occurrence and source-reference objects reject unknown
fields. `quantity`, `from`, and `to` appear only when their kind permits them.
An incompatible shape change requires a new schema version.
## Registry grounding
Both extraction and normalization require an `item_registry` reference bound to
an earlier normalized `dnd/item-registry` artifact. The registry is immutable
for an operation and contributes names-only grounding after the shared evidence
message. Notarius resolves the model's selected name into the unchanged exact
durable ID/name pair. It is never occurrence evidence.
Each occurrence must use one exact registry ID/name pair. An extraction response
with an unknown or ambiguous selected name is rejected as invalid model output;
the configured pipeline may retry it and never accepts a partial artifact.
Normalization and validation remain defense in depth for artifacts entering
through other boundaries: normalization canonicalizes a recognized name by ID,
preserves unknown values for the registry validator, and the registry validator
rejects unknown or mismatched pairs.
## Wire shape
Each source reference has exactly `source_id`, `start_unit_id`, and
`end_unit_id`. It identifies an inclusive range in the current transcript;
unit IDs are positive and the start may not follow the end.
```json
{
"occurrences": [
{
"item_id": "item:sha256:…",
"name": "Silver Pieces",
"kind": "acquired",
"quantity": 20,
"to": "party",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 2, "end_unit_id": 2}
]
}
]
}
```
The five kinds remain `discovered`, `acquired`, `lost`, `consumed`, and
`transferred`. Holder, quantity, currency, ordering, and exact-duplicate rules
are unchanged: discovered has no holder; acquired requires `to`; lost and
consumed require `from`; transferred requires distinct non-`party` holders.
The only current downstream compatibility requirement is its registry handoff;
the normalized occurrence list is otherwise published for callers. See
[Configuration](../config.md#d-d-reference-slots) for the binding and
[JSON output](json-output.md) for publication.
See [item registry](dnd-item-registry-artifacts.md) for the grounding artifact
and [D&D module internals](../internal/dnd.md) for implementation details.

View File

@@ -0,0 +1,95 @@
# D&D Item Registry Artifact
This contract defines the durable, source-grounded item registry produced by
`dnd/item-registry`. It records transcript-established item types and unique
designations for one source document; it is not an inventory, holder record,
quantity ledger, or item-occurrence artifact.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/item-registry` |
| Schema ID | `notarius.dnd.item_registry` |
| Schema name | `notarius_dnd_item_registry_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
| Identity policy | `dnd.item_registry.identity.v1` |
`v1` accepts one strict JSON object with required `items`; the array may be
empty. Item and source-reference objects reject unknown fields. An incompatible
artifact shape or identity-policy change uses a new version or policy.
## Wire shape and identity
Each item has these required fields:
| Field | Contract |
| --- | --- |
| `id` | `item:sha256:` followed by 64 lowercase hexadecimal characters. |
| `name` | Non-empty transcript-established item type or unique designation. |
| `source_refs` | One or more transcript evidence ranges that establish the item. |
A source reference has exactly `source_id`, `start_unit_id`, and `end_unit_id`.
The source ID identifies the transcript, unit IDs are positive inclusive unit
identifiers, and the start may not follow the end.
```json
{
"items": [
{
"id": "item:sha256:31e73b6280ef98e4d8070e07fd4de9b2c3e842cc03af1a09ca631cb95b73e3b3",
"name": "Star Compass",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
]
}
]
}
```
The ID is deterministic for an item name or type, rather than for one physical
instance. Notarius normalizes the display name for comparison with Unicode
NFKC, supported apostrophe normalization, collapsed whitespace, and case
folding. It hashes compact JSON for this array:
```text
["dnd.item_registry.identity.v1", comparison_name]
```
The canonical ID is the lowercase SHA-256 digest of those bytes with the
`item:sha256:` prefix. Equal comparison names represent one item identity;
normalization unions their transcript evidence when it safely consolidates a
candidate group.
## Scope, reconciliation, and evidence
The registry includes named unique items, concrete reusable item types, stable
unique designations, and separately established currency denominations. It
excludes vague loot or treasure, generic weapons, quantities, inferred
properties, and inferred uniqueness. Capitalization alone does not establish
eligibility.
Normalization first applies deterministic display, evidence, and ID rules. It
then may use a bounded LLM-assisted proposal to reconcile semantically duplicate
records. The proposal may choose only a supplied candidate display name;
invalid, uncertain, overlapping, or unsafe proposals retain the deterministic
result with retry or fallback diagnostics. A proposal that mixes a recognized
currency denomination with a non-currency item, or combines recognized
denominations, is unsafe and retains every deterministic record. Currency
denominations, materially different item types, and merely nearby objects
remain distinct. Source references establish registry provenance, not evidence
for later artifacts.
## Consumers and publication
`dnd/item-occurrences` requires one approved item registry through its
`item_registry` reference slot for both extraction and normalization. Its
consumer receives names-only grounding; Notarius resolves the selected name
into the unchanged exact durable ID/name pair. The registrys source references
are never occurrence evidence. Unknown or ambiguous selections are rejected by
the occurrence contract. See the
[item-occurrence artifact](dnd-item-occurrence-artifacts.md) for that strict
wire contract, [Configuration](../config.md#d-d-reference-slots) for binding
rules and validator selection, and the [JSON output contract](json-output.md)
for publication.

View File

@@ -0,0 +1,86 @@
# D&D Location-Occurrence Artifact
This contract defines the durable occurrence list produced by
`dnd/location-occurrences`. It records source-grounded ways the party relates
to locations in a required normalized location registry; it does not extend
that registry or infer a place absent from it.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/location-occurrence-list` |
| Schema ID | `notarius.dnd.location_occurrences` |
| Schema name | `notarius_dnd_location_occurrences_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
`v1` accepts one strict JSON object with required `occurrences`; the array may
be empty. Occurrence and source-reference objects reject unknown fields. An
incompatible shape change requires a new schema version.
## Wire shape
Each occurrence has these required fields:
| Field | Contract |
| --- | --- |
| `location_id` | Exact ID from the required normalized [location registry](dnd-location-registry-artifacts.md). |
| `name` | Exact canonical display name for `location_id` in that registry. |
| `kind` | One of `visited`, `planned`, `recalled`, or `mentioned`. |
| `source_refs` | One or more current-transcript evidence ranges for this occurrence. |
A source reference has exactly `source_id`, `start_unit_id`, and `end_unit_id`.
It identifies an inclusive range in the current transcript; unit IDs are
positive and the start may not follow the end.
```json
{
"occurrences": [
{
"location_id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
"name": "Moon Gate",
"kind": "visited",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 12, "end_unit_id": 13}
]
}
]
}
```
## Occurrence categories
| Kind | Meaning |
| --- | --- |
| `visited` | The transcript establishes physical party presence, including arrival, continuing presence, or departure. |
| `planned` | The party explicitly proposes, intends, or agrees to future travel; speculation alone is not enough. |
| `recalled` | The transcript explicitly recounts prior party presence before the current live events. |
| `mentioned` | The location is explicit but no stronger category applies, including lore, directions, third-party activity, non-actionable speculation, a mere hypothetical reference, or out-of-character discussion. |
For overlapping evidence, precedence is `visited`, then `planned`, then
`recalled`, then `mentioned`. For example, “What if we went to Moon Gate?” is
eligible as `mentioned` when its narrow evidence explicitly references that
registry location, but it is not `planned` without an actual proposal,
intention, or agreement to travel. Inferred, unstated, uncertain, and
unsupported places or occurrences are omitted. Normalization
canonicalizes the registry name, orders and deduplicates source references, and
orders occurrences by source chronology, location ID, name, kind, and reference
sequence. It collapses only exact duplicates with the same ID, kind, and
complete canonical evidence sequence.
## Required grounding and evidence
Both extraction and normalization require exactly one `location_registry` reference of
kind `dnd/location-registry`, media type `application/json`, and at most 1 MiB. The
registry provides identity grounding only. The model selects a supplied
contextual name-and-registry-reference descriptor, and Notarius resolves it
into the exact durable ID/name pair. Unknown, partial, or ambiguous selections
are rejected rather than guessed or reassigned. The current transcript is the
only evidence source for an occurrence; registry evidence and provenance never
become occurrence evidence.
See [Configuration](../config.md#d-d-reference-slots) for the selectable slot
and generated-handoff compatibility, [D&D module internals](../internal/dnd.md)
for implementation behavior, and the [JSON output contract](json-output.md)
for publication.

View File

@@ -0,0 +1,93 @@
# D&D Location Registry Artifact
This contract defines the durable, source-grounded location registry produced
by `dnd/location-registry`. It records transcript-established physical places for one
source document; it is not a map, location hierarchy, campaign-wide world
registry, or location description.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/location-registry` |
| Schema ID | `notarius.dnd.location_registry` |
| Schema name | `notarius_dnd_location_registry_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
| Identity policy | `dnd.location_registry.identity.v1` |
`v1` accepts one strict JSON object with required `locations`; the array may be
empty. Location and source-reference objects reject unknown fields. An
incompatible artifact shape or identity-policy change uses a new version or
policy.
## Wire shape and identity
Each location has these required fields:
| Field | Contract |
| --- | --- |
| `id` | `location:sha256:` followed by 64 lowercase hexadecimal characters. |
| `name` | Non-empty transcript-established display name. |
| `source_refs` | One or more transcript evidence ranges that identify the place. |
A source reference has exactly `source_id`, `start_unit_id`, and `end_unit_id`.
The source ID identifies the transcript, unit IDs are positive inclusive unit
identifiers, and the start may not follow the end.
```json
{
"locations": [
{
"id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
"name": "Moon Gate",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
]
}
]
}
```
The ID is deterministic and scoped to the source document. Notarius normalizes
the display name for comparison with Unicode NFKC, supported apostrophe
normalization, collapsed whitespace, and case folding. It hashes compact JSON
for this array, using the earliest canonical source reference as the anchor:
```text
["dnd.location_registry.identity.v1", comparison_name, source_id, start_unit_id, end_unit_id]
```
The canonical ID is the lowercase SHA-256 digest of those bytes with the
`location:sha256:` prefix. Equal display names are allowed when their evidence
anchors differ, so a generic name does not force distinct places to collapse.
## Scope, reconciliation, and evidence
Locations are physical or spatial places established by the transcript with a
stable proper name or unique in-world designation, such as named planes,
regions, settlements, districts, buildings, rooms, landmarks, routes, and
geographic features. Generic, temporary, relative, and descriptive phrases
such as “the room,” “the bar,” “the hallway,” “outside,” and “upstairs” are not
registry locations. Capitalization alone does not establish eligibility.
Notarius does not infer an unstated place or add hierarchy, coordinates,
descriptions, participants, or ownership.
Normalization first applies deterministic display, evidence, and ID rules. It
then may use a bounded LLM-assisted proposal to reconcile semantically duplicate
records. The proposal is validated and applied conservatively; invalid or
unusable proposals retain the deterministic result with retry or fallback
diagnostics. The registry's source references establish registry provenance,
not evidence for later artifacts.
## Consumers and publication
`dnd/location-occurrences` requires one approved location registry through its
`location_registry` reference slot. Its prompt receives contextual selectors
containing a canonical name and registry references; Notarius resolves a
selection into the unchanged exact durable ID/name pair. Registry references
must not be treated as occurrence evidence. See the
[location-occurrence artifact](dnd-location-occurrence-artifacts.md)
for that contract, [Configuration](../config.md#references-and-ordered-handoffs)
for binding rules, and the [JSON output contract](json-output.md) for
publication.

View File

@@ -1,72 +0,0 @@
# D&D NPC Artifact
This contract defines the durable NPC registry produced by `dnd/npcs`. It is a
minimal, source-grounded identity registry for other D&D artifacts, not a
character sheet or a relationship summary.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/npc-list` |
| Schema ID | `notarius.dnd.npcs` |
| Schema name | `notarius_dnd_npcs_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
| Identity policy | `dnd.npcs.identity.v1` |
`v1` accepts one strict JSON object with required `npcs`; the array may be
empty. NPC and source-reference objects reject unknown fields. An incompatible
artifact shape or identity-policy change uses a new version or policy.
## Wire shape and identity
Each NPC has these required fields:
| Field | Contract |
| --- | --- |
| `id` | `npc:sha256:` followed by 64 lowercase hexadecimal characters. |
| `name` | Non-empty canonical display name. |
| `source_refs` | One or more transcript evidence ranges for the identity. |
A source reference has exactly `source_id`, `start_unit_id`, and `end_unit_id`.
The source ID identifies the transcript, unit IDs are positive inclusive unit
identifiers, and the start may not follow the end.
```json
{
"npcs": [
{
"id": "npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7",
"name": "Mira Thorn",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
]
}
]
}
```
The ID is deterministic: normalize the name to Unicode NFKC, normalize the
supported apostrophe forms, collapse whitespace, case-fold it, SHA-256 the
result, then prefix the lowercase hexadecimal digest with `npc:sha256:`. Each
canonical identity and ID appears at most once. Normalization collapses records
with the same canonical identity, retains their earliest position, and merges
their canonicalized evidence; it does not add aliases, roles, descriptions, or
relationship fields.
## Scope and consumers
Only individually identifiable NPC names with transcript evidence belong in
this artifact. Groups, generic roles, invented labels, and descriptive
enrichment are excluded. Its source references prove registry provenance; they
do not become evidence for a spell, interaction, combat, or enemy-event
occurrence.
This registry can ground actor or caster names in the [spell](dnd-spell-artifacts.md)
and [combat-turn](dnd-combat-turn-artifacts.md) artifacts. It is required to
resolve the canonical `name` in an [NPC interaction](dnd-npc-interaction-artifacts.md).
The [enemy-event artifact](dnd-enemy-event-artifacts.md) also uses it only for
subject grounding and canonical display names.
The [JSON output contract](json-output.md) defines publication, and
[D&D module internals](../internal/dnd.md) owns pipeline mechanics.

View File

@@ -1,7 +1,7 @@
# D&D NPC Interaction Artifact # D&D NPC Occurrence Artifact
This contract defines the durable occurrence list produced by This contract defines the durable occurrence list produced by
`dnd/npc-interactions`. It records discrete, source-grounded interactions with `dnd/npc-occurrences`. It records discrete, source-grounded occurrences with
NPCs already present in a normalized registry; it does not extend that registry NPCs already present in a normalized registry; it does not extend that registry
or summarize the session. or summarize the session.
@@ -9,35 +9,37 @@ or summarize the session.
| Property | Value | | Property | Value |
| --- | --- | | --- | --- |
| Artifact kind | `dnd/npc-interaction-list` | | Artifact kind | `dnd/npc-occurrence-list` |
| Schema ID | `notarius.dnd.npc_interactions` | | Schema ID | `notarius.dnd.npc_occurrences` |
| Schema name | `notarius_dnd_npc_interactions_v1` | | Schema name | `notarius_dnd_npc_occurrences_v1` |
| Schema version | `v1` | | Schema version | `v1` |
| Media type | `application/json` | | Media type | `application/json` |
`v1` is a strict JSON object with required `interactions`; the array may be `v1` is a strict JSON object with required `occurrences`; the array may be
empty. Interaction and source-reference objects reject unknown fields. An empty. Occurrence and source-reference objects reject unknown fields. An
incompatible shape change requires a new schema version. incompatible shape change requires a new schema version.
## Wire shape ## Wire shape
Each interaction has these required fields: Each occurrence has these required fields:
| Field | Contract | | Field | Contract |
| --- | --- | | --- | --- |
| `npc_id` | Exact durable ID from the required NPC registry. |
| `name` | Non-empty canonical display name from the required NPC registry. | | `name` | Non-empty canonical display name from the required NPC registry. |
| `kind` | One of the interaction categories below. | | `kind` | One of the occurrence categories below. |
| `source_refs` | One or more transcript evidence ranges. | | `source_refs` | One or more transcript evidence ranges. |
Each source reference has exactly `source_id`, `start_unit_id`, and Each source reference has exactly `source_id`, `start_unit_id`, and
`end_unit_id`. It identifies an inclusive range in the current transcript; `end_unit_id`. It identifies an inclusive range in the current transcript;
unit IDs are positive and the start may not follow the end. Extraction evidence unit IDs are positive and the start may not follow the end. Extraction evidence
for an interaction is confined to its accepted chunk. for an occurrence is confined to its accepted chunk.
```json ```json
{ {
"interactions": [ "occurrences": [
{ {
"npc_id": "npc:sha256:example",
"name": "Mira Thorn", "name": "Mira Thorn",
"kind": "dialogue", "kind": "dialogue",
"source_refs": [ "source_refs": [
@@ -48,7 +50,7 @@ for an interaction is confined to its accepted chunk.
} }
``` ```
## Interaction categories ## Occurrence categories
| Kind | Meaning | | Kind | Meaning |
| --- | --- | | --- | --- |
@@ -65,17 +67,24 @@ for uncertain classification.
## Identity, evidence, and order ## Identity, evidence, and order
The required normalized [NPC artifact](dnd-npc-artifacts.md) resolves `name`. The required normalized [NPC registry artifact](dnd-npc-registry-artifacts.md)
Registry references are provenance only and never replace an interaction's own supplies names-only contextual grounding to the model. Notarius resolves the
evidence. Normalization canonicalizes recognized registry names, orders and selected name and writes the exact `{npc_id, name}` pair. An unknown or
deduplicates exact source references, then orders interactions by valid source ambiguous selection rejects the complete model result; normalization does not
repair names by similarity. Registry references are provenance only and never
replace an occurrence's own evidence.
The registry may include an identity established by a factual third-party
mention; that provenance alone does not create a `mentioned` occurrence. Each
occurrence remains a separately cited fact in the current transcript.
Normalization validates the exact pair, orders and
deduplicates exact source references, then orders occurrences by valid source
chronology, NPC comparison identity, display name, kind, and reference sequence. chronology, NPC comparison identity, display name, kind, and reference sequence.
Only entries with the same canonical name, kind, and complete valid evidence Only entries with the same NPC ID, canonical name, kind, and complete valid evidence
sequence are collapsed; distinct categories or evidence remain separate. sequence are collapsed; distinct categories or evidence remain separate.
See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action
occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes
only `combat_opponent` interactions as grounding; they never establish an enemy only `combat_opponent` occurrences as grounding; they never establish an enemy
event or outcome. The [JSON output contract](json-output.md) defines event or outcome. The [JSON output contract](json-output.md) defines
publication. Pipeline mechanics are described in publication. Pipeline mechanics are described in
[D&D module internals](../internal/dnd.md). [D&D module internals](../internal/dnd.md).

View File

@@ -0,0 +1,92 @@
# D&D NPC Registry Artifact
This contract defines the durable NPC registry produced by `dnd/npc-registry`. It is a
minimal, source-grounded identity registry for other D&D artifacts, not a
character sheet or a relationship summary.
## Identity and compatibility
| Property | Value |
| --- | --- |
| Artifact kind | `dnd/npc-registry` |
| Schema ID | `notarius.dnd.npc_registry` |
| Schema name | `notarius_dnd_npc_registry_v1` |
| Schema version | `v1` |
| Media type | `application/json` |
| Identity policy | `dnd.npc_registry.identity.v1` |
`v1` accepts one strict JSON object with required `npcs`; the array may be
empty. NPC and source-reference objects reject unknown fields. An incompatible
artifact shape or identity-policy change uses a new version or policy.
## Wire shape and identity
Each NPC has these required fields:
| Field | Contract |
| --- | --- |
| `id` | `npc:sha256:` followed by 64 lowercase hexadecimal characters. |
| `name` | Non-empty canonical display name. |
| `source_refs` | One or more transcript evidence ranges for the identity. |
A source reference has exactly `source_id`, `start_unit_id`, and `end_unit_id`.
The source ID identifies the transcript, unit IDs are positive inclusive unit
identifiers, and the start may not follow the end.
```json
{
"npcs": [
{
"id": "npc:sha256:35ba5f679aee69e07ae3bd65c44278f29539d5dc9bb5225db1c0060555b23221",
"name": "Mira Thorn",
"source_refs": [
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
]
}
]
}
```
The ID is deterministic: normalize the name to Unicode NFKC, normalize the
supported apostrophe forms, collapse whitespace, case-fold it, then serialize
`["dnd.npc_registry.identity.v1", comparison_name]` as compact JSON. SHA-256
those UTF-8 bytes and prefix the lowercase hexadecimal digest with
`npc:sha256:`. Each canonical identity and ID appears at most once.
Normalization collapses records with the same canonical identity, retains their
earliest position, and merges
their canonicalized evidence; it does not add aliases, roles, descriptions, or
relationship fields.
When evidence supports a semantically duplicate group, the canonical display
name is one of that group's supplied candidates. A complete, stable proper name
is preferred over an abbreviation. An unadorned proper name is preferred over
the same name plus a contextual class, role, title, or relationship descriptor
unless the transcript establishes that descriptor as part of the person's
name. A longer candidate is not preferred solely because it includes such a
descriptor.
## Scope and consumers
Only individually identifiable NPC names with transcript evidence belong in
this artifact. A factual third-party mention can establish an identity even if
the NPC is not present, speaking, or acting in the cited passage. Names used
only in hypothetical, speculative, or imagined examples are excluded, as are
groups, generic roles, invented labels, and descriptive enrichment. Its source
references prove registry provenance; they do not become evidence for a spell,
occurrence, combat, or enemy-event occurrence.
Registry evidence establishes an identity, not an [NPC occurrence](dnd-npc-occurrence-artifacts.md).
That later artifact independently records any current-transcript occurrence
with its own cited evidence and category.
This registry can ground actor or caster names in the [spell](dnd-spell-artifacts.md)
and [combat-turn](dnd-combat-turn-artifacts.md) artifacts. It is required to
resolve the canonical `name` in an [NPC occurrence](dnd-npc-occurrence-artifacts.md).
Occurrence consumers receive names-only grounding; Notarius resolves the
selected canonical name and writes the unchanged exact durable ID/name pair.
Spells, combat turns, and the [enemy-event artifact](dnd-enemy-event-artifacts.md)
also receive names-only grounding for actor or subject display. None of these
projections supply later-artifact evidence. [Configuration](../config.md#d-d-reference-slots)
owns the `npc_registry` binding rules.
The [JSON output contract](json-output.md) defines publication, and
[D&D module internals](../internal/dnd.md) owns pipeline mechanics.

View File

@@ -61,7 +61,7 @@ only when it has the same canonical spell, the same case- and
whitespace-insensitive caster identity, and the same complete valid reference whitespace-insensitive caster identity, and the same complete valid reference
sequence. Remaining entries retain their merged order. sequence. Remaining entries retain their merged order.
The optional normalized [NPC artifact](dnd-npc-artifacts.md) can ground a The optional normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) can ground a
caster name. Its own references remain registry provenance and are never copied caster name. Its own references remain registry provenance and are never copied
into `source_refs`. into `source_refs`.

View File

@@ -67,6 +67,12 @@ including a collision with the embedded catalog. Matching uses the catalogs
case, whitespace, and apostrophe normalization, so authors should avoid names case, whitespace, and apostrophe normalization, so authors should avoid names
or aliases that normalize to another spell. or aliases that normalize to another spell.
Spell extraction receives the effective catalog as deterministic canonical-name
and alias pairs. An alias in the transcript selects its associated canonical
name; the extractor is instructed to return that canonical spelling. The
projection contains no catalog source metadata or provenance, and aliases
remain recognition context rather than transcript evidence.
The overlay is a recognition aid only. The durable spell-artifact schema and The overlay is a recognition aid only. The durable spell-artifact schema and
source-evidence rules are defined by the source-evidence rules are defined by the
[D&D spell artifact contract](dnd-spell-artifacts.md). [D&D spell artifact contract](dnd-spell-artifacts.md).

View File

@@ -1,9 +1,11 @@
# Published Evidence Context # Published Evidence Context
This contract defines the optional `source/evidence-context` artifact emitted This contract defines the optional `source/evidence-context` artifact emitted
by the production JSON output. Its configuration is owned by by the production JSON output. It is a selected source-unit excerpt for
[Configuration](../config.md#module-bindings-and-validators); its logical-file convenient reading alongside normalized lane artifacts; it is not a second
discovery is owned by [Published JSON Output](json-output.md). citation or provenance model. Its configuration is owned by
[Configuration](../config.md#module-bindings-and-validators), and its
logical-file discovery is owned by [Published JSON Output](json-output.md).
## Identity And Discovery ## Identity And Discovery
@@ -26,35 +28,13 @@ its absence means evidence publication was not enabled for that bundle.
## Payload ## Payload
The v1 payload is a JSON object with required `source_id`, `source_digest`, The v1 payload is a top-level JSON array of generic source units. There is no
`window_units`, `selected_lanes`, and `contexts` fields. `selected_lanes` and wrapper, source-level metadata, context grouping, lane identifier, or evidence
`contexts` are always arrays; an enabled configuration with no accepted direct reference in the payload. An enabled configuration with no contributing
evidence publishes `contexts: []`. accepted evidence publishes `[]`.
```json ```json
{ [
"source_id": "session-alpha",
"source_digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"window_units": 1,
"selected_lanes": ["npcs", "spells"],
"contexts": [
{
"context_ref": {
"source_id": "session-alpha",
"start_unit_id": 10,
"end_unit_id": 20
},
"evidence_refs": [
{
"lane_id": "spells",
"source_ref": {
"source_id": "session-alpha",
"start_unit_id": 10,
"end_unit_id": 10
}
}
],
"units": [
{ {
"id": 10, "id": 10,
"kind": "transcript_segment", "kind": "transcript_segment",
@@ -75,42 +55,53 @@ evidence publishes `contexts: []`.
"end_unit_id": 20 "end_unit_id": 20
} }
} }
] ]
}
]
}
``` ```
Each context requires a `context_ref` object and `evidence_refs` and `units` Each source unit has required `id`, `kind`, `text`, and self `ref` fields.
arrays. `context_ref` identifies the first and last included unit. Each `ref` contains `source_id`, `start_unit_id`, and `end_unit_id`, and both unit
evidence entry contains a selected `lane_id` and an original `source_ref`. A endpoints identify that unit's `id`. A unit may also contain source-owned
unit uses the existing source-unit shape: required `id`, `kind`, `text`, and `metadata`, an open-ended JSON object. Fixed unit and reference fields are
self `ref`, plus optional JSON-object `metadata`. Fixed payload objects reject strict: consumers must reject unknown fixed fields, malformed units, invalid
unknown fields; unit metadata may contain application-defined JSON values. self-references, units whose `source_id` differs from other units in the same
excerpt, and a payload that is not the array described here.
## Citations And Context The excerpt preserves each selected unit exactly as represented by the
validated generic source document. It does not add evidence-context-specific
annotations or reshape source-owned metadata.
`evidence_refs` are the authoritative citations. They identify the direct ## Selection And Citations
references emitted by accepted normalized artifacts. `context_ref` and the
units collection include those cited units plus nearby source units selected by
the configured window. They are explanatory context, not widened citations.
Only accepted outputs from the configured lane allowlist contribute. Rejected, The framework obtains direct source references only through typed evidence
failed, absent, and lane-filtered outputs do not contribute. The artifact never projections of accepted normalized artifacts in the configured lane allowlist.
contains raw input bytes, prompts, model responses, auxiliary reference It validates each reference against the current source document, expands its
content, credentials, or filesystem paths. range by `window_units` source-unit positions on each side, clamps at document
boundaries, and takes the union of all expanded ranges. The output contains
each selected source unit once in source-document position order, regardless
of numeric unit IDs. Repeated references, overlapping windows, and citations
from multiple lanes do not duplicate a unit. Rejected, failed, absent,
inactive, and unselected lanes contribute nothing.
## Ordering And Compatibility Normalized lane artifacts remain authoritative for citations and for which lane
cited a range. The excerpt has no lane attribution and must not be used to
reconstruct it. Its included nearby units provide reading context only; they
do not widen any citation in a lane artifact.
The selected lane allowlist is lexical. Contexts and units are in source The excerpt contains at most every generic source unit once. It can therefore
document position order, not numeric unit-ID order. Direct evidence entries equal the complete generic source document when coverage is broad or the
are deterministically ordered by lane and source reference. Overlapping or window is large. No byte-, token-, or compression-size guarantee is made, and
contiguous windows merge, and each source unit appears at most once in the the framework does not truncate the excerpt to meet an arbitrary size limit.
resulting contexts.
## Consumer Responsibilities And Data Handling
The artifact is additive to the JSON bundle and is not a lane payload, The artifact is additive to the JSON bundle and is not a lane payload,
normalized-output count, checkpoint, or generated reference. Consumers that normalized-output count, checkpoint, or generated reference. Consumers that
do not need it must tolerate the absent optional descriptor. Consumers that do do not need it must tolerate an absent descriptor. Consumers that do use it
use it should preserve the artifact and its schema identity with the run should validate the descriptor and payload before use, retain the artifact with
provenance, and should treat its source text and metadata as sensitive durable its schema identity when needed for a run record, and read citations from the
content. corresponding normalized lane artifacts.
The excerpt contains source-unit text and source-owned metadata and is durable
output. Treat it as sensitive source content, apply appropriate access controls
and retention, and do not assume its selected form is materially smaller or
less sensitive than the original input.

View File

@@ -24,7 +24,7 @@ root for the logical discovery described here.
| `warnings.json` | Accepted-output and run warnings. | | `warnings.json` | Accepted-output and run warnings. |
| `lanes/<safe-lane-id>.json` | One normalized artifact payload for each lane. | | `lanes/<safe-lane-id>.json` | One normalized artifact payload for each lane. |
| `chunk-map.json` | Optional accepted chunk map, when its export is enabled and available. | | `chunk-map.json` | Optional accepted chunk map, when its export is enabled and available. |
| `evidence-context.json` | Optional source-context artifact, when evidence publication is enabled. | | `evidence-context.json` | Optional selected source-unit excerpt, when evidence publication is enabled. |
JSON files are pretty-printed with a trailing newline. Lane payloads are JSON files are pretty-printed with a trailing newline. Lane payloads are
accepted only when their media type is `application/json`. accepted only when their media type is `application/json`.
@@ -71,12 +71,15 @@ output encoding fail.
Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for
that lane. Consumers should use the index descriptors schema identity rather that lane. Consumers should use the index descriptors schema identity rather
than infer a lane schema from its name. The current D&D payload contracts are than infer a lane schema from its name. The current D&D payload contracts are
[spells](dnd-spell-artifacts.md), [NPCs](dnd-npc-artifacts.md), [spells](dnd-spell-artifacts.md), [NPC registry](dnd-npc-registry-artifacts.md),
[NPC interactions](dnd-npc-interaction-artifacts.md), [NPC occurrences](dnd-npc-occurrence-artifacts.md),
[combat turns](dnd-combat-turn-artifacts.md), [combat turns](dnd-combat-turn-artifacts.md),
[item events](dnd-item-event-artifacts.md), [item registry](dnd-item-registry-artifacts.md),
[scene descriptions](dnd-scene-description-artifacts.md), and [item occurrences](dnd-item-occurrence-artifacts.md),
[enemy events](dnd-enemy-event-artifacts.md). [scene descriptions](dnd-scene-description-artifacts.md),
[enemy events](dnd-enemy-event-artifacts.md),
[location registry](dnd-location-registry-artifacts.md), and
[location occurrences](dnd-location-occurrence-artifacts.md).
## `manifest.json` ## `manifest.json`

View File

@@ -7,18 +7,21 @@ selectable keys, bindings, reference syntax, and default validator chains.
## Durable Artifact Contracts ## Durable Artifact Contracts
The seven lanes have separate durable wire contracts. This guide deliberately The ten lanes have separate durable wire contracts. This guide deliberately
does not repeat their JSON shapes or schemas. does not repeat their JSON shapes or schemas.
| Lane | Durable contract | | Lane | Durable contract |
| --- | --- | | --- | --- |
| Spells | [spell artifacts](../integrations/dnd-spell-artifacts.md) | | Spells | [spell artifacts](../integrations/dnd-spell-artifacts.md) |
| NPCs | [NPC artifacts](../integrations/dnd-npc-artifacts.md) | | NPC registry | [NPC registry artifacts](../integrations/dnd-npc-registry-artifacts.md) |
| Combat turns | [combat-turn artifacts](../integrations/dnd-combat-turn-artifacts.md) | | Combat turns | [combat-turn artifacts](../integrations/dnd-combat-turn-artifacts.md) |
| Item events | [item-event artifacts](../integrations/dnd-item-event-artifacts.md) | | Item occurrences | [item-occurrence artifacts](../integrations/dnd-item-occurrence-artifacts.md) |
| NPC interactions | [NPC-interaction artifacts](../integrations/dnd-npc-interaction-artifacts.md) | | Item registry | [item-registry artifacts](../integrations/dnd-item-registry-artifacts.md) |
| NPC occurrences | [NPC-occurrence artifacts](../integrations/dnd-npc-occurrence-artifacts.md) |
| Scene descriptions | [scene-description artifacts](../integrations/dnd-scene-description-artifacts.md) | | Scene descriptions | [scene-description artifacts](../integrations/dnd-scene-description-artifacts.md) |
| Enemy events | [enemy-event artifacts](../integrations/dnd-enemy-event-artifacts.md) | | Enemy events | [enemy-event artifacts](../integrations/dnd-enemy-event-artifacts.md) |
| Location registry | [location-registry artifacts](../integrations/dnd-location-registry-artifacts.md) |
| Location occurrences | [location-occurrence artifacts](../integrations/dnd-location-occurrence-artifacts.md) |
## Family Composition ## Family Composition
@@ -26,9 +29,9 @@ The D&D registrar registers the familys artifact codecs, extractors, typed
append-order mergers, normalizers, validators, prompt assets, fallback LLM append-order mergers, normalizers, validators, prompt assets, fallback LLM
profile asset, and default validator chains. Each extractor and normalizer has profile asset, and default validator chains. Each extractor and normalizer has
a stable module spec, explicit execution class, strict option decoding, and a a stable module spec, explicit execution class, strict option decoding, and a
typed builder. Scene chunking, every extractor, and NPC normalization are typed builder. Scene chunking, every extractor, and NPC, location, and item-registry
registered as `llm_backed`; the remaining current D&D mergers and normalizers normalization are registered as `llm_backed`; the remaining current D&D mergers
are `deterministic`. The metadata is available to catalog inspection and and normalizers are `deterministic`. The metadata is available to catalog inspection and
resolved-pipeline debug data and determines which selected bindings inherit the resolved-pipeline debug data and determines which selected bindings inherit the
pipeline profile. Configuration remains the canonical owner of the exact keys, pipeline profile. Configuration remains the canonical owner of the exact keys,
profile precedence, and validator order. profile precedence, and validator order.
@@ -41,50 +44,69 @@ the contracts above define durable data.
## Prompt Construction ## Prompt Construction
D&D extractors assemble prompts from an ordered manifest of shared and D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a
module-owned assets. Reuse the shared D&D system, evidence, identity, local `prompt.yaml` declaration and `instructions.md`; input-specific files
reference, and transcript assets instead of copying their text into individual such as a catalog, registry, grounding projection, or candidate collection are
modules. A manifests declared sequence, including cache-control placement, is local only when that module needs them. New extractor content uses its feature
part of the prompt behavior. subtree, while families with both extraction and normalization content use their
`extract` and `normalize` subtrees. Shared visual-provenance fragments use
the `common-dnd-` prefix. Production lane code belongs with its D&D codec,
extractor, normalizer, and validator packages; registry projections and
identity helpers remain in their owning entity packages rather than in a
consumer lane.
The owning modules manifest is the source of truth for which local and shared
assets are selected, their mount paths, their message order, cache controls,
and the files included in its prompt fingerprint. Shared fragments belong to
the D&D shared implementation and are selected by name rather than copied into
module directories. The root `assets` package is a content-only boundary; its
physical ownership and rationale are defined by
[ADR-0011](../adr/0011-centralize-llm-assets.md).
Put each rule at its narrowest owner:
- universal behavior belongs in the shared system asset;
- D&D-family behavior belongs in a selected `common-dnd-` asset;
- rules for an input projection belong with that input asset;
- lane-specific policy belongs in the modules `instructions.md`; and
- transport-envelope shape belongs in the private response schema.
A rule is eligible for the system prompt only when every D&D LLM prompt needs
it regardless of lane, inputs, or response shape. Module instructions must not
repeat rules selected from shared assets or schemas. Reintroduce such repetition
only after observational evaluation with representative transcripts shows that
it improves results at the intended target models and cost; structural prompt
tests alone are not that evidence.
Every maintained D&D LLM prompt selects `dnd-extraction` as its default Every maintained D&D LLM prompt selects `dnd-extraction` as its default
profile. The D&D registrar embeds that fallback profile with the maintained profile. The D&D registrar registers the fallback, while an operator can
OpenRouter model, timeout, and service-tier policy. An operator may provide a replace it with a complete profile of the same ID from the configured PromptKit
complete profile with the same ID through the configured PromptKit source; that source. Deployment profile selection is documented in
definition replaces the fallback rather than merging with it. The fallback
leaves reasoning and optional sampling controls unspecified. Deployment profile
selection and the maintained operator example are documented in
[Configuration](../config.md#promptkit-profiles). [Configuration](../config.md#promptkit-profiles).
All extraction prompts share this four-message rendered prefix: the system The D&D transcript assets have distinct consumers. Scene chunking consumes the
message without cache control, the identity message without cache control, the complete-session `common-dnd-transcript-full.md`, while extraction prompts
campaign-reference message with ephemeral cache control, and the chunk consume the current-chunk `common-dnd-transcript-chunk.md`. NPC, location, and
transcript message with ephemeral cache control. This gives equivalent item normalization instead mount the generic semantic-reconciliation
extraction requests the same reusable prefix through their source material. candidate and transcript-window presentation assets. Player, party, glossary,
and compatible campaign references provide disambiguating context only when
declared by the active prompt; they never establish evidence. Reference
material is canonically ordered before rendering so equivalent inputs remain
stable.
Extraction-evidence policy, generated NPC registries, spell catalogs, module Extraction prompts render the common system and identity messages first, then
tasks, and instructions follow the transcript because they are not universal cached campaign references and the cached chunk transcript. Evidence policy and
across all extraction lanes. The final instructions message carries ephemeral any lane-specific registry, catalog, or grounding projection follow that
cache control; evidence, registry, catalog, and task messages do not. Preserve prefix. The final module instructions message is ephemeral. This keeps the
this division when changing an extractor or its assets so prompt-cache behavior reusable extraction prefix identical while preserving the lane-specific suffix.
remains stable.
The other D&D LLM prompts intentionally follow different patterns. Scene Scene chunking intentionally uses a different order: system, cached campaign
chunking has no sibling extraction lane with which to share its full transcript, references, uncached module instructions, then the final ephemeral full
so it renders campaign references before its task and instructions, then places transcript. Entity normalization also has its own order: D&D system, mandatory
the cacheable full transcript last. NPC normalization keeps its task and generic protocol, ephemeral domain semantic instructions, generic candidate
cacheable instructions before the candidate collection, followed by the presentation, and final ephemeral generic transcript windows. These orders and
cacheable transcript windows: candidates must be available before their cache controls are prompt behavior; change them only through the owning
supporting evidence is evaluated, and those windows are not a cross-lane manifest and prompt declaration.
prefix. Mounted assets and their declared message order determine the prompt
fingerprint, so intentional prompt edits continue to invalidate stale
checkpoints.
All extractors use the shared prompt-input preparation rules. The current chunk
is copied into transcript material; player, party, glossary, and compatible
campaign references are context for disambiguation, not source evidence.
Reference prompt material is canonically ordered before it is rendered, which
keeps equivalent inputs stable across runs.
## Evidence, Candidates, And Normalization ## Evidence, Candidates, And Normalization
@@ -97,7 +119,8 @@ result.
Default chains keep responsibilities separate: structural validators assess the Default chains keep responsibilities separate: structural validators assess the
candidate, source-reference validators resolve cited ranges against the current candidate, source-reference validators resolve cited ranges against the current
source, durable-schema validation checks an approved representation, and source and require extraction evidence to stay within the current chunk,
durable-schema validation checks an approved representation, and
relatedness validators report advisory evidence concerns. The configured order relatedness validators report advisory evidence concerns. The configured order
is documented in is documented in
[Configuration](../config.md#production-validator-keys-and-default-chains). [Configuration](../config.md#production-validator-keys-and-default-chains).
@@ -107,14 +130,52 @@ the same comparison identity within one scene-scoped result. Normalization may
combine results from distinct scenes, so it intentionally does not apply that combine results from distinct scenes, so it intentionally does not apply that
rule. Configuration owns the exact validator key and chain position. rule. Configuration owns the exact validator key and chain position.
Normalizers are deterministic for spells, combat turns, item events, NPC Normalizers are deterministic for spells, combat turns, item occurrences, NPC
interactions, scene descriptions, and enemy events. They canonicalize display occurrences, scene descriptions, enemy events, and location occurrences. They
values and evidence, use source-document order for stable output, and issue canonicalize display values and evidence, use source-document order for stable
bounded warnings for changes or collapsed duplicates. The NPC normalizer is output, and issue bounded warnings for changes or collapsed duplicates. NPC,
the intentional exception: it first produces a deterministic candidate set, item, and location registry normalizers are intentional exceptions: each first
then uses a bounded structured-LLM proposal to reconcile identity groups. produces a deterministic candidate set, then may use a bounded structured-LLM
Invalid or unusable proposals retain the deterministic result and surface retry proposal to reconcile identity groups.
or fallback diagnostics; the model does not directly replace durable records.
## Semantic Registry Reconciliation
The three registry normalizers instantiate the domain-neutral
`internal/framework/semanticreconcile` engine with default bounds. Each
eligible candidate receives a contiguous, one-based `candidate_id` for that
request. The model sees that handle, the candidate label and source-free
evidence ranges, plus bounded transcript windows; it returns only duplicate
groups of supplied handles and one supplied canonical handle per group. It
never returns names, evidence, durable IDs, or replacement records. Identical
labels and evidence remain independently selectable because their handles are
distinct.
The generic core owns the mandatory handle protocol, candidate and transcript
presentation, the private response schema, source-reference validation,
candidate and combined-material limits, structured completion, proposal
assessment, stable group ordering, and typed plan-application mechanics. The
D&D prompt contributes its system message and registry-specific semantic
instructions. The generic registrar registers the shared prompt and schema;
the D&D registrar registers each consuming prompt and the fallback profile.
Fewer than two eligible candidates skips the LLM without a semantic warning.
An exceeded bound also skips the call and preserves the deterministic
preprocessed registry, adding the registry's bounded fallback warning. Invalid
structured output or discarded proposal groups use the normalizer's existing
retry contract; retry exhaustion preserves the safe deterministic or
partially applied result and emits its bounded fallback warning. Provider,
transport, cancellation, and context-material failures remain execution
errors.
Application remains typed and registry-owned. All three policies select the
canonical member's normalized display name, union member evidence in source
order, preserve ungrouped records, and derive durable identity only after
consolidation. NPC IDs derive from the final name. Item IDs also derive from
the final name, and a typed guard prevents currency aliases from crossing
denominations or mixing currency with non-currency records. Location IDs
derive from the final name and final evidence, preserving same-name,
parent/child, and distinct physical-place identities. Registry warning scopes,
reason codes, and postconditions remain outside the generic core.
## Generated References And Grounding ## Generated References And Grounding
@@ -124,29 +185,47 @@ producer provenance; consumers resolve the handed-off artifact into an
immutable, validated projection for each operation. External files are checked immutable, validated projection for each operation. External files are checked
during preparation, while generated artifacts are resolved at the handoff. during preparation, while generated artifacts are resolved at the handoff.
NPC registries are names-only grounding projections: they may canonicalize NPC and item registry consumers receive names-only grounding. Location
actors for spells and combat turns and are required for NPC interactions, but consumers receive a contextual selector containing the canonical name and the
they do not supply evidence. Scene-description registries are eligibility-only registry references needed to distinguish same-name places. The calling module
projections: they retain the current chunks classification data, not scene resolves those supplied selections locally and maps them into the unchanged
prose or evidence, and exist to route combat extraction. Enemy-event extraction durable ID/name pair; an unknown or ambiguous selection rejects the complete
also projects combat turns to `actor` and `turn_kind` and filters NPC occurrence result rather than accepting a partial mapping. The NPC registry
interactions to `combat_opponent` names and kinds. These compact projections, additionally supplies names-only actor grounding to spells, combat turns, and
like NPC grounding, are source-free guidance and never event evidence. enemy events.
Registry references establish a registry identity and may disambiguate a
selection, but never become occurrence evidence. Each occurrence keeps its own
current-transcript source references, even when it was grounded through the
same registry record.
Scene descriptions are eligibility-only projections: they retain current-chunk
classification data, not scene prose or evidence, and exist to route combat
extraction. Enemy-event extraction also projects combat turns to `actor` and
`turn_kind` and filters NPC occurrences to `combat_opponent` names and kinds.
These projections are guidance only and never event evidence.
## Lane-Specific Rules ## Lane-Specific Rules
The following differences are intentional and should remain explicit when a The following differences are intentional and should remain explicit when a
shared helper changes. shared helper changes.
Shared D&D text comparison is identified by `dnd.text_comparison.v1`. Any
semantic change requires an explicit policy-version review for every affected
identity, mapping, normalization, and validator policy; helper source is not a
checkpoint fingerprint.
| Lane | Intentional behavior | | Lane | Intentional behavior |
| --- | --- | | --- | --- |
| Spells | May use a spell-catalog overlay and optional NPC grounding; the catalog validator supplies domain-specific semantic checks. | | Spells | May use a spell-catalog overlay and optional NPC grounding; the catalog validator supplies domain-specific semantic checks. |
| NPCs | Does not consume an NPC registry. Its normalizer is the LLM-assisted reconciliation exception described above. | | NPC registry | Establishes transcript-grounded NPC identities, including factual third-party mentions, without assigning occurrence categories. It does not consume an NPC registry, and its normalizer is the LLM-assisted reconciliation exception described above. |
| Combat turns | Requires a scene-description artifact. It calls the LLM only for an exact `combat` classification; exact non-combat classifications return an accepted empty result, while missing or mismatched classifications return an empty result with a bounded warning. Optional NPC grounding never becomes evidence. | | Combat turns | Requires a scene-description artifact. It calls the LLM only for an exact `combat` classification; exact non-combat classifications return an accepted empty result, while missing or mismatched classifications return an empty result with a bounded warning. Optional NPC grounding never becomes evidence. |
| Item events | Uses campaign context for disambiguation but has no NPC-registry or scene-description dependency. | | Item occurrences | Requires the normalized item registry for exact deterministic grounding at extraction and normalization. Campaign context may disambiguate, but the registry never becomes occurrence evidence. |
| NPC interactions | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. | | Item registry | Produces source-grounded item types and unique designations. Its LLM-assisted reconciliation is proposal-only, preserves distinct currency denominations and item types, and does not create per-instance identities. |
| NPC occurrences | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. It separately emits cited current-transcript occurrence facts, including `mentioned`, rather than deriving them from registry provenance. |
| Scene descriptions | Produces the classifications consumed by combat routing; it does not consume an NPC registry or provide evidence for combat artifacts. | | Scene descriptions | Produces the classifications consumed by combat routing; it does not consume an NPC registry or provide evidence for combat artifacts. |
| Enemy events | Requires NPC, scene-description, combat-turn, and NPC-interaction artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. | | Enemy events | Requires NPC, scene-description, combat-turn, and NPC-occurrence artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. |
| Location registry | Produces a source-anchored, session-scoped registry from stable proper names or unique in-world designations. Its LLM-assisted reconciliation is proposal-only and never collapses same-name places without validated identity and evidence rules. |
| Location occurrences | Requires the normalized location registry for both extraction and normalization. Its [durable occurrence categories](../integrations/dnd-location-occurrence-artifacts.md#occurrence-categories) distinguish explicit speculation from unsupported inference; the deterministic normalizer enforces exact registry grounding and never turns registry provenance into occurrence evidence. |
The combat and scene-description contracts describe their exact handoff and The combat and scene-description contracts describe their exact handoff and
empty-result behavior in more detail: empty-result behavior in more detail:

View File

@@ -24,6 +24,9 @@ adapter does not own source evidence, artifact conversion, normalization, or
durable schemas. Those responsibilities remain with the module and its durable schemas. Those responsibilities remain with the module and its
[integration contract](../integrations/). [integration contract](../integrations/).
The calling module also resolves contextual entity selections and attaches any
application identity; PromptKit and this adapter do not own entity identity.
`PromptKitClient` validates the request target and prompt identity, maps each `PromptKitClient` validates the request target and prompt identity, maps each
named material to a PromptKit inline artifact while preserving its origin URI, named material to a PromptKit inline artifact while preserving its origin URI,
passes the supplied request session through to PromptKit's direct per-run passes the supplied request session through to PromptKit's direct per-run
@@ -99,7 +102,8 @@ because it changes scheduling rather than execution semantics.
Production construction creates one PromptKit client and wraps it in one Production construction creates one PromptKit client and wraps it in one
scheduled client. The scheduler has a fixed, positive permit limit, serves scheduled client. The scheduler has a fixed, positive permit limit, serves
queued calls in FIFO order, and removes a queued call when its context is queued calls in FIFO order, and removes a queued call when its context is
cancelled. A granted permit is released exactly once on every completion path. cancelled. It rechecks the caller context after admission and before dispatch.
A granted permit is released exactly once on every completion path.
The scheduled wrapper surrounds every `CompleteStructured` call, so concurrent The scheduled wrapper surrounds every `CompleteStructured` call, so concurrent
lanes, pipeline retries, and LLM-backed validators share the same provider-call lanes, pipeline retries, and LLM-backed validators share the same provider-call
@@ -125,16 +129,40 @@ the corresponding PromptKit filesystems and rejects invalid roots, unreadable
assets, duplicate paths, and missing prompt or schema files during preparation. assets, duplicate paths, and missing prompt or schema files during preparation.
Fallback assets receive a safe content digest for checkpoint identity; raw Fallback assets receive a safe content digest for checkpoint identity; raw
paths and bytes are never included. The frameworks `promptfs` helper combines paths and bytes are never included. The frameworks `promptfs` helper combines
module-owned prompt files with reusable domain fragments without making the module-selected prompt files with reusable domain fragments without making the
framework depend on D&D content. framework depend on D&D content.
Each LLM-backed module owns its prompt declaration, package-specific assets, LLM-facing content is embedded once by the root `assets` package. Each consumer
and private response schema. Shared D&D wording is owned by the D&D shared uses only its scoped subtree, while the module retains ownership of its prompt
asset package; the detailed D&D conventions are in declaration, ordered manifest, private response-schema identity, and
[D&D Module Internals](dnd.md). The mounted prompt assets used by a module also registration. Shared D&D fragments are selected by D&D's shared implementation;
determine its prompt fingerprint. Schema loaders validate JSON, attach identity the detailed convention is in [D&D Module Internals](dnd.md). This physical
and digest metadata, make defensive copies, and expose diagnostics without raw arrangement and its data-only boundary are defined by
schema bytes. [Architecture](../policy/architecture.md) and
[ADR-0011](../adr/0011-centralize-llm-assets.md), rather than by this runtime
guide.
The generic registrar is the sole production registration owner for the
semantic-reconciliation default prompt and private response schema. The
domain-neutral reconciliation package also exposes only its mandatory protocol
and candidate/transcript presentation files for domain prompt manifests. D&D
registry normalizers mount those files while retaining ownership and hashing
of their D&D system message, semantic instructions, and complete prompt
declaration. The response schema is therefore registered once even though
several typed normalizers select it.
Mounted prompt assets determine a module's fingerprint. The fingerprint hashes
only the module and shared files explicitly selected by its manifest, so an
unrelated asset does not invalidate a checkpoint. Schema loaders validate JSON,
attach identity and digest metadata, make defensive copies, and expose
diagnostics without raw schema bytes.
Semantic-reconciliation normalizers extend this identity with the shared
response-schema digest, framework policy version, and complete limit-policy
digest. Their manifest metadata records the same content-free prompt, schema,
policy, and limit identities together with domain identity and normalization
policies. Request-local handles, source material, proposal content, and raw
asset bytes are not checkpoint metadata.
Private response schemas validate a model transport envelope. They are not the Private response schemas validate a model transport envelope. They are not the
durable artifact schema and should not be documented as an external wire durable artifact schema and should not be documented as an external wire
@@ -168,7 +196,9 @@ structured-output validation. The adapter reports an empty result, validation
failure, empty structured body, or decode failure as failure, empty structured body, or decode failure as
`ErrInvalidStructuredOutput`, while retaining the returned raw bytes and debug `ErrInvalidStructuredOutput`, while retaining the returned raw bytes and debug
material when they exist. Provider failures remain operational errors rather material when they exist. Provider failures remain operational errors rather
than output-validation failures. than output-validation failures. Apart from documented context, capacity, and
invalid-output categories, provider error values and types do not cross the
adapter error chain; callers receive only a credential-redacted diagnostic.
When PromptKit rejects backend admission before generation, the adapter maps When PromptKit rejects backend admission before generation, the adapter maps
`promptkit.ErrCapacityExceeded` to `promptkit.ErrCapacityExceeded` to

View File

@@ -42,14 +42,23 @@ generic source references and must use the codec's exact Go type. It does not
interpret surrounding context or publish files; the pipeline validates the interpret surrounding context or publish files; the pipeline validates the
capability during preparation and the output boundary owns publication. See capability during preparation and the output boundary owns publication. See
the [Published Evidence Context contract](../integrations/evidence-context.md) the [Published Evidence Context contract](../integrations/evidence-context.md)
for the durable result. for the durable source-unit excerpt. Lane artifacts retain citation and lane
provenance; the framework does not add either to that published excerpt.
An artifact family is broader than a module: it owns the cohesive domain
feature across its artifact type, codec, stage modules, validators, prompt
policy, schemas, identity helpers, and reference projections. An extractor and
normalizer in one artifact family remain independently registered modules in
their respective pipeline stages. This ownership vocabulary does not create a
new registry or change the fixed pipeline.
## Production Composition ## Production Composition
Production composition is intentionally split by family: Production composition is intentionally split by family:
- The generic registrar provides the unit chunker, generic JSON validators, - The generic registrar provides the unit chunker, generic JSON validators,
and JSON output encoder. JSON output encoder, and shared semantic-reconciliation prompt and response
schema assets.
- The Seriatim registrar provides the transcript input adapter. Its external - The Seriatim registrar provides the transcript input adapter. Its external
input behavior is defined by the [Seriatim contract](../integrations/seriatim.md). input behavior is defined by the [Seriatim contract](../integrations/seriatim.md).
- The D&D registrar provides its codecs, extractors, mergers, normalizers, - The D&D registrar provides its codecs, extractors, mergers, normalizers,
@@ -60,6 +69,36 @@ The CLI owns the composition that invokes these registrars. A module package
may register its own family but must not assemble the CLI or make framework may register its own family but must not assemble the CLI or make framework
packages depend on production extensions. packages depend on production extensions.
## Semantic Reconciliation
`internal/framework/semanticreconcile` is a domain-neutral strategy used by a
typed normalize module; it is not itself a selectable stage module. A
source-backed artifact-family normalizer projects its deterministic records
into contextual candidates and owned typed record envelopes, supplies its
chosen prompt identity and resolved LLM profile, and constructs an engine with
explicit limits. The core filters invalid evidence, assigns contiguous
request-local integer handles, renders bounded candidate and transcript
materials, invokes the structured-completion boundary, and assesses the
returned duplicate groups into a stable non-overlapping plan.
The normalizer then applies that plan through a typed `ApplicationPolicy`. The
core preserves ungrouped records, contribution order, and provenance while the
artifact family owns group guards, field and evidence consolidation, durable
ID derivation, retry and fallback presentation, warnings, and postconditions.
Request-local handles do not enter the typed value or durable artifact. Fewer
than two eligible candidates skips model invocation; exceeding a candidate or
combined-material bound preserves the deterministic result under the family's
fallback policy. Provider, transport, cancellation, and context-construction
failures remain execution errors.
The core supplies a conservative generic prompt and the single private
response schema. A domain prompt may substitute its semantic instructions but
mounts the core-owned protocol and candidate/transcript presentation assets.
Prompt, schema, policy, and limit identities participate in manifest metadata
and checkpoint fingerprints. The generic registrar owns production
registration of those shared assets; a consuming domain registrar owns only
its domain prompt.
## Adding Or Changing A Module ## Adding Or Changing A Module
1. Choose the pipeline stage and the typed artifact boundary. Put external 1. Choose the pipeline stage and the typed artifact boundary. Put external

View File

@@ -29,6 +29,8 @@ physical state roots.
| Generic models | **internal/core/source**, **internal/core/artifacts**, **internal/framework/contracts** | Source documents and chunks, manifests and provenance, plus typed artifact, reference, validation, output, and structured-completion contracts. | | Generic models | **internal/core/source**, **internal/core/artifacts**, **internal/framework/contracts** | Source documents and chunks, manifests and provenance, plus typed artifact, reference, validation, output, and structured-completion contracts. |
| Pipeline framework | **internal/framework/pipeline** | Registries, profile and reference resolution, typed preparation, validation, retry coordination, ordered execution, handoff, and result assembly. | | Pipeline framework | **internal/framework/pipeline** | Registries, profile and reference resolution, typed preparation, validation, retry coordination, ordered execution, handoff, and result assembly. |
| LLM and prompt runtime | **internal/framework/llm**, **internal/framework/promptfs** | Provider-neutral structured completions, scheduling, profile recording, prompt assets, schema registration, and credential-shaped-value redaction. | | LLM and prompt runtime | **internal/framework/llm**, **internal/framework/promptfs** | Provider-neutral structured completions, scheduling, profile recording, prompt assets, schema registration, and credential-shaped-value redaction. |
| Semantic reconciliation | **internal/framework/semanticreconcile** | Bounded source-backed candidate preparation, request-local handle proposals, deterministic assessment, typed plan application, and reconciliation identity metadata; see [Module Internals](modules.md#semantic-reconciliation) and [D&D Module Internals](dnd.md#semantic-registry-reconciliation). |
| Embedded LLM content | **assets** | Read-only centralized LLM-facing content, scoped by its consuming package; see [LLM Runtime](llm.md#prompt-and-schema-assets) and [D&D Module Internals](dnd.md#prompt-construction). |
| Runtime state | **internal/core/fileio**, **internal/core/debugbundle**, **internal/framework/checkpoint**, **internal/framework/chunkplan**, **internal/framework/chunkmap**, **internal/framework/debug** | Confined atomic files, debug bundles, checkpoint and chunk-plan state, accepted chunk maps, and pipeline-facing debug recording. | | Runtime state | **internal/core/fileio**, **internal/core/debugbundle**, **internal/framework/checkpoint**, **internal/framework/chunkplan**, **internal/framework/chunkmap**, **internal/framework/debug** | Confined atomic files, debug bundles, checkpoint and chunk-plan state, accepted chunk maps, and pipeline-facing debug recording. |
| Production extensions | **internal/modules/generic**, **internal/modules/seriatim**, **internal/modules/dnd** | Domain-neutral extensions, Seriatim input support, and D&D extraction families registered into the production catalog. | | Production extensions | **internal/modules/generic**, **internal/modules/seriatim**, **internal/modules/dnd** | Domain-neutral extensions, Seriatim input support, and D&D extraction families registered into the production catalog. |
@@ -48,8 +50,9 @@ the CLI composition boundary.
composition, and path safety. composition, and path safety.
- [LLM Runtime](llm.md): structured completion, scheduling, prompt assets, - [LLM Runtime](llm.md): structured completion, scheduling, prompt assets,
profiles, and secret handling. profiles, and secret handling.
- [Module Internals](modules.md): generic extension registration, module - [Module Internals](modules.md): generic extension registration, artifact
construction, validation, and reference mechanics. families, module construction, semantic reconciliation, validation, and
reference mechanics.
- [D&D Module Internals](dnd.md): shared D&D extractor conventions, generated - [D&D Module Internals](dnd.md): shared D&D extractor conventions, generated
reference projections, and lane-specific exceptions. Durable D&D and reference projections, and lane-specific exceptions. Durable D&D and
Seriatim data shapes remain in the [integration contracts](../integrations/). Seriatim data shapes remain in the [integration contracts](../integrations/).

View File

@@ -46,16 +46,18 @@ External reference materialization happens before preparation. The materializer
checks that each slot is declared by the selected module, resolves a file path checks that each slot is declared by the selected module, resolves a file path
relative to the correct configuration or working-directory origin, reads relative to the correct configuration or working-directory origin, reads
UTF-8 text, verifies media type and size limits, and retains bounded UTF-8 text, verifies media type and size limits, and retains bounded
provenance. A generated-artifact selector remains declared but has no bytes provenance. For a positive slot limit, it reads at most the limit plus one byte
until its producing step completes. and rejects overflow before retaining content. A generated-artifact selector
remains declared but has no bytes until its producing step completes.
Preparation is the construction boundary. It validates the resolved shape and Preparation is the construction boundary. It validates the resolved shape and
registry set, clones the resolved data, then constructs the input adapter, registry set, clones the resolved data, then constructs the input adapter,
chunker, stage-local validators, every typed lane, and output encoder with chunker, stage-local validators, every typed lane, and output encoder. Each
cloned options, references, and shared dependencies. It also collects stable registered builder receives its own cloned build request immediately before its
checkpoint fingerprints. Missing registrations, incompatible typed entries, module-owned code runs. Preparation also collects stable checkpoint
nil implementations, and constructor failures are reported before source fingerprints. Missing registrations, incompatible typed entries, nil
parsing or any stage operation begins. implementations, and constructor failures are reported before source parsing
or any stage operation begins.
An output encoder can opt into source-evidence publication through its output An output encoder can opt into source-evidence publication through its output
policy. Preparation keeps the configured lane allowlist and active lanes policy. Preparation keeps the configured lane allowlist and active lanes
@@ -115,9 +117,12 @@ for started workers, and prevents output encoding.
Every chunk, extract, merge, and normalize candidate passes its resolved Every chunk, extract, merge, and normalize candidate passes its resolved
validator chain. Validators receive immutable canonical input appropriate to validator chain. Validators receive immutable canonical input appropriate to
their target: chunks, typed values, or serialized codec bytes. They may their target: chunks, codec-decoded typed candidates, or serialized codec
approve, approve with warnings, reject, or fail. A rejection is an ordinary bytes. Each typed validator receives a newly decoded value from the one
pipeline result; a validator error is a framework error. candidate serialization for that attempt, while serialized validators receive
separately owned representation bytes and schema metadata. They may approve,
approve with warnings, reject, or fail. A rejection is an ordinary pipeline
result; a validator error is a framework error.
The runner applies the binding's retry policy around a stage operation and its The runner applies the binding's retry policy around a stage operation and its
complete validation chain. It preserves warnings only from the final accepted complete validation chain. It preserves warnings only from the final accepted

View File

@@ -47,6 +47,9 @@ The serialized
they do not describe a current public state surface. they do not describe a current public state surface.
Ordered-step lane checkpoints include the step identity in their storage scope. Ordered-step lane checkpoints include the step identity in their storage scope.
Accepted step and lane identities are encoded injectively before becoming
filesystem path components, while ordinary safe identifiers retain their
readable paths.
When a later lane consumes a generated artifact, its dependency fingerprints When a later lane consumes a generated artifact, its dependency fingerprints
include the producer's artifact kind, complete schema identity, media type, include the producer's artifact kind, complete schema identity, media type,
canonical content digest, and size. Ordinary resume compares those fingerprints canonical content digest, and size. Ordinary resume compares those fingerprints

View File

@@ -110,9 +110,9 @@ are defined in [Accepted Chunk Map](integrations/chunk-map.md). An optional
[evidence context](integrations/evidence-context.md) contains source-unit text [evidence context](integrations/evidence-context.md) contains source-unit text
and metadata. It is not a cache or debug artifact: retain it with the output and metadata. It is not a cache or debug artifact: retain it with the output
bundle only for as long as consumers need it, and apply source-content access bundle only for as long as consumers need it, and apply source-content access
controls to the entire bundle. Selected lanes may collectively cite most of a controls to the entire bundle. Its selected source-unit excerpt may include
transcript, so a broad allowlist can make the evidence artifact nearly as every source unit once when coverage is broad or its configured window is
sensitive and large as the source itself. large, so do not assume a byte or token reduction or reduced sensitivity.
## Chunk-Plan Cache ## Chunk-Plan Cache

View File

@@ -24,6 +24,12 @@ DAGs or a general workflow language. Every stage remains explicit; general
chunking, merging, or normalization behavior must not be hidden inside an chunking, merging, or normalization behavior must not be hidden inside an
extractor. extractor.
A stage module is one configured implementation of one pipeline stage. An
artifact family is the cohesive domain feature that owns an artifact across
the explicit stages and supporting codecs, validators, prompts, identity
rules, and reference projections. Artifact-family ownership does not combine
stages or alter the fixed pipeline.
Input and chunking are pipeline-wide. Each selected artifact lane owns its Input and chunking are pipeline-wide. Each selected artifact lane owns its
extract, merge, and normalize stages, and the output stage aggregates the run's extract, merge, and normalize stages, and the output stage aggregates the run's
lane outcomes. lane outcomes.
@@ -39,11 +45,24 @@ implementations. Domain-neutral model and framework layers provide reusable
policy, contracts, and orchestration. Concrete input, pipeline, output, and policy, contracts, and orchestration. Concrete input, pipeline, output, and
validation extensions depend inward on those generic layers. validation extensions depend inward on those generic layers.
Semantic reconciliation is one such domain-neutral framework mechanism. It
prepares bounded source context, invokes a shared model-judgment protocol,
validates proposals, and applies safe plans through typed policies supplied by
the consuming artifact family. It does not own domain identity, durable IDs,
warning semantics, or artifact construction rules.
Generic layers must not depend on production extensions. Concrete extensions Generic layers must not depend on production extensions. Concrete extensions
must not compose the application or take ownership of process behavior. The must not compose the application or take ownership of process behavior. The
current packages implementing these layers are inventoried in current packages implementing these layers are inventoried in
[Internal Overview](../internal/overview.md). [Internal Overview](../internal/overview.md).
The root `assets` package is a content-only dependency leaf. It may expose a
read-only embedded filesystem, but it must contain no business logic and must
not depend on `internal` packages or PromptKit. Consumers scope that filesystem
to the content they own; the root package is not a behavioral registry or a
public extension contract. The rationale and compatibility consequence are
recorded in [ADR-0011](../adr/0011-centralize-llm-assets.md).
The following dependency boundaries are mandatory: The following dependency boundaries are mandatory:
- extractors and validators do not depend on concrete input adapters; - extractors and validators do not depend on concrete input adapters;
@@ -73,6 +92,12 @@ Extract modules own artifact semantics, prompt use, response schemas, and
domain interpretation. Domain-specific concepts remain in the relevant module, domain interpretation. Domain-specific concepts remain in the relevant module,
validator, shared domain helper, and artifact contract. validator, shared domain helper, and artifact contract.
Physical centralization of LLM-facing content does not transfer semantic
ownership from those modules. Modules retain their manifests, response-schema
identities, prompt ordering, and registration, while reading only their scoped
content subtree. Generic framework code remains domain-neutral when it reads
its own scoped generic assets from the shared content container.
Typed artifact registrations declare one stable artifact kind and exact Go Typed artifact registrations declare one stable artifact kind and exact Go
type from extraction through merge, normalization, and semantic validation. type from extraction through merge, normalization, and semantic validation.
Pipeline resolution requires a compatible codec and matching kind-specific Pipeline resolution requires a compatible codec and matching kind-specific
@@ -170,6 +195,17 @@ The caller of the LLM owns prompt selection, prompt inputs, response schema,
and interpretation of structured output. Provider adapters do not own source- and interpretation of structured output. Provider adapters do not own source-
or domain-specific prompt logic. or domain-specific prompt logic.
When a model selects an application entity, callers must supply a contextual
selection and deterministically attach the opaque application identity whenever
the selection resolves exactly. Models do not receive or reproduce opaque
application identifiers. Semantic reconciliation may instead expose
contiguous, one-based candidate handles that exist only for one request;
deterministic code resolves them before typed application, and they never
become durable identity. This is the approved request-local-label application
of [ADR-0012](../adr/0012-resolve-opaque-entity-identifiers-deterministically.md)
recorded by
[ADR-0013](../adr/0013-use-request-local-candidate-handles-for-semantic-reconciliation.md).
LLM calls and other external operations accept cancellation and respect LLM calls and other external operations accept cancellation and respect
timeouts. Concurrency control belongs in shared runtime plumbing rather than in timeouts. Concurrency control belongs in shared runtime plumbing rather than in
individual modules. individual modules.

File diff suppressed because it is too large Load Diff

View File

@@ -7,19 +7,6 @@ not as committed release dates.
## Near-Term D&D Pipeline ## Near-Term D&D Pipeline
### Location Extraction
- Add a D&D artifact for locations visited by the party or otherwise mentioned
in the transcript.
- Distinguish observed visits from references, plans, recalled places, and
uncertain or inferred locations so a mention alone is not reported as a
visit.
- Preserve transcript evidence for each visit or mention and reconcile aliases,
nested places, and repeated appearances without collapsing distinct
locations that share a generic name.
- Define how the location artifact should ground later narrative reports and
whether future event artifacts should retain canonical location identities.
### Evaluate Spell Extraction And Normalization ### Evaluate Spell Extraction And Normalization
- Evaluate ordinary extraction retries and the completed normalization path - Evaluate ordinary extraction retries and the completed normalization path
@@ -37,26 +24,52 @@ not as committed release dates.
## Shared Normalization And Quality Work ## Shared Normalization And Quality Work
### Generic LLM-Assisted Deduplication The implemented source-backed core and initial D&D registry adoption are
described by [Module Internals](../internal/modules.md#semantic-reconciliation)
and
[D&D Module Internals](../internal/dnd.md#semantic-registry-reconciliation).
The [Semantic Reconciliation Roadmap](semantic-reconciliation.md) retains the
original feature scope; the sections below keep broader extensions deferred.
- Add a reusable normalizer that asks an LLM to identify duplicate sets in a ### Large-Collection Semantic Reconciliation
list and propose one replacement element for each set.
- Define the minimum domain-neutral input contract, initially an ordered list
whose elements have stable unique IDs. Artifact-kind registrations or
adapters may expose that structure without moving domain rules into the
generic package.
- Keep mutation deterministic: parse and validate the model's duplicate groups,
require every referenced ID to exist, reject overlapping or malformed groups,
prevent unrelated insertion or deletion, and apply only approved replacement
operations in code.
- Preserve provenance needed for audit and downstream validation, and emit
warnings describing every collapsed group.
- Evaluate batching and context-window limits before applying the normalizer to
large artifact collections.
The model may use its own domain knowledge to judge semantic duplication; the - Evaluate deterministic candidate blocking only after representative registry
generic implementation is responsible only for the common proposal contract, inputs exceed the active roadmap's bounded single-request limits. Blocking
safety checks, and deterministic application of accepted changes. should use cheap, explainable signals to form plausible comparison sets while
preserving the possibility that a duplicate appears outside a lexical name
match.
- Define correctness for candidates that appear in more than one block,
conflicting canonical selections, transitive identity across blocks, retry
isolation, and deterministic final ordering before implementation.
- Prefer a reconciliation graph or union plan with explicit conflict checks
over arbitrary fixed-size slices. Never silently treat a batch boundary as
evidence that two candidates are distinct.
- Record per-request bounds, block provenance, model calls, discarded
proposals, and final group derivation well enough to audit a collapse.
### Operator-Selected Semantic Policies
- Consider allowing an operator to select an approved semantic-policy prompt
for a typed reconciliation module without replacing the shared protocol,
response schema, or deterministic safety rules.
- Define the trusted asset source, configuration syntax, compatibility checks,
startup validation, provenance, prompt fingerprinting, checkpoint effects,
and support boundary before exposing the option.
- Prefer selection among registered, typed-policy-compatible prompt assets over
arbitrary filesystem prompt paths. Do not add this flexibility until an
operator workflow requires it; artifact-family-owned policy remains simpler
and safer for the initial implementation.
### Broader Reconciliation Inputs And Module Selection
- Revisit alternate context providers when a concrete non-source-backed entity
collection needs semantic reconciliation. Any extension must preserve the
same request-local identity, deterministic proposal validation, provenance,
and typed application guarantees.
- Consider a selectable generic normalizer only if Notarius gains a real
domain-neutral typed artifact contract that can safely support it. Do not
weaken exact artifact registration or introduce reflection-based arbitrary
JSON mutation merely to expose a universal module key.
### Validation And Review ### Validation And Review
@@ -113,6 +126,18 @@ checkpoint reuse, when an older artifact may be decoded or adapted, and when a
producer or all dependents must be recomputed. Do not add a general migration producer or all dependents must be recomputed. Do not add a general migration
framework until an actual contract change requires one. framework until an actual contract change requires one.
### Artifact-family-oriented physical packaging
[ADR-0004](../adr/0004-package-modules-by-domain.md) currently groups production
extensions by domain and then by pipeline stage. After artifact-family
ownership terminology is established and more families span extraction,
normalization, validation, codecs, references, and assets, reassess whether a
feature-first physical layout would improve navigation and reduce scattered
changes enough to justify a repository-wide package migration. Any change must
address Go dependency cycles, registrar ownership, stable public module keys,
and supersession of the affected ADR-0004 decision. Conceptual artifact-family
ownership does not by itself require this move.
## Blue-Sky Platform And Operations ## Blue-Sky Platform And Operations
These ideas are intentionally less specified. Promote one into an earlier These ideas are intentionally less specified. Promote one into an earlier

View File

@@ -1,649 +0,0 @@
# D&D Location Tracking Implementation Plan
## Objective
Implement the target state in [D&D Location Tracking](location.md): an
evidence-grounded `dnd/locations` registry lane and a dependent
`dnd/location-occurrences` lane, including conservative location identity,
shared D&D entity-reconciliation infrastructure, production validation,
generated-reference wiring, maintained examples, and current documentation.
This is an ordered implementation plan for a `gpt-5.6-terra` coding agent.
Implement one stage per prompt, in order. Finish each stage's tests and leave
the repository coherent before proceeding. Do not implement later-stage
production registrations early merely to make an incomplete feature selectable.
All stages must follow:
- [Architecture Policy](../policy/architecture.md)
- [Testing Policy](../policy/testing.md)
- [Documentation Policy](../policy/documentation.md)
- the D&D conventions in [D&D Module Internals](../internal/dnd.md)
- the durable policy decisions in [the feature roadmap](location.md)
Use behavior-level tests. Do not add tests that merely freeze source layout,
exact prompt wording, message counts, shared-prefix length, or other incidental
implementation details. Keep tests deterministic, offline, and owned by the
component whose behavior they exercise.
## Stage 1: Define Location Domain Types And Identity
### Goal
Establish the in-process contracts and deterministic identity policy on which
both lanes depend.
### Work
- Extend `internal/modules/dnd/types.go` with:
- `LocationListKind` = `dnd/location-list`;
- `LocationOccurrenceListKind` = `dnd/location-occurrence-list`;
- `LocationList`, `Location`, `LocationOccurrenceList`,
`LocationOccurrence`, and `LocationOccurrenceKind`;
- exact JSON members and the four occurrence constants specified in
`location.md`.
- Add `internal/modules/dnd/locations/identity`.
- Implement display normalization and comparison normalization consistently
with the existing NPC identity policy. Share a lower-level comparison helper
only if doing so preserves NPC behavior exactly; otherwise keep the small
policy-specific function explicit.
- Implement the versioned compact-JSON-array ID derivation contract from
`location.md`, including ID syntax checks and immutable list validation.
- Identity validation must require correctly derived, unique IDs while allowing
two records to have the same comparison name when their evidence anchors
differ.
- Add focused tests for Unicode normalization, whitespace, apostrophes,
deterministic encoding, evidence ordering, same-name/different-anchor IDs,
malformed IDs, missing evidence, and non-mutation.
### Acceptance Criteria
- The types compile without production registration.
- ID derivation exactly follows the documented five-element compact JSON input.
- Same normalized name plus different earliest evidence yields different IDs.
- Validation does not reject same-name records solely because their names
match, and it reports duplicate or mismatched IDs deterministically.
- `go test ./internal/modules/dnd/locations/... ./internal/modules/dnd/...` passes
for the packages available at this stage.
### Prompt Size
Small enough for one implementation prompt.
## Stage 2: Add Durable Codecs And Schemas
### Goal
Create strict durable JSON ownership for both artifact kinds without exposing
either lane as a selectable pipeline yet.
### Work
- Add `internal/modules/dnd/codec/locations` and
`internal/modules/dnd/codec/locationoccurrences`, following the existing D&D
candidate/approved codec pattern.
- Add embedded Draft 2020-12 schemas with the IDs, names, root members, required
fields, enums, source-reference shape, and `additionalProperties: false`
contracts in `location.md`.
- Keep both schemas at `v1`.
- Support strict candidate decoding before semantic approval and strict durable
encoding/decoding after approval.
- Add representative valid fixtures and tests for schema metadata, defensive
schema bytes, empty arrays, unknown fields, missing fields, invalid types,
invalid enum values, malformed source references, invalid ID syntax, and
round trips.
### Acceptance Criteria
- Each codec advertises the correct artifact kind and metadata count.
- Candidate decoding preserves semantic mistakes for validators while
rejecting structurally invalid JSON.
- Approved encoding and decoding enforce the durable shape.
- `go test ./internal/modules/dnd/codec/locations/... ./internal/modules/dnd/codec/locationoccurrences/...`
passes offline.
### Prompt Size
Small enough for one implementation prompt.
## Stage 3: Extract Shared Entity-Reconciliation Infrastructure
### Goal
Create the D&D-shared, domain-safe proposal machinery needed by both NPC and
location normalization, without changing NPC production behavior yet.
### Work
- Add `internal/modules/dnd/shared/entityreconcile`.
- Move or generalize the reusable behavior currently owned by
`internal/modules/dnd/normalize/npcs/context_material.go` and `proposal.go`:
- assign deterministic opaque candidate keys such as `candidate-000001` in
input order;
- clone candidate names and source references;
- build bounded transcript windows in source-document order;
- omit candidates whose references cannot safely produce context;
- coalesce overlapping or adjacent windows without mutating the source;
- define the private `duplicate_groups` proposal with `members` and
`canonical` candidate keys;
- reject blank, unknown, repeated, ineligible, overlapping, too-small, or
canonical-not-a-member groups; and
- return defensive, immutable assessment data identifying only safe groups.
- Keep LLM calls, retry decisions, artifact mutation, canonical-name policy,
durable ID derivation, and warning wording out of this package.
- Add a shared prompt instruction asset that states the key-copying and
proposal-safety contract without NPC- or location-specific identity rules.
- Add a shared private structured-response schema and loader/registration
support with a stable `v1` key, ID, name, and fingerprint. Registering the
schema more than once must not be required.
- Add table-driven tests for context bounds, ordering, invalid references,
coalescing, every unsafe proposal category, non-overlapping safe groups,
deterministic keys, defensive copies, and non-mutation.
### Acceptance Criteria
- The package has no dependency on `dnd.NPC`, `dnd.Location`, either
normalizer, or a concrete LLM client.
- Proposal values can identify duplicate candidates even when display names
are equal.
- The shared response contract cannot directly supply replacement records or
evidence.
- Existing NPC packages still compile before their migration.
- `go test ./internal/modules/dnd/shared/...` passes offline.
### Prompt Size
Medium, but coherent and suitable for one implementation prompt. Do not combine
it with the NPC migration.
## Stage 4: Migrate NPC Normalization To The Shared Helper
### Goal
Make the existing NPC normalizer the first production consumer of the shared
entity-reconciliation contract while retaining its durable behavior.
### Work
- Refactor `internal/modules/dnd/normalize/npcs` to use opaque candidate keys,
shared context construction, shared proposal assessment, the shared response
schema, and the shared generic reconciliation instruction asset.
- Retain NPC-owned responsibilities:
- comparison-name preparation and deterministic duplicate handling;
- the NPC-specific task and canonical display-name rules;
- LLM invocation, bounded retry, fallback, warnings, and diagnostics;
- application of safe groups, evidence union, NPC ID derivation, and output
ordering.
- Remove superseded NPC-private context/proposal code and private schema assets
once no longer referenced.
- Update NPC prompt metadata and checkpoint fingerprints for the intentional
prompt/private-schema contract change.
- Preserve public module keys, durable NPC schema, identity policy, validator
chains, warning bounds, and fallback semantics.
- Test equal display names as distinct keyed candidates, alias consolidation,
rejected unsafe groups, retry exhaustion, private input ownership, redacted
errors, deterministic fallback, and non-mutation.
### Acceptance Criteria
- No durable NPC artifact field or module key changes.
- NPC normalization cannot confuse two candidates merely because their display
names match.
- Unsafe proposals leave a valid deterministic result and follow existing
retry/fallback policy.
- Obsolete NPC-only reconciliation helpers and schema are removed.
- `go test ./internal/modules/dnd/normalize/npcs/... ./internal/modules/dnd/shared/...`
passes offline.
### Prompt Size
Medium-to-large but bounded to one existing module. Suitable for one
implementation prompt; do not add location normalization in this stage.
## Stage 5: Implement Location Extraction
### Goal
Add the LLM-backed extractor that produces evidence-grounded location
candidates.
### Work
- Add `internal/modules/dnd/extract/locations` following current D&D extractor
conventions: strict empty options, typed builder and registration function,
`llm_backed` execution metadata, immutable inputs, redacted errors, prompt
and response-schema fingerprints, and bounded diagnostics.
- Add a private response schema containing only `name` and source ranges; the
model must not produce durable IDs or prose.
- Compose the prompt from existing shared D&D system, identity, campaign
reference, transcript, and evidence assets plus module-owned task and
instructions. Preserve the documented extraction-message ordering and cache
controls.
- Define physical-place inclusion and conservative omission exactly as in
`location.md`, including generic labels, aliases, and nested places.
- Map source ranges to the current source ID, canonicalize exact duplicate
ranges, derive candidate location IDs in code, preserve semantically invalid
candidates for validators where safe, and return deterministic ordering.
- Add prompt-asset tests that verify shared asset reuse and rendered inputs by
behavior, without asserting exact shared-prefix length or prompt wording.
- Add extractor tests for empty output, mapping, evidence ownership, generic
same-name locations with different anchors, invalid candidate preservation,
client failures, registration metadata, fingerprints, and non-mutation.
### Acceptance Criteria
- The extractor cannot manufacture source identities or accept campaign
references as evidence.
- The private model response does not contain a durable ID.
- Same-name candidates with different evidence survive extraction as distinct
candidates.
- The package is testable through its local registration but is not yet added
to the production D&D registrar.
- `go test ./internal/modules/dnd/extract/locations/...` passes offline.
### Prompt Size
Medium and suitable for one implementation prompt.
## Stage 6: Implement Semantic Location Normalization
### Goal
Add conservative alias and repeated-place reconciliation without collapsing
same-named or nested locations by default.
### Work
- Add `internal/modules/dnd/normalize/locations` as an `llm_backed` normalizer
using the shared entity-reconciliation package and private response schema.
- Deterministically clone and prepare the merged candidates first:
- normalize display whitespace;
- canonicalize and deduplicate source references;
- remove only exact duplicates with the same comparison name and exact
canonical evidence;
- assign opaque reconciliation keys; and
- retain same-name records with different evidence.
- Use bounded transcript windows and a module-owned location task that permits
grouping only when evidence clearly identifies one physical place. Explicitly
prohibit grouping solely by equal names, proximity, nesting, or generic
labels.
- Validate proposals through the shared package. Apply only safe groups in
deterministic code, choose the canonical name from the selected existing
candidate, union evidence, and derive the final evidence-anchored ID.
- Retain the deterministic candidate set on unusable proposals and follow the
existing NPC retry/fallback and bounded-warning conventions.
- Publish prompt, response-schema, identity-policy, normalization-policy, and
semantic-context fingerprints.
- Test aliases, repeated appearances, same-name distinct places, parent/child
locations, invalid and overlapping proposals, proposal retries, fallback,
ordering, ID recomputation, warning bounds, idempotent deterministic
application, and non-mutation.
### Acceptance Criteria
- The model proposes groups but cannot directly replace durable locations.
- A failed or ambiguous proposal cannot lose a valid candidate.
- Same-name locations remain distinct unless an approved evidence-backed group
joins them.
- Final IDs are derived only after group evidence is unioned.
- `go test ./internal/modules/dnd/normalize/locations/...` passes offline.
### Prompt Size
Medium-to-large but scoped to one normalizer and suitable for one implementation
prompt.
## Stage 7: Add The Immutable Location Registry
### Goal
Provide safe generated-reference resolution and an unambiguous prompt
projection for downstream occurrence extraction.
### Work
- Add `internal/modules/dnd/locations/registry`, modeled on the immutable NPC
registry and its operation-time resolver.
- Define `ReferenceSlot = "locations"`, a 1,048,576-byte limit, and exactly one
accepted `application/json` location-list item when bound.
- Validate durable decoding and location identity before constructing a
registry.
- Store canonical durable bytes and semantic digests without retaining mutable
caller-owned content. Return defensive copies from all accessors.
- Produce a compact, source-free prompt projection containing ordered
`{id, name}` pairs. Do not include source references or generated-reference
provenance.
- Support exact lookup by ID and verify the matching canonical name; do not
provide an ambiguous name-only lookup as the occurrence linkage mechanism.
- Preserve the established seeded/operation resolver behavior and concurrency-
safe semantic caching.
- Test absent, empty, malformed, oversized, wrong-media-type, invalid-identity,
and valid registries; projections; ID lookup; defensive copies; raw and
semantic cache reuse; and concurrent resolution.
### Acceptance Criteria
- Distinct same-name records are both representable and addressable by ID.
- Registry evidence cannot appear in the prompt projection.
- Malformed static references fail during construction and malformed generated
references fail at operation resolution through existing boundaries.
- `go test ./internal/modules/dnd/locations/registry/...` passes offline.
### Prompt Size
Medium and suitable for one implementation prompt.
## Stage 8: Implement Location-Occurrence Extraction
### Goal
Add the dependent LLM-backed lane that classifies source-grounded location
occurrences.
### Work
- Add `internal/modules/dnd/extract/locationoccurrences` with module key
`dnd/location-occurrences`, `llm_backed` execution metadata, strict empty
options, typed construction, and a required `locations` reference slot.
- Resolve the immutable registry at construction and for each operation using
the established generated-reference pattern.
- Add a private response schema requiring `location_id`, `name`, `kind`, and
source ranges. Restrict kinds to `visited`, `planned`, `recalled`, and
`mentioned`.
- Reuse the shared D&D extraction prompt assets and ordering. Place the compact
location registry after the shared transcript/evidence material and before
module task/instructions, consistent with current generated grounding.
- Encode the exact classification rules, precedence, multi-fact behavior, and
conservative omission policy from `location.md`.
- Map evidence only to the current source. Copy candidate IDs and names without
silently repairing unknown or mismatched values so deterministic validators
retain ownership of those diagnostics.
- Canonically order output and exact duplicates without dropping distinct
kinds or independent evidence.
- Test every kind, precedence, multiple supported facts, no-location and no-
occurrence outputs, required registry failures, same-name ID selection,
source-free prompt projection, current-transcript evidence, prompt/profile
metadata, client failures, non-mutation, and local registration.
### Acceptance Criteria
- Construction and operation specs declare `locations` as required and accept
only `dnd/location-list` JSON.
- The model sees IDs and names but no registry evidence.
- Registry context never becomes occurrence evidence.
- The extractor remains locally testable but is not production-selectable yet.
- `go test ./internal/modules/dnd/extract/locationoccurrences/...` passes
offline.
### Prompt Size
Medium and suitable for one implementation prompt.
## Stage 9: Implement Deterministic Occurrence Normalization
### Goal
Canonicalize occurrence records against the exact location registry without a
second LLM call.
### Work
- Add `internal/modules/dnd/normalize/locationoccurrences` as a deterministic
normalizer with the same required `locations` slot.
- Clone all inputs. Normalize source ranges, names, ordering, and exact
duplicates.
- For a known `location_id`, replace display-name variation with the registry's
exact canonical name. Do not perform a name-only guess.
- Preserve an unknown ID or otherwise invalid record for validator diagnostics
and emit bounded warnings where current D&D normalizer conventions require
them.
- Sort using the complete order defined in `location.md`.
- Publish normalization and registry-projection fingerprints consistent with
the other registry-backed normalizers.
- Test all kind values, canonical name replacement, same-name distinct IDs,
exact-duplicate removal, distinct evidence retention, stable ordering,
unknown IDs, malformed registry resolution, warnings, idempotence, and
non-mutation.
### Acceptance Criteria
- No LLM client or prompt assets are required.
- Canonicalization is exclusively ID-based.
- Invalid records are not silently redirected to a different location.
- `go test ./internal/modules/dnd/normalize/locationoccurrences/...` passes
offline.
### Prompt Size
Small enough for one implementation prompt.
## Stage 10: Add Location Registry Validators
### Goal
Give `dnd/location-list` the complete validator ownership expected of a
production D&D artifact.
### Work
- Add location validator packages under
`internal/modules/dnd/validate/locations` for:
- extraction shape;
- source-reference bounds/current-chunk ownership;
- advisory source relatedness; and
- normalized identity derivation and ID uniqueness.
- Use shared D&D citation, unit-reference, diagnostic, and matching helpers
where their contracts apply.
- The identity validator must allow repeated comparison names and validate the
evidence-anchored derivation policy instead of importing NPC uniqueness
assumptions.
- Keep diagnostics indexed, aggregated, bounded, stable, and free of raw prompt
or reference content.
- Add tests for accepted values, every owned failure, same-name distinct
locations, malformed/unreadable citations, advisory relatedness, bounds,
registration metadata, fingerprints, nil safety where applicable, and
non-mutation.
### Acceptance Criteria
- Validator responsibilities do not overlap merely to increase test coverage.
- Relatedness remains advisory and uses only cited current-transcript text.
- Validators do not repair or mutate artifacts.
- `go test ./internal/modules/dnd/validate/locations/...` passes offline.
### Prompt Size
Medium and suitable for one implementation prompt.
## Stage 11: Add Location-Occurrence Validators
### Goal
Give `dnd/location-occurrence-list` complete structural, registry, ordering,
evidence, and advisory validation.
### Work
- Add validator packages under
`internal/modules/dnd/validate/locationoccurrences` for:
- extraction shape and supported kinds;
- required registry membership and exact `location_id`/`name` pairing;
- normalized ordering and exact-duplicate invariants;
- source-reference bounds/current-chunk ownership; and
- advisory source relatedness.
- Reuse the immutable location resolver rather than decoding caller-owned
references independently in each validator.
- Ensure same-name registry records remain distinguishable by ID.
- Keep registry context out of evidence checks.
- Add focused tests for each kind, unknown IDs, mismatched names, same-name
locations, ordering, duplicates, malformed required references, invalid
evidence, advisory diagnostics, registration metadata, fingerprints,
diagnostic bounds, and non-mutation.
### Acceptance Criteria
- An ID/name mismatch is rejected even when another registry record has the
supplied name.
- Missing or malformed required registry references fail at the established
boundary.
- Validators remain deterministic and do not alter occurrence records or the
registry.
- `go test ./internal/modules/dnd/validate/locationoccurrences/...` passes
offline.
### Prompt Size
Medium and suitable for one implementation prompt.
## Stage 12: Compose The Production D&D Family
### Goal
Make both lanes selectable as one coherent production addition after all
component contracts are present.
### Work
- Extend `internal/modules/dnd/register` to register, in dependency-safe order:
- both codecs;
- both extractors;
- typed append-order mergers;
- the LLM-backed location normalizer;
- the deterministic occurrence normalizer;
- all validators;
- shared reconciliation schema assets and both new prompt manifests;
- evidence projectors; and
- extract and normalize default validator chains.
- Ensure both LLM-backed modules select the maintained `dnd-extraction`
fallback profile and inherit the existing profile policy.
- Define chain order consistently with existing D&D artifacts: generic JSON,
shape, registry/identity or normalized invariants at the appropriate stage,
source references, durable JSON Schema, then advisory relatedness.
- Update registrar tests for artifact kinds, keys, execution classes, reference
slot requirements, builder construction, assets, profile use, evidence
projection, chain contents/order, duplicate registration, and failure
propagation.
- Update any integration-level artifact-kind allowlists or typed registries
required by the framework; do not add module-specific orchestration logic.
### Acceptance Criteria
- One D&D registration call exposes both complete lanes and no partial
registration succeeds silently.
- Catalog inspection reports correct artifact kinds, stages, execution classes,
reference slots, profiles, and fingerprints.
- Both artifact kinds support evidence projection without registry evidence
leakage.
- `go test ./internal/modules/dnd/register/... ./internal/modules/dnd/...` passes
offline.
### Prompt Size
Medium-to-large but limited to composition and suitable for one implementation
prompt.
## Stage 13: Add Maintained Pipeline And Handoff Coverage
### Goal
Exercise the feature through real configuration, ordered generated references,
chunk operations, acceptance gates, and durable output.
### Work
- Update `examples/dnd-complete.config.yml`:
- add `locations` to the first descriptive step with extract, append-order
merge, and LLM-backed normalize bindings;
- add a generated `locations` reference to the next step;
- add `location-occurrences` to that step with extract, append-order merge,
and deterministic normalize bindings; and
- add both lanes to evidence-context output where appropriate.
- Keep the minimal example minimal unless its stated purpose requires a
location lane; do not turn it into a second complete example.
- Extend maintained example-loading/config-validation tests.
- Add integration tests that prove:
- the normalized accepted registry is handed off in memory;
- the occurrence lane cannot run before its producer;
- missing, cyclic, wrong-kind, wrong-media-type, rejected, or unaccepted
producers are rejected at the existing boundaries;
- same-name locations remain distinguishable by ID through the handoff;
- registry evidence never becomes occurrence evidence;
- retries and checkpoints honor prompt, schema, identity, and generated-
reference fingerprints; and
- output contains both durable artifact envelopes and evidence context.
- Use recording/fake structured clients only; no network-dependent tests.
### Acceptance Criteria
- The complete example loads through the real config path and exercises all
registered D&D lanes.
- Ordered handoff failure semantics match the framework's existing fail-whole-
run policy.
- Integration tests cover behavior rather than duplicating package internals.
- `go test ./internal/modules/integration/... ./internal/config/...` and any
example-specific test targets pass offline.
### Prompt Size
Medium-to-large but coherent as one end-to-end integration prompt.
## Stage 14: Publish Current Documentation And Perform Final Verification
### Goal
Make the implemented feature discoverable and retire fulfilled future-work
language without leaving development-history documentation behind.
### Work
- Create canonical integration contracts:
- `docs/integrations/dnd-location-artifacts.md`;
- `docs/integrations/dnd-location-occurrence-artifacts.md`.
- Update `docs/config.md` with both selectable keys, the `locations` reference
slot and compatibility, execution classes, validators, default chains, and
the complete-example link.
- Update `docs/internal/dnd.md` with nine-lane composition, shared entity
reconciliation, evidence-anchored identity, occurrence grounding, prompt
asset reuse, and intentional lane differences. Link to integration contracts
instead of duplicating their JSON shapes.
- Update `docs/integrations/json-output.md`, `README.md`, and other current
canonical inventories only where repository inspection shows that the new
artifact kinds or maintained example must be listed.
- Remove the fulfilled `Location Extraction` section from
`docs/roadmap/future.md`. Keep the generic LLM-assisted deduplication item and
clarify only if needed that the new D&D helper does not fulfill that broader
feature.
- Verify all relative links and search current documentation for stale
seven-lane counts, missing keys, obsolete location-planning claims, and
accidental claims that references are evidence.
- Run formatting, focused tests, the full Go test suite, static analysis, and
repository-provided config/example checks. Inspect `git diff --check` and
confirm no unrelated files changed.
- After implementation and verification are complete, leave `location.md` and
this plan in place for the user's separate roadmap-retirement step; do not
delete them unless explicitly asked.
### Acceptance Criteria
- Current documentation describes the implemented contracts and only
implemented behavior outside `docs/roadmap/`.
- The roadmap no longer presents completed location tracking as future work.
- Links, examples, module inventories, and default-chain tables agree with
production registration.
- `go test ./internal/modules/dnd/...` passes.
- `go test ./internal/modules/integration/...` passes.
- `go test ./...` passes.
- `go vet ./...` passes.
- Repository-provided configuration/example validation passes.
- `git diff --check` reports no errors.
### Prompt Size
Medium and suitable for one implementation prompt.
## Open Questions
None. The feature roadmap fixes the artifact shapes, identity scope and
derivation, occurrence categories, classification precedence, reference
dependency, reconciliation safety boundary, pipeline placement, and non-goals
needed to implement every stage without an additional product decision.

View File

@@ -1,337 +0,0 @@
# D&D Location Tracking
## Purpose
Add evidence-grounded D&D location tracking without turning a single extractor
into both an entity registry and an event classifier. The target design follows
the established NPC pattern: one lane identifies canonical location records and
a later lane records how the party related to those locations in the transcript.
This roadmap defines the desired end state and policy choices. The ordered work
needed to reach that state is in [the implementation plan](implementation.md).
## User Intent
- Record locations the party visits or that the session otherwise discusses.
- Distinguish current physical presence from plans, recollections, and ordinary
mentions.
- Preserve transcript evidence for every durable record.
- Reconcile aliases and repeated appearances conservatively.
- Keep distinct places separate when they happen to share a generic name.
- Keep the schemas minimal. Location description, hierarchy, participants, and
narrative analysis belong in other artifacts or deterministic joins.
## Target Capability
The D&D module family will have two new lanes:
1. `dnd/locations` produces a session-scoped registry of physical places.
2. `dnd/location-occurrences` consumes the normalized location registry and
produces an ordered list of source-grounded relationships between the party
and those places.
The normalized location artifact is handed to the occurrence lane through a
required generated reference named `locations`. The occurrence lane must use
that registry for identity grounding, but the current transcript remains its
only evidence source.
## Durable Artifact Contracts
Both contracts remain at `v1`; Notarius is pre-release and does not need a
compatibility layer for these new artifacts.
### Location registry
The location lane uses:
- artifact kind: `dnd/location-list`
- module key: `dnd/locations`
- schema ID: `notarius.dnd.locations`
- schema name: `notarius_dnd_locations_v1`
- media type: `application/json`
- root member: `locations`
Each location contains exactly:
| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string | Deterministic, session-scoped canonical location identity. |
| `name` | string | Evidence-grounded display name or transcript-established label. |
| `source_refs` | non-empty source-reference array | Current-transcript evidence that identifies the place. |
Locations are physical or spatial places: planes, regions, settlements,
districts, buildings, rooms, landmarks, routes, and geographic features. A
generic label such as `the tavern` is permitted only when the transcript uses
it for a specific place. The extractor must not invent a qualifier merely to
distinguish that place from another place with the same label.
The registry does not contain type, parent, description, summary, coordinates,
participants, visit status, or occurrence data. Parent and child places are
separate identities when the transcript identifies both; nesting alone is not
a reason to merge them.
### Location occurrences
The occurrence lane uses:
- artifact kind: `dnd/location-occurrence-list`
- module key: `dnd/location-occurrences`
- schema ID: `notarius.dnd.location_occurrences`
- schema name: `notarius_dnd_location_occurrences_v1`
- media type: `application/json`
- root member: `occurrences`
Each occurrence contains exactly:
| Field | Type | Meaning |
| --- | --- | --- |
| `location_id` | string | An exact ID from the consumed normalized location registry. |
| `name` | string | The canonical display name associated with `location_id`. |
| `kind` | enum | `visited`, `planned`, `recalled`, or `mentioned`. |
| `source_refs` | non-empty source-reference array | Current-transcript evidence for both the place and the classified occurrence. |
`location_id` is required even though existing NPC interactions currently use
name-only grounding. Locations can legitimately share the same display name,
so a name alone cannot provide an unambiguous cross-artifact link. The name is
retained for readable standalone output and must exactly match the registry
record selected by the ID after normalization.
## Identity Policy
Location identity is conservative and scoped to one source document. It is not
a campaign-wide or cross-session world identity.
Display normalization trims surrounding whitespace and collapses internal
Unicode whitespace. Comparison normalization uses the existing D&D entity
rules: Unicode NFKC normalization, normalized apostrophes, collapsed
whitespace, and Unicode case folding.
The canonical ID is:
~~~text
location:sha256:<lowercase SHA-256 hex digest>
~~~
The digest input is the UTF-8 encoding of compact JSON for this five-element
array:
~~~text
["dnd.locations.identity.v1", comparison_name, source_id, start_unit_id, end_unit_id]
~~~
The source values come from the earliest reference after canonical reference
sorting and exact deduplication. Compact JSON array encoding is part of the
identity contract: it avoids delimiter ambiguity and must not be replaced
without changing the policy version. A blank comparison name or missing valid
source reference produces no manufactured ID and remains a validation error.
Including the evidence anchor prevents two unrelated places called `the
tavern` from receiving the same ID. When semantic normalization safely groups
aliases or repeated appearances, it first chooses an existing canonical display
name and unions the evidence; it then derives the final ID from that name and
the earliest unioned reference.
The normalizer may merge records only when transcript evidence clearly shows
that they denote the same physical place. It must not merge records solely
because:
- their comparison names are equal;
- they are near one another in the transcript;
- one is spatially nested inside the other; or
- their labels are both generic.
Distinct normalized records may therefore have the same comparison name, but
their IDs must be unique and correctly derived. Exact duplicates with the same
comparison name and canonical evidence may be collapsed deterministically.
## Occurrence Semantics
Each occurrence has one kind:
- `visited`: current-session gameplay establishes that one or more party
members are physically present at the location, including an arrival,
continuing presence, or departure.
- `planned`: the party explicitly proposes, intends, or agrees to future travel
to the location. Mere hypotheticals or speculation are not plans.
- `recalled`: the transcript explicitly recounts or recaps the party being at
the location before the current session's live events.
- `mentioned`: the location is explicitly referenced but the occurrence does
not meet a stronger definition. This includes lore, directions, third-party
activity, non-actionable speculation, and out-of-character discussion.
An inferred but unstated place produces no location or occurrence. Uncertainty
is handled by conservative omission rather than an `uncertain` enum value.
For one occurrence supported by overlapping evidence, classification precedence
is `visited`, then `planned`, then `recalled`, then `mentioned`; `mentioned` is
the fallback. A passage may produce multiple records when it independently
supports separate facts, such as recalling an earlier visit while planning a
return. Exact duplicates with the same ID, kind, and canonical evidence are
collapsed. Different kinds or independently supported evidence remain.
Output is ordered by earliest evidence in source-document order, then by
`location_id`, `name`, kind order (`visited`, `planned`, `recalled`,
`mentioned`), and the remaining canonical reference sequence.
## Extraction, Normalization, And Evidence
### Location registry lane
The extractor is LLM-backed and follows the shared D&D extraction prompt and
input conventions. It emits names and source ranges through a private response
schema; deterministic mapping supplies the current source ID and derives
candidate IDs. Campaign references may disambiguate terminology but never
become durable evidence.
The merger uses the typed append-order convention. The normalizer is LLM-backed:
it deterministically prepares names and evidence, then asks the model only for
duplicate groups. The model may identify groups and choose a canonical member,
but it may not create, delete, rewrite, or directly replace durable records.
Code validates the proposal, applies non-overlapping safe groups, unions
evidence, derives final IDs, orders output, and emits bounded warnings.
Malformed, unknown, overlapping, or ambiguous proposal groups are rejected.
The normalizer uses the existing bounded retry behavior and falls back to the
safe deterministic candidate set if no usable proposal is obtained.
### Location occurrence lane
The extractor is LLM-backed and requires exactly one validated `locations`
reference. The prompt projection contains only ordered `{id, name}` pairs; it
omits registry evidence and reference provenance. The model must copy both
values from one projected record and cite current-transcript source ranges for
the occurrence.
The occurrence normalizer is deterministic. It canonicalizes names by exact
registry ID, normalizes evidence and ordering, and removes exact duplicates.
Unknown IDs and mismatched ID/name pairs remain inspectable validation failures
rather than being guessed or silently reassigned.
The occurrence lane cannot add a missing location to the registry. If the
location extractor omitted a place, the correct behavior is to omit its
occurrence and improve the upstream extraction later.
## Shared Entity Reconciliation
Adding a second LLM-assisted entity registry demonstrates a concrete shared
need in the D&D domain. The existing NPC normalization context-window and
proposal-safety logic will move to
`internal/modules/dnd/shared/entityreconcile` and serve both NPC and location
normalizers.
The shared package owns:
- deterministic opaque candidate keys;
- source-window construction and canonical prompt materials;
- a common private duplicate-group response contract;
- validation of unknown, repeated, overlapping, malformed, or ineligible
candidate keys; and
- immutable assessment results identifying safe groups.
It does not call the LLM, choose domain-specific canonical names, derive
durable IDs, mutate domain artifacts, or format domain warnings. Those
responsibilities remain in each normalizer.
NPC normalization will migrate to the shared key-based proposal contract
without changing its durable NPC behavior. Its prompt and private response
schema fingerprints are expected to change, so stale NPC normalization
checkpoints will invalidate normally.
The two normalizers will reuse an exactly identical shared reconciliation
instruction asset and private response schema. Module-owned task text will
continue to define the different NPC and location identity rules. This keeps
shared prompt content identical without pretending the two domains have the
same semantic merge policy.
This helper is intentionally D&D-specific. It does not implement the broader
domain-neutral replacement-element normalizer still described in
[future work](future.md).
## Reference Contract And Pipeline Placement
The generated reference slot is named `locations` and accepts exactly one JSON
artifact of kind `dnd/location-list`, with the established 1 MiB limit. It is
required by both extraction and normalization for
`dnd/location-occurrences`. Static file bindings remain valid where the
framework permits them, but the maintained complete example uses a generated
same-run artifact.
The complete D&D pipeline places `locations` in the first descriptive step
alongside the independent NPC, item-event, and scene-description lanes. It
places `location-occurrences` in the next step and binds the accepted normalized
location artifact from the first step. The occurrence lane has no mandatory
NPC or scene-description dependency.
No current downstream lane is changed to consume location artifacts. Future
narrative reports or joins may use the canonical IDs after defining their own
contracts.
## Validation And Production Defaults
The location registry receives production validators for:
- required shape and supported ID syntax;
- current-document and current-chunk source ranges;
- normalized identity derivation and ID uniqueness; and
- advisory source relatedness.
The location occurrence artifact receives production validators for:
- required shape and the four supported kinds;
- registry membership and exact ID/name correspondence;
- normalized ordering and exact-duplicate invariants;
- current-document and current-chunk source ranges; and
- advisory source relatedness.
Validators remain immutable and diagnostic. Durable JSON Schema validation
stays in the production chains after semantic shape and source-reference
checks, consistent with the existing D&D lanes.
## Documentation End State
Implementation will add canonical integration documents for both durable
artifacts and update current-state documentation to cover:
- both module and artifact keys;
- the `locations` generated-reference slot;
- production validators and default chains;
- D&D family composition, reconciliation, identity, and grounding behavior;
- the complete maintained pipeline example; and
- JSON output and evidence-context support.
After the feature is implemented, the fulfilled Location Extraction section is
removed from `future.md`. Historical implementation narration remains in
version control rather than current documentation.
## Out Of Scope
- Campaign-wide or cross-session canonical location IDs.
- A location ontology, hierarchy, map, coordinates, or containment graph.
- Location descriptions, summaries, participants, ownership, or encounter
analysis.
- Inferring a location that the transcript does not identify.
- Automatically creating registry records from occurrence output.
- Changing NPC-interaction artifacts to use NPC IDs.
- Making other lanes consume location references.
- A generic domain-neutral LLM deduplication framework.
- Long-term artifact-version migration machinery.
## Acceptance Criteria
- Both durable contracts are minimal, strict, versioned, and registered.
- Location IDs are deterministic under the documented policy and do not force
same-named places to collapse.
- Alias and repeat reconciliation is proposal-only, conservatively validated,
and safe on retry exhaustion.
- Occurrences use one of the four defined kinds and carry an unambiguous
registry ID/name pair plus current-transcript evidence.
- Missing, malformed, oversized, or incompatible `locations` references fail
through the established configuration or operation boundaries.
- Both lanes have typed mergers, normalizers, evidence projectors, validators,
default chains, prompt/profile metadata, and registration coverage consistent
with the D&D family.
- NPC normalization retains its durable behavior after adopting the shared
reconciliation helper.
- The maintained complete example loads and exercises the generated handoff.
- Focused D&D and integration tests pass offline, and current documentation
describes only implemented behavior once the work is complete.

View File

@@ -35,29 +35,40 @@ pipelines:
enabled: true enabled: true
window_units: 3 window_units: 3
lanes: lanes:
- item-events - item-occurrences
- npcs - item-registry
- location-registry
- location-occurrences
- npc-registry
- spells - spells
- combat-turns - combat-turns
- npc-interactions - npc-occurrences
- enemy-events - enemy-events
steps: steps:
# Establish session-wide reference artifacts alongside independent item events. # Establish session-wide reference artifacts before their consumers.
- id: describe-session - id: describe-session
artifacts: artifacts:
item-events: item-registry:
extract: extract:
module: dnd/item-events module: dnd/item-registry
retries: 2 retries: 2
merge: appendorder merge: appendorder
normalize: dnd/item-events normalize: dnd/item-registry
npcs: npc-registry:
extract: extract:
module: dnd/npcs module: dnd/npc-registry
retries: 2 retries: 2
merge: appendorder merge: appendorder
normalize: normalize:
module: dnd/npcs module: dnd/npc-registry
retries: 2
location-registry:
extract:
module: dnd/location-registry
retries: 2
merge: appendorder
normalize:
module: dnd/location-registry
retries: 2 retries: 2
scene-descriptions: scene-descriptions:
extract: extract:
@@ -66,18 +77,32 @@ pipelines:
merge: appendorder merge: appendorder
normalize: dnd/scene-descriptions normalize: dnd/scene-descriptions
- id: extract-events - id: extract-events
# Accepted NPC grounding and scene-description eligibility artifacts are # Accepted registry artifacts and scene-description eligibility artifacts
# supplied in memory to their compatible consumers in this step. # are supplied in memory to their compatible consumers in this step.
references: references:
npcs: location_registry:
artifact: artifact:
step: describe-session step: describe-session
lane: npcs lane: location-registry
npc_registry:
artifact:
step: describe-session
lane: npc-registry
scene_descriptions: scene_descriptions:
artifact: artifact:
step: describe-session step: describe-session
lane: scene-descriptions lane: scene-descriptions
item_registry:
artifact:
step: describe-session
lane: item-registry
artifacts: artifacts:
item-occurrences:
extract:
module: dnd/item-occurrences
retries: 2
merge: appendorder
normalize: dnd/item-occurrences
spells: spells:
extract: extract:
module: dnd/spells module: dnd/spells
@@ -96,18 +121,24 @@ pipelines:
retries: 2 retries: 2
merge: appendorder merge: appendorder
normalize: dnd/combat-turns normalize: dnd/combat-turns
npc-interactions: npc-occurrences:
extract: extract:
module: dnd/npc-interactions module: dnd/npc-occurrences
retries: 2 retries: 2
merge: appendorder merge: appendorder
normalize: dnd/npc-interactions normalize: dnd/npc-occurrences
location-occurrences:
extract:
module: dnd/location-occurrences
retries: 2
merge: appendorder
normalize: dnd/location-occurrences
- id: track-enemies - id: track-enemies
references: references:
npcs: npc_registry:
artifact: artifact:
step: describe-session step: describe-session
lane: npcs lane: npc-registry
scene_descriptions: scene_descriptions:
artifact: artifact:
step: describe-session step: describe-session
@@ -116,10 +147,10 @@ pipelines:
artifact: artifact:
step: extract-events step: extract-events
lane: combat-turns lane: combat-turns
npc_interactions: npc_occurrences:
artifact: artifact:
step: extract-events step: extract-events
lane: npc-interactions lane: npc-occurrences
artifacts: artifacts:
enemy-events: enemy-events:
extract: extract:

View File

@@ -0,0 +1,81 @@
package cli
import (
"context"
"errors"
"strings"
"testing"
"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"
)
const invalidEnemyEventExtractorKey = "test/dnd/invalid-enemy-events"
func TestAssembledEnemyEventLaneRejectsInvalidFinalArtifactDespiteValidatorOverrides(t *testing.T) {
components := productionTestComponents(t)
if err := pipeline.RegisterExtractor[dnd.EnemyEventList](components.registries.Extractors, pipeline.ModuleSpec{
Key: invalidEnemyEventExtractorKey,
Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassDeterministic,
Requires: []string{"chunks", "source.transcript"},
Provides: []string{"dnd.enemy_events"},
ArtifactKind: dnd.EnemyEventListKind,
}, func() (contracts.Extractor[dnd.EnemyEventList], error) {
return invalidEnemyEventExtractor{}, nil
}); err != nil {
t.Fatalf("register extractor: %v", err)
}
accept := pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}}
resolved, err := pipeline.ResolvePipeline(pipeline.PipelineProfile{
ID: "assembled-invalid-enemy-events",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"enemy-events": {
Extract: pipeline.ModuleBinding{Module: invalidEnemyEventExtractorKey, Validators: accept},
Normalize: pipeline.ModuleBinding{Module: pipeline.DefaultNormalizeModule, Validators: accept},
},
},
Output: pipeline.Binding("json"),
}, pipeline.ResolveOptions{}, catalogFromRegistries(components.registries))
if err != nil {
t.Fatalf("ResolvePipeline() error = %v", err)
}
prepared, err := pipeline.Prepare(resolved, components.registries, pipeline.ModuleDependencies{})
if err != nil {
t.Fatalf("Prepare() error = %v", err)
}
_, err = pipeline.New().Run(context.Background(), pipeline.RunInput{
Prepared: prepared,
RawInput: readRepositoryFile(t, "examples", "seriatim-minimal-transcript.json"),
ChunkCacheMode: pipeline.ChunkCacheBypass,
})
if err == nil || !strings.Contains(err.Error(), "serialize accepted extract output") || !strings.Contains(err.Error(), "must not exceed") {
t.Fatalf("Run() error = %v, want final durable range rejection", err)
}
}
type invalidEnemyEventExtractor struct{}
func (invalidEnemyEventExtractor) Key() string { return invalidEnemyEventExtractorKey }
func (invalidEnemyEventExtractor) ReferenceSlots() []contracts.ReferenceSlot { return nil }
func (invalidEnemyEventExtractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.EnemyEventList], error) {
if err := ctx.Err(); err != nil {
return contracts.TypedExtractionResult[dnd.EnemyEventList]{}, err
}
if req.Source == nil {
return contracts.TypedExtractionResult[dnd.EnemyEventList]{}, errors.New("assembled extractor requires source")
}
return contracts.TypedExtractionResult[dnd.EnemyEventList]{Value: dnd.EnemyEventList{Events: []dnd.EnemyEvent{{
Name: "Ashfang",
Kind: dnd.EnemyEventKindEngaged,
SourceRefs: []source.SourceRef{{SourceID: req.Source.ID, StartUnitID: 2, EndUnitID: 1}},
}}}}, nil
}

View File

@@ -134,7 +134,7 @@ func TestAssembledSpellPipelineHonorsNormalizeValidatorOverride(t *testing.T) {
} }
} }
func TestAssembledSpellPipelineRejectsUnknownSpellWithoutPromotingAttemptWarning(t *testing.T) { func TestAssembledSpellPipelinePromotesTerminalUnknownSpellWarning(t *testing.T) {
registries, resolved, _ := assembledSpellPipeline(t, assembledSpellPipelineOptions{unknownSpell: true}) registries, resolved, _ := assembledSpellPipeline(t, assembledSpellPipelineOptions{unknownSpell: true})
prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{}) prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{})
if err != nil { if err != nil {
@@ -161,10 +161,8 @@ func TestAssembledSpellPipelineRejectsUnknownSpellWithoutPromotingAttemptWarning
if !reflect.DeepEqual(rejectedFile.Rejected, output.Rejected) { if !reflect.DeepEqual(rejectedFile.Rejected, output.Rejected) {
t.Fatalf("rejected file = %#v, run rejections = %#v, want durable rejection diagnostic", rejectedFile.Rejected, output.Rejected) t.Fatalf("rejected file = %#v, run rejections = %#v, want durable rejection diagnostic", rejectedFile.Rejected, output.Rejected)
} }
for _, warning := range output.Warnings { if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Warnings[0].Scope != "spell_casts[0]" {
if warning.ReasonCode == spellnormalize.ReasonCodeSpellNameUnresolved { t.Fatalf("warnings = %#v, want terminal normalize catalog warning", output.Warnings)
t.Fatalf("warnings = %#v, want rejected-attempt warning to remain non-durable", output.Warnings)
}
} }
} }

View File

@@ -48,7 +48,7 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" { if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" {
t.Fatalf("combat codec spec = %#v, want compatible durable schema", codecSpec) t.Fatalf("combat codec spec = %#v, want compatible durable schema", codecSpec)
} }
if !hasReferenceSlot(extractSpec.ReferenceSlots, "npcs") || !hasReferenceSlot(extractSpec.ReferenceSlots, "scene_descriptions") || !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npcs") { if !hasReferenceSlot(extractSpec.ReferenceSlots, "npc_registry") || !hasReferenceSlot(extractSpec.ReferenceSlots, "scene_descriptions") || !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npc_registry") {
t.Fatalf("combat reference slots = %#v / %#v, want extraction scene and NPC slots plus normalization NPC slot", extractSpec.ReferenceSlots, normalizeSpec.ReferenceSlots) t.Fatalf("combat reference slots = %#v / %#v, want extraction scene and NPC slots plus normalization NPC slot", extractSpec.ReferenceSlots, normalizeSpec.ReferenceSlots)
} }
sceneSlot := referenceSlot(extractSpec.ReferenceSlots, "scene_descriptions") sceneSlot := referenceSlot(extractSpec.ReferenceSlots, "scene_descriptions")
@@ -85,15 +85,15 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
PipelineID: "dnd-combat", PipelineID: "dnd-combat",
Catalog: catalog, Catalog: catalog,
ReferenceOverrides: []pipeline.ReferenceBinding{ ReferenceOverrides: []pipeline.ReferenceBinding{
{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI}, {Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
{Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI}, {Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
}, },
}) })
if err != nil { if err != nil {
t.Fatalf("Resolve(bound references) error = %v, want nil", err) t.Fatalf("Resolve(bound references) error = %v, want nil", err)
} }
boundLane := bound.ResolvedPipeline.Steps[0].ArtifactLanes[0] boundLane := bound.ResolvedPipeline.Steps[0].ArtifactLanes[0]
if len(boundLane.ExtractReferences.Bindings) != 2 || len(boundLane.NormalizeReferences.Bindings) != 1 || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "npcs") || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "scene_descriptions") || !hasReferenceBinding(boundLane.NormalizeReferences.Bindings, "npcs") { if len(boundLane.ExtractReferences.Bindings) != 2 || len(boundLane.NormalizeReferences.Bindings) != 1 || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "npc_registry") || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "scene_descriptions") || !hasReferenceBinding(boundLane.NormalizeReferences.Bindings, "npc_registry") {
t.Fatalf("bound combat references = %#v / %#v, want extraction scene and NPC bindings plus normalization NPC binding", boundLane.ExtractReferences, boundLane.NormalizeReferences) t.Fatalf("bound combat references = %#v / %#v, want extraction scene and NPC bindings plus normalization NPC binding", boundLane.ExtractReferences, boundLane.NormalizeReferences)
} }
} }

View File

@@ -20,15 +20,26 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences"
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
npcoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
combat "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" combat "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
enemyevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" enemyevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
itemevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" itemoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
npcinteractions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
npcs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs" locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
npcoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
scenedescriptions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" scenedescriptions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs" itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
) )
func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T) { func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T) {
@@ -50,17 +61,17 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
t.Fatalf("enemy event lane = %#v, want typed production composition", lane) t.Fatalf("enemy event lane = %#v, want typed production composition", lane)
} }
for slot, want := range map[string]struct{ step, lane string }{ for slot, want := range map[string]struct{ step, lane string }{
"npcs": {step: "describe-session", lane: "npcs"}, "npc_registry": {step: "describe-session", lane: "npc-registry"},
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"}, "scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
"combat_turns": {step: "extract-events", lane: "combat-turns"}, "combat_turns": {step: "extract-events", lane: "combat-turns"},
"npc_interactions": {step: "extract-events", lane: "npc-interactions"}, "npc_occurrences": {step: "extract-events", lane: "npc-occurrences"},
} { } {
binding, found := generatedReferenceBinding(lane.ExtractReferences.Bindings, slot) binding, found := generatedReferenceBinding(lane.ExtractReferences.Bindings, slot)
if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane { if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane {
t.Fatalf("enemy event %s reference = %#v, want generated %s/%s artifact", slot, binding, want.step, want.lane) t.Fatalf("enemy event %s reference = %#v, want generated %s/%s artifact", slot, binding, want.step, want.lane)
} }
} }
if binding, found := generatedReferenceBinding(lane.NormalizeReferences.Bindings, "npcs"); !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "npcs" { if binding, found := generatedReferenceBinding(lane.NormalizeReferences.Bindings, "npc_registry"); !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "npc-registry" {
t.Fatalf("enemy event normalizer NPC reference = %#v, want generated NPC artifact", binding) t.Fatalf("enemy event normalizer NPC reference = %#v, want generated NPC artifact", binding)
} }
@@ -73,24 +84,24 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) { if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
t.Fatalf("enemy event normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec) t.Fatalf("enemy event normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
} }
for _, slot := range []string{"npcs", "scene_descriptions", "combat_turns", "npc_interactions"} { for _, slot := range []string{"npc_registry", "scene_descriptions", "combat_turns", "npc_occurrences"} {
if !hasReferenceSlot(extractSpec.ReferenceSlots, slot) { if !hasReferenceSlot(extractSpec.ReferenceSlots, slot) {
t.Fatalf("enemy event extractor slots = %#v, want %q", extractSpec.ReferenceSlots, slot) t.Fatalf("enemy event extractor slots = %#v, want %q", extractSpec.ReferenceSlots, slot)
} }
} }
if !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npcs") { if !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npc_registry") {
t.Fatalf("enemy event normalizer slots = %#v, want NPC registry", normalizeSpec.ReferenceSlots) t.Fatalf("enemy event normalizer slots = %#v, want NPC registry", normalizeSpec.ReferenceSlots)
} }
profile := cfg.Pipelines["dnd-session"] profile := cfg.Pipelines["dnd-session"]
profile.Steps[2].References["npcs"] = pipeline.GeneratedReference("track-enemies", "enemy-events") profile.Steps[2].References["npc_registry"] = pipeline.GeneratedReference("track-enemies", "enemy-events")
cfg.Pipelines["dnd-session"] = profile cfg.Pipelines["dnd-session"] = profile
if _, err := cfg.Resolve(resolveInputForMaintainedExample(components, "dnd-session")); err == nil || !strings.Contains(err.Error(), "earlier step") { if _, err := cfg.Resolve(resolveInputForMaintainedExample(components, "dnd-session")); err == nil || !strings.Contains(err.Error(), "earlier step") {
t.Fatalf("Resolve() error = %v, want future generated-reference rejection", err) t.Fatalf("Resolve() error = %v, want future generated-reference rejection", err)
} }
} }
func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t *testing.T) { func TestMaintainedCompleteExamplePublishesRegistryBackedEntityOccurrences(t *testing.T) {
t.Chdir(repositoryPath()) t.Chdir(repositoryPath())
outputRoot := filepath.Join(t.TempDir(), "output") outputRoot := filepath.Join(t.TempDir(), "output")
configPath := completeExampleConfigWithTemporaryCache(t) configPath := completeExampleConfigWithTemporaryCache(t)
@@ -116,13 +127,17 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
runRoot := filepath.Join(outputRoot, productionRunID) runRoot := filepath.Join(outputRoot, productionRunID)
index := readProductionJSON[exampleOutputIndex](t, filepath.Join(runRoot, "index.json")) index := readProductionJSON[exampleOutputIndex](t, filepath.Join(runRoot, "index.json"))
var enemyOutput exampleOutputIndexEntry outputs := make(map[string]exampleOutputIndexEntry)
for _, entry := range index.OutputFiles { for _, entry := range index.OutputFiles {
if entry.LaneID == "enemy-events" { outputs[entry.LaneID] = entry
enemyOutput = entry
break
}
} }
enemyOutput := outputs["enemy-events"]
locationOutput := outputs["location-registry"]
locationOccurrenceOutput := outputs["location-occurrences"]
npcRegistryOutput := outputs["npc-registry"]
npcOccurrenceOutput := outputs["npc-occurrences"]
itemRegistryOutput := outputs["item-registry"]
itemOccurrenceOutput := outputs["item-occurrences"]
if enemyOutput.File != "lanes/enemy-events.json" || enemyOutput.SchemaID != "notarius.dnd.enemy_events" || enemyOutput.SchemaVersion != "v1" { if enemyOutput.File != "lanes/enemy-events.json" || enemyOutput.SchemaID != "notarius.dnd.enemy_events" || enemyOutput.SchemaVersion != "v1" {
t.Fatalf("enemy event output = %#v, want typed enemy-event JSON", enemyOutput) t.Fatalf("enemy event output = %#v, want typed enemy-event JSON", enemyOutput)
} }
@@ -130,10 +145,62 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
if len(value.Events) != 1 || value.Events[0].Name != "Kesh" || value.Events[0].Kind != dnd.EnemyEventKindFled || len(value.Events[0].SourceRefs) != 1 || value.Events[0].SourceRefs[0].SourceID != "session-ravenfall" || value.Events[0].SourceRefs[0].StartUnitID != 10 { if len(value.Events) != 1 || value.Events[0].Name != "Kesh" || value.Events[0].Kind != dnd.EnemyEventKindFled || len(value.Events[0].SourceRefs) != 1 || value.Events[0].SourceRefs[0].SourceID != "session-ravenfall" || value.Events[0].SourceRefs[0].StartUnitID != 10 {
t.Fatalf("enemy event artifact = %#v, want source-linked Kesh fleeing event", value) t.Fatalf("enemy event artifact = %#v, want source-linked Kesh fleeing event", value)
} }
if locationOutput.File != "lanes/location-registry.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion {
t.Fatalf("location output = %#v, want typed location registry JSON", locationOutput)
}
locationsValue := readProductionJSON[dnd.LocationRegistry](t, filepath.Join(runRoot, locationOutput.File))
if len(locationsValue.Locations) != 2 || locationsValue.Locations[0].Name != "Moon Gate" || locationsValue.Locations[1].Name != "Moon Gate" || locationsValue.Locations[0].ID == locationsValue.Locations[1].ID {
t.Fatalf("location registry = %#v, want distinct source-grounded identities for same-name locations", locationsValue)
}
if locationOccurrenceOutput.File != "lanes/location-occurrences.json" || locationOccurrenceOutput.SchemaID != locationoccurrencecodec.SchemaID || locationOccurrenceOutput.SchemaVersion != locationoccurrencecodec.SchemaVersion {
t.Fatalf("location occurrence output = %#v, want typed occurrence JSON", locationOccurrenceOutput)
}
occurrencesValue := readProductionJSON[dnd.LocationOccurrenceList](t, filepath.Join(runRoot, locationOccurrenceOutput.File))
if len(occurrencesValue.Occurrences) != 2 || occurrencesValue.Occurrences[0].LocationID == occurrencesValue.Occurrences[1].LocationID || occurrencesValue.Occurrences[0].Name != "Moon Gate" || occurrencesValue.Occurrences[1].Name != "Moon Gate" {
t.Fatalf("location occurrences = %#v, want source-grounded references to distinct registry identities", occurrencesValue)
}
if npcRegistryOutput.File != "lanes/npc-registry.json" || npcRegistryOutput.SchemaID != npccodec.SchemaID || npcRegistryOutput.SchemaVersion != npccodec.SchemaVersion {
t.Fatalf("NPC registry output = %#v, want typed registry JSON", npcRegistryOutput)
}
npcRegistry := readProductionJSON[dnd.NPCRegistry](t, filepath.Join(runRoot, npcRegistryOutput.File))
if len(npcRegistry.NPCs) != 1 || npcRegistry.NPCs[0].Name != "Kesh" {
t.Fatalf("NPC registry = %#v, want Kesh identity", npcRegistry)
}
if npcOccurrenceOutput.File != "lanes/npc-occurrences.json" || npcOccurrenceOutput.SchemaID != npcoccurrencecodec.SchemaID || npcOccurrenceOutput.SchemaVersion != npcoccurrencecodec.SchemaVersion {
t.Fatalf("NPC occurrence output = %#v, want typed occurrence JSON", npcOccurrenceOutput)
}
npcOccurrences := readProductionJSON[dnd.NPCOccurrenceList](t, filepath.Join(runRoot, npcOccurrenceOutput.File))
if len(npcOccurrences.Occurrences) != 1 || npcOccurrences.Occurrences[0].NPCID != npcRegistry.NPCs[0].ID || npcOccurrences.Occurrences[0].Name != "Kesh" || len(npcOccurrences.Occurrences[0].SourceRefs) != 1 || npcOccurrences.Occurrences[0].SourceRefs[0].StartUnitID != 7 {
t.Fatalf("NPC occurrences = %#v, want independently evidenced Kesh registry grounding", npcOccurrences)
}
if itemRegistryOutput.File != "lanes/item-registry.json" || itemRegistryOutput.SchemaID != itemregistrycodec.SchemaID || itemRegistryOutput.SchemaVersion != itemregistrycodec.SchemaVersion {
t.Fatalf("item registry output = %#v, want typed registry JSON", itemRegistryOutput)
}
itemRegistry := readProductionJSON[dnd.ItemRegistry](t, filepath.Join(runRoot, itemRegistryOutput.File))
if len(itemRegistry.Items) != 1 || itemRegistry.Items[0].Name != "Moonblade" {
t.Fatalf("item registry = %#v, want Moonblade identity", itemRegistry)
}
if itemOccurrenceOutput.File != "lanes/item-occurrences.json" || itemOccurrenceOutput.SchemaID != itemoccurrencecodec.SchemaID || itemOccurrenceOutput.SchemaVersion != itemoccurrencecodec.SchemaVersion {
t.Fatalf("item occurrence output = %#v, want typed occurrence JSON", itemOccurrenceOutput)
}
itemOccurrences := readProductionJSON[dnd.ItemOccurrenceList](t, filepath.Join(runRoot, itemOccurrenceOutput.File))
if len(itemOccurrences.Occurrences) != 1 || itemOccurrences.Occurrences[0].ItemID != itemRegistry.Items[0].ID || itemOccurrences.Occurrences[0].Name != "Moonblade" || len(itemOccurrences.Occurrences[0].SourceRefs) != 1 || itemOccurrences.Occurrences[0].SourceRefs[0].StartUnitID != 5 {
t.Fatalf("item occurrences = %#v, want independently evidenced Moonblade registry grounding", itemOccurrences)
}
evidence := readProductionJSON[evidencecontext.Document](t, filepath.Join(runRoot, "evidence-context.json")) evidence := readProductionJSON[evidencecontext.Document](t, filepath.Join(runRoot, "evidence-context.json"))
if !containsString(evidence.SelectedLanes, "enemy-events") || !evidenceHasLane(evidence, "enemy-events") { if len(evidence) == 0 {
t.Fatalf("evidence context = %#v, want direct enemy-event evidence", evidence) t.Fatalf("evidence context = %#v, want selected source-unit evidence", evidence)
}
seenEvidenceUnits := make(map[int]struct{}, len(evidence))
for _, unit := range evidence {
if unit.Ref.SourceID != "session-ravenfall" || unit.Ref.StartUnitID != unit.ID || unit.Ref.EndUnitID != unit.ID {
t.Fatalf("evidence unit = %#v, want unchanged source-unit self-reference", unit)
}
if _, exists := seenEvidenceUnits[unit.ID]; exists {
t.Fatalf("evidence context = %#v, want each source unit once", evidence)
}
seenEvidenceUnits[unit.ID] = struct{}{}
} }
requests := client.requestsFor(enemyevents.PromptID) requests := client.requestsFor(enemyevents.PromptID)
@@ -145,15 +212,46 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
t.Fatalf("enemy event session = %q, want shared session", request.SessionID) t.Fatalf("enemy event session = %q, want shared session", request.SessionID)
} }
for slot, required := range map[string]string{ for slot, required := range map[string]string{
"npcs": "Kesh", "npc_registry": "Kesh",
"combat_turns": "Kesh", "combat_turns": "Kesh",
"npc_interactions": "Kesh", "npc_occurrences": "Kesh",
} { } {
input, ok := request.Inputs[slot] input, ok := request.Inputs[slot]
if !ok || !strings.Contains(string(input.Content), required) || strings.Contains(string(input.Content), "source_refs") || strings.Contains(string(input.Content), "start_unit_id") { if !ok || !strings.Contains(string(input.Content), required) || strings.Contains(string(input.Content), "source_refs") || strings.Contains(string(input.Content), "start_unit_id") {
t.Fatalf("enemy event %s prompt input = %q, want compact source-free grounding", slot, input.Content) t.Fatalf("enemy event %s prompt input = %q, want compact source-free grounding", slot, input.Content)
} }
} }
locationRequests := client.requestsFor(locationoccurrences.PromptID)
if len(locationRequests) != 2 {
t.Fatalf("location occurrence requests = %#v, want one request per scene", locationRequests)
}
for _, request := range locationRequests {
registryInput := request.Inputs["location_registry"]
if !strings.Contains(string(registryInput.Content), "Moon Gate") || !strings.Contains(string(registryInput.Content), "registry_refs") || strings.Contains(string(registryInput.Content), `"id"`) || strings.Contains(string(registryInput.Content), "source_refs") {
t.Fatalf("location occurrence registry input = %q, want contextual selector grounding", registryInput.Content)
}
}
for _, test := range []struct {
promptID string
slot string
name string
requiresIDs bool
}{
{promptID: npcoccurrences.PromptID, slot: "npc_registry", name: "Kesh"},
{promptID: itemoccurrences.PromptID, slot: "item_registry", name: "Moonblade"},
} {
requests := client.requestsFor(test.promptID)
if len(requests) != 2 {
t.Fatalf("%s requests = %#v, want one request per scene", test.promptID, requests)
}
for _, request := range requests {
registryInput := request.Inputs[test.slot]
hasID := strings.Contains(string(registryInput.Content), `"id"`)
if !strings.Contains(string(registryInput.Content), test.name) || hasID != test.requiresIDs || strings.Contains(string(registryInput.Content), "source_refs") {
t.Fatalf("%s registry input = %q, want source-free configured grounding", test.promptID, registryInput.Content)
}
}
}
} }
func completeExampleConfigWithTemporaryCache(t *testing.T) string { func completeExampleConfigWithTemporaryCache(t *testing.T) string {
@@ -193,7 +291,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
switch request.PromptID { switch request.PromptID {
case scenes.PromptID: case scenes.PromptID:
content = []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":6},{"start_unit_id":7,"end_unit_id":11}]}`) content = []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":6},{"start_unit_id":7,"end_unit_id":11}]}`)
case npcs.PromptID: case npcregistry.PromptID:
if combatScene { if combatScene {
content = []byte(`{"npcs":[{"name":"Kesh","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`) content = []byte(`{"npcs":[{"name":"Kesh","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
} else { } else {
@@ -207,18 +305,89 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
kind, title = "combat", "Raiders attack" kind, title = "combat", "Raiders attack"
} }
content = []byte(fmt.Sprintf(`{"kind":%q,"title":%q,"summary":"session scene"}`, kind, title)) content = []byte(fmt.Sprintf(`{"kind":%q,"title":%q,"summary":"session scene"}`, kind, title))
case locations.PromptID:
unitID := 1
if combatScene {
unitID = 7
}
content = []byte(fmt.Sprintf(`{"locations":[{"name":"Moon Gate","source_refs":[{"start_unit_id":%d,"end_unit_id":%d}]}]}`, unitID, unitID))
case locationnormalize.PromptID:
content = []byte(`{"duplicate_groups":[]}`)
case spells.PromptID: case spells.PromptID:
content = []byte(`{"spell_casts":[]}`) content = []byte(`{"spell_casts":[]}`)
case itemevents.PromptID: case itemregistry.PromptID:
content = []byte(`{"events":[]}`) if combatScene {
content = []byte(`{"items":[]}`)
} else {
content = []byte(`{"items":[{"name":"Moonblade","source_refs":[{"start_unit_id":5,"end_unit_id":5}]}]}`)
}
case itemregistrynormalize.PromptID:
content = []byte(`{"duplicate_groups":[]}`)
case itemoccurrences.PromptID:
if combatScene {
content = []byte(`{"occurrences":[]}`)
} else {
var registry struct {
Items []struct {
Name string `json:"name"`
} `json:"items"`
}
if err := json.Unmarshal(request.Inputs["item_registry"].Content, &registry); err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("decode generated item registry: %w", err)
}
if len(registry.Items) != 1 || registry.Items[0].Name != "Moonblade" {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated item registry has %d items, want 1", len(registry.Items))
}
content = []byte(`{"occurrences":[{"name":"Moonblade","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[{"start_unit_id":5,"end_unit_id":5}]}]}`)
}
case combat.PromptID: case combat.PromptID:
content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`) content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`)
case npcinteractions.PromptID: case npcoccurrences.PromptID:
if combatScene { if combatScene {
content = []byte(`{"interactions":[{"name":"Kesh","kind":"combat_opponent","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`) var registry struct {
} else { NPCs []struct {
content = []byte(`{"interactions":[]}`) Name string `json:"name"`
} `json:"npcs"`
} }
if err := json.Unmarshal(request.Inputs["npc_registry"].Content, &registry); err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("decode generated NPC registry: %w", err)
}
if len(registry.NPCs) == 0 || registry.NPCs[0].Name != "Kesh" {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated NPC registry has no NPCs")
}
content = []byte(`{"occurrences":[{"name":"Kesh","kind":"combat_opponent","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
} else {
content = []byte(`{"occurrences":[]}`)
}
case locationoccurrences.PromptID:
var registry struct {
Locations []struct {
Name string `json:"name"`
RegistryRefs []struct {
StartUnitID int `json:"start_unit_id"`
EndUnitID int `json:"end_unit_id"`
} `json:"registry_refs"`
} `json:"locations"`
}
if err := json.Unmarshal(request.Inputs["location_registry"].Content, &registry); err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("decode generated location registry: %w", err)
}
if len(registry.Locations) == 0 {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated location registry has no locations")
}
unitID := 1
location := registry.Locations[0]
if combatScene {
unitID = 7
if len(registry.Locations) > 1 {
location = registry.Locations[1]
}
}
registryRefs, err := json.Marshal(location.RegistryRefs)
if err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("encode location selector: %w", err)
}
content = []byte(fmt.Sprintf(`{"occurrences":[{"name":%q,"registry_refs":%s,"kind":"visited","source_refs":[{"start_unit_id":%d,"end_unit_id":%d}]}]}`, location.Name, registryRefs, unitID, unitID))
case enemyevents.PromptID: case enemyevents.PromptID:
content = []byte(`{"events":[{"name":"Kesh","kind":"fled","source_refs":[{"start_unit_id":10,"end_unit_id":10}]}]}`) content = []byte(`{"events":[{"name":"Kesh","kind":"fled","source_refs":[{"start_unit_id":10,"end_unit_id":10}]}]}`)
default: default:
@@ -254,17 +423,6 @@ func containsString(values []string, want string) bool {
return false return false
} }
func evidenceHasLane(value evidencecontext.Document, laneID string) bool {
for _, context := range value.Contexts {
for _, reference := range context.EvidenceRefs {
if reference.LaneID == laneID {
return true
}
}
}
return false
}
func generatedReferenceBinding(bindings []pipeline.ReferenceBinding, slotName string) (pipeline.ReferenceBinding, bool) { func generatedReferenceBinding(bindings []pipeline.ReferenceBinding, slotName string) (pipeline.ReferenceBinding, bool) {
for _, binding := range bindings { for _, binding := range bindings {
if binding.SlotName == slotName && binding.Artifact != nil { if binding.SlotName == slotName && binding.Artifact != nil {

View File

@@ -1,135 +0,0 @@
package cli
import (
"os"
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
)
func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) {
components := productionTestComponents(t)
resolved, err := pipeline.ResolvePipeline(npcInteractionProfile(pipeline.GeneratedReference("npcs", "npcs")), pipeline.ResolveOptions{}, catalogFromRegistries(components.registries))
if err != nil {
t.Fatalf("ResolvePipeline() error = %v", err)
}
if len(resolved.Steps) != 2 || len(resolved.Steps[1].ArtifactLanes) != 1 {
t.Fatalf("resolved pipeline = %#v", resolved)
}
lane := resolved.Steps[1].ArtifactLanes[0]
if lane.ArtifactKind != dnd.NPCInteractionListKind || lane.Extract.Module != interactionextract.Key || lane.Normalize.Module != interactionnormalize.Key {
t.Fatalf("interaction lane = %#v", lane)
}
for _, bindings := range [][]pipeline.ReferenceBinding{lane.ExtractReferences.Bindings, lane.NormalizeReferences.Bindings} {
if len(bindings) != 1 || bindings[0].SlotName != "npcs" || bindings[0].Artifact == nil || bindings[0].Artifact.Step != "npcs" || bindings[0].Artifact.Lane != "npcs" {
t.Fatalf("generated bindings = %#v", bindings)
}
}
if _, err := pipeline.Prepare(resolved, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
catalog := catalogFromRegistries(components.registries)
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCInteractionListKind)
if !ok || codecSpec.Schema.ID != interactioncodec.SchemaID || codecSpec.Schema.Version != interactioncodec.SchemaVersion {
t.Fatalf("NPC interaction codec spec = %#v", codecSpec)
}
}
func TestProductionNPCInteractionReferencesRequireEarlierCompatibleProducer(t *testing.T) {
components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries)
laterProfile := npcInteractionProfile(pipeline.GeneratedReference("npcs", "npcs"))
laterProfile.Steps[0].ID = "seed"
laterProfile.Steps[0].Artifacts["seed"] = laterProfile.Steps[0].Artifacts["npcs"]
delete(laterProfile.Steps[0].Artifacts, "npcs")
laterProfile.Steps = append(laterProfile.Steps, pipeline.PipelineStepProfile{ID: "future", Artifacts: map[string]pipeline.ArtifactLaneProfile{
"npcs": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
}})
laterProfile.Steps[1].References["npcs"] = pipeline.GeneratedReference("future", "npcs")
tests := []struct {
name string
profile pipeline.PipelineProfile
want string
}{
{name: "missing", profile: npcInteractionProfile(pipeline.ReferenceSource{}), want: "source must not be empty"},
{name: "same step", profile: npcInteractionProfile(pipeline.GeneratedReference("interactions", "interactions")), want: "earlier step"},
{name: "later step", profile: laterProfile, want: "earlier step"},
{name: "wrong artifact kind", profile: npcInteractionProfile(pipeline.GeneratedReference("npcs", "npcs")), want: "does not accept artifact kind"},
}
tests[3].profile.Steps[0].Artifacts["npcs"] = pipeline.ArtifactLaneProfile{Extract: pipeline.Binding("dnd/spells")}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := pipeline.ResolvePipeline(test.profile, pipeline.ResolveOptions{}, catalog)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("ResolvePipeline() error = %v, want %q", err, test.want)
}
})
}
}
func TestProductionNPCInteractionReferencesRejectIncompatibleExternalRegistries(t *testing.T) {
components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries)
root := t.TempDir()
for _, test := range []struct {
name string
file string
content string
prepare bool
want string
}{
{name: "media type", file: "registry.txt", content: "not JSON", want: "media type"},
{name: "artifact schema", file: "registry.json", content: `{"npcs":[{"name":"missing required fields"}]}`, prepare: true, want: "NPC registry"},
} {
t.Run(test.name, func(t *testing.T) {
path := filepath.Join(root, test.file)
if err := os.WriteFile(path, []byte(test.content), 0o600); err != nil {
t.Fatal(err)
}
resolved, err := pipeline.ResolvePipeline(npcInteractionProfile(pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog)
if err != nil {
t.Fatalf("ResolvePipeline() error = %v", err)
}
materialized, _, err := pipeline.MaterializeReferences(resolved, catalog, pipeline.ReferenceMaterializationOptions{})
if !test.prepare {
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("MaterializeReferences() error = %v, want %q", err, test.want)
}
return
}
if err != nil {
t.Fatalf("MaterializeReferences() error = %v", err)
}
if _, err := pipeline.Prepare(materialized, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Prepare() error = %v, want %q", err, test.want)
}
})
}
}
func npcInteractionProfile(reference pipeline.ReferenceSource) pipeline.PipelineProfile {
profile := pipeline.PipelineProfile{
ID: "dnd-npc-interactions",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
Output: pipeline.Binding("json"),
Steps: []pipeline.PipelineStepProfile{
{ID: "npcs", Artifacts: map[string]pipeline.ArtifactLaneProfile{
"npcs": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
}},
{ID: "interactions", References: map[string]pipeline.ReferenceSource{"npcs": reference}, Artifacts: map[string]pipeline.ArtifactLaneProfile{
"interactions": {Extract: pipeline.Binding(interactionextract.Key), Normalize: pipeline.Binding(interactionnormalize.Key)},
}},
},
}
return profile
}

Some files were not shown because too many files have changed in this diff Show More