Compare commits
109 Commits
29fcad6e9b
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| bef8ca263b | |||
| f6d037b613 | |||
| 449b506804 | |||
| 071a78ae22 | |||
| ad1cba41c2 | |||
| 67338798aa | |||
| e95e2f2220 | |||
| 628b8d1800 | |||
| d24d4609b6 | |||
| c7f79fb38e | |||
| 8c071800cf | |||
| 569e12c6f4 | |||
| 5b6eb591b2 | |||
| b630384aa0 | |||
| 297d58f090 | |||
| ee71dc4937 | |||
| 65e5d65d14 | |||
| b40b40aaf3 | |||
| f120be1cb4 | |||
| 17673d74ea | |||
| ef19a03cbf | |||
| 0546f6eb4f | |||
| d28d1062e0 | |||
| b3ebfcef37 | |||
| b70d9f77e3 | |||
| 2a75f40871 | |||
| a705ba74a1 | |||
| 8d9c9e7c87 | |||
| 0b5cc4f251 | |||
| 82ffe85f2d | |||
| b3644abc0e | |||
| d653bf1b90 | |||
| 3e66127b94 | |||
| 5d086c13ca | |||
| d36d4e7689 | |||
| 1456aa51cc | |||
| ffc179c822 | |||
| 8e669a1f14 | |||
| 14bfae216d | |||
| 5a58d87995 | |||
| 557809f364 | |||
| ee600975f0 | |||
| 0d8017e23f | |||
| 37b18edf3d | |||
| cda7a61b47 | |||
| 2ad9283148 | |||
| 41a8a80dda | |||
| 90c7fa6381 | |||
| e3839f8620 | |||
| 0fc2f9ee01 | |||
| 5d6305f21a | |||
| 551e4daea2 | |||
| 3589d33468 | |||
| a22c1a7f59 | |||
| ad85d71b0f | |||
| f3506240c2 | |||
| 70c199aa31 | |||
| e2b82746ab | |||
| 4235507f7b | |||
| b346670cc7 | |||
| 7868c26be7 | |||
| 92e89076a2 | |||
| d9b87347b8 | |||
| 20397ef710 | |||
| fc449863f2 | |||
| 51d62de1f3 | |||
| fc76805075 | |||
| 8e680cf96e | |||
| ece1bca460 | |||
| 516af12916 | |||
| 1015d61b2d | |||
| 4192aa8584 | |||
| 8cf03a2a44 | |||
| 2ee6b495e1 | |||
| f1b120b590 | |||
| 2ec17f5b4f | |||
| 6de470d541 | |||
| a6e176e160 | |||
| 3dfefd0e14 | |||
| f91237e9c0 | |||
| ad9ee076b5 | |||
| ce04387dbc | |||
| e8965ebbbb | |||
| c006b163d5 | |||
| 2f61118e78 | |||
| 5e2ccffc0f | |||
| 3f4a1f2647 | |||
| 9653e06297 | |||
| 916a32195b | |||
| 299c110267 | |||
| 4093ff2e8b | |||
| 39563f3ea0 | |||
| 4b52cace76 | |||
| 16d1b29b14 | |||
| 856b26b718 | |||
| ab70347c5d | |||
| 8ed99ceaee | |||
| 257bca0dc2 | |||
| bad5db3ef3 | |||
| 406ad1d362 | |||
| 5f207b2ab1 | |||
| c613b306ae | |||
| 1c7291e17d | |||
| 2345da106a | |||
| 03761c97dd | |||
| 9cb9ee48a7 | |||
| 08954f17e2 | |||
| a57f83e30d | |||
| f4c05c34ef |
@@ -2,9 +2,9 @@
|
||||
|
||||
Notarius is a Go CLI for turning source material into structured artifacts with
|
||||
configured extraction pipelines. The implemented D&D workflow reads Seriatim
|
||||
transcript JSON and can produce location registries and occurrences, scene
|
||||
descriptions, item and currency events, NPC identities, combat turns, NPC
|
||||
interactions, enemy events, and spell casts.
|
||||
transcript JSON and can produce NPC, location, and item registries; their
|
||||
source-grounded occurrences; scene descriptions, combat turns, enemy events,
|
||||
and spell casts.
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
||||
18
assets/dnd/combat-turns/prompts/instructions.md
Normal file
18
assets/dnd/combat-turns/prompts/instructions.md
Normal 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.
|
||||
@@ -14,7 +14,7 @@ inputs:
|
||||
- name: glossary
|
||||
required: false
|
||||
content_type: text/plain
|
||||
- name: npcs
|
||||
- name: npc_registry
|
||||
required: false
|
||||
content_type: application/json
|
||||
messages:
|
||||
@@ -27,15 +27,13 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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-npcs.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./sharedassets/common-dnd-npc-registry.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -9,4 +9,4 @@ Combat-turn grounding:
|
||||
|
||||
Named combat-opponent grounding:
|
||||
|
||||
{{ input "npc_interactions" }}
|
||||
{{ input "npc_occurrences" }}
|
||||
25
assets/dnd/enemy-events/prompts/instructions.md
Normal file
25
assets/dnd/enemy-events/prompts/instructions.md
Normal 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.
|
||||
@@ -14,13 +14,13 @@ inputs:
|
||||
- name: glossary
|
||||
required: false
|
||||
content_type: text/plain
|
||||
- name: npcs
|
||||
- name: npc_registry
|
||||
required: true
|
||||
content_type: application/json
|
||||
- name: combat_turns
|
||||
required: true
|
||||
content_type: application/json
|
||||
- name: npc_interactions
|
||||
- name: npc_occurrences
|
||||
required: true
|
||||
content_type: application/json
|
||||
messages:
|
||||
@@ -33,17 +33,15 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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-npcs.md
|
||||
content_file: ./sharedassets/common-dnd-npc-registry.md
|
||||
- role: user
|
||||
content_file: ./grounding.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./combat-grounding.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
30
assets/dnd/item-occurrences/prompts/instructions.md
Normal file
30
assets/dnd/item-occurrences/prompts/instructions.md
Normal 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.
|
||||
6
assets/dnd/item-occurrences/prompts/item-registry.md
Normal file
6
assets/dnd/item-occurrences/prompts/item-registry.md
Normal 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" }}
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.npc_interactions
|
||||
id: dnd.item_occurrences
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -14,7 +14,7 @@ inputs:
|
||||
- name: glossary
|
||||
required: false
|
||||
content_type: text/plain
|
||||
- name: npcs
|
||||
- name: item_registry
|
||||
required: true
|
||||
content_type: application/json
|
||||
messages:
|
||||
@@ -27,15 +27,13 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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-npcs.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./item-registry.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -43,5 +41,5 @@ messages:
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_npc_interactions_llm.v1.json
|
||||
schema_path: dnd_item_occurrences_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.location_occurrences.llm",
|
||||
"$id": "notarius.dnd.item_occurrences.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["occurrences"],
|
||||
@@ -10,11 +10,13 @@
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["location_id", "name", "kind", "source_refs"],
|
||||
"required": ["name", "kind", "quantity", "from", "to", "source_refs"],
|
||||
"properties": {
|
||||
"location_id": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"kind": {"enum": ["visited", "planned", "recalled", "mentioned"]},
|
||||
"kind": {"type": "string"},
|
||||
"quantity": {"type": ["integer", "null"]},
|
||||
"from": {"type": ["string", "null"]},
|
||||
"to": {"type": ["string", "null"]},
|
||||
"source_refs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
12
assets/dnd/item-registry/extract/prompts/instructions.md
Normal file
12
assets/dnd/item-registry/extract/prompts/instructions.md
Normal 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.npcs
|
||||
id: dnd.item_registry
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -24,13 +24,11 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -38,5 +36,5 @@ messages:
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_npcs_llm.v1.json
|
||||
schema_path: dnd_item_registry_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.item_registry.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["items"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.npcs.normalize
|
||||
id: dnd.item_registry.normalize
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -12,19 +12,19 @@ messages:
|
||||
- role: system
|
||||
content_file: ./sharedassets/common-dnd-system.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./sharedassets/protocol.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./candidates.md
|
||||
content_file: ./sharedassets/candidates.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
content_file: ./sharedassets/transcript-windows.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_entity_reconcile_llm.v1.json
|
||||
schema_path: semantic_reconciliation_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -1,10 +1,6 @@
|
||||
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
|
||||
only the exact ID and name pair from the supplied location registry. Return an
|
||||
empty occurrences array when no supplied location has an evidenced occurrence
|
||||
in this transcript passage.
|
||||
location, one occurrence kind, and a coherent passage supporting both.
|
||||
|
||||
Use exactly one kind per occurrence:
|
||||
|
||||
@@ -16,9 +12,18 @@ Use exactly one kind per occurrence:
|
||||
|
||||
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 itself explicitly names a supplied registry
|
||||
location, it may be mentioned using the narrow passage that supports that
|
||||
reference.
|
||||
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
|
||||
@@ -26,4 +31,4 @@ 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.
|
||||
state it. Do not summarize location descriptions.
|
||||
11
assets/dnd/location-occurrences/prompts/location-registry.md
Normal file
11
assets/dnd/location-occurrences/prompts/location-registry.md
Normal 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" }}
|
||||
@@ -14,7 +14,7 @@ inputs:
|
||||
- name: glossary
|
||||
required: false
|
||||
content_type: text/plain
|
||||
- name: locations
|
||||
- name: location_registry
|
||||
required: true
|
||||
content_type: application/json
|
||||
messages:
|
||||
@@ -27,15 +27,13 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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: ./locations.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./location-registry.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
assets/dnd/location-registry/extract/prompts/instructions.md
Normal file
13
assets/dnd/location-registry/extract/prompts/instructions.md
Normal 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.locations
|
||||
id: dnd.location_registry
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -24,13 +24,11 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -38,5 +36,5 @@ messages:
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_locations_llm.v1.json
|
||||
schema_path: dnd_location_registry_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.locations.llm",
|
||||
"$id": "notarius.dnd.location_registry.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["locations"],
|
||||
@@ -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.
|
||||
30
assets/dnd/location-registry/normalize/prompts/prompt.yaml
Normal file
30
assets/dnd/location-registry/normalize/prompts/prompt.yaml
Normal 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
|
||||
@@ -1,6 +1,14 @@
|
||||
Return the interactions array even when no interaction is established. Every
|
||||
record must contain name, kind, and source_refs. Cite transcript ranges that
|
||||
support both the NPC identity and the interaction kind.
|
||||
Extract Dungeons & Dragons NPC occurrences from the supplied
|
||||
transcript. Include an occurrence only when the transcript establishes one
|
||||
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:
|
||||
|
||||
45
assets/dnd/npc-occurrences/prompts/prompt.yaml
Normal file
45
assets/dnd/npc-occurrences/prompts/prompt.yaml
Normal 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
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.npc_interactions.llm",
|
||||
"$id": "notarius.dnd.npc_occurrences.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["interactions"],
|
||||
"required": ["occurrences"],
|
||||
"properties": {
|
||||
"interactions": {
|
||||
"occurrences": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
19
assets/dnd/npc-registry/extract/prompts/instructions.md
Normal file
19
assets/dnd/npc-registry/extract/prompts/instructions.md
Normal 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.item_events
|
||||
id: dnd.npc_registry
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -24,13 +24,11 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -38,5 +36,5 @@ messages:
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_item_events_llm.v1.json
|
||||
schema_path: dnd_npc_registry_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.npcs.llm",
|
||||
"$id": "notarius.dnd.npc_registry.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["npcs"],
|
||||
11
assets/dnd/npc-registry/normalize/prompts/instructions.md
Normal file
11
assets/dnd/npc-registry/normalize/prompts/instructions.md
Normal 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`.
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.locations.normalize
|
||||
id: dnd.npc_registry.normalize
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -12,19 +12,19 @@ messages:
|
||||
- role: system
|
||||
content_file: ./sharedassets/common-dnd-system.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./sharedassets/protocol.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./candidates.md
|
||||
content_file: ./sharedassets/candidates.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
content_file: ./sharedassets/transcript-windows.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_entity_reconcile_llm.v1.json
|
||||
schema_path: semantic_reconciliation_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -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
|
||||
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
|
||||
outcome established within the accepted chunk. Do not add analysis, inferred
|
||||
motives, hidden state, future consequences, relationship claims, or facts from
|
||||
outside the chunk. Campaign references may disambiguate names but never add
|
||||
events or lore.
|
||||
|
||||
Do not return identifiers, source identifiers, source ranges, unit identifiers,
|
||||
participants, confidence, or any fields besides kind, title, and summary.
|
||||
outside the chunk.
|
||||
@@ -24,11 +24,9 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
@@ -1,5 +1,5 @@
|
||||
Divide the provided transcript into coherent Dungeons & Dragons scenes for the
|
||||
`dnd/scenes` chunk module.
|
||||
Divide the complete provided transcript into coherent Dungeons & Dragons scenes
|
||||
for the `dnd/scenes` chunk module.
|
||||
|
||||
A scene is a coherent unit of play. Start a new scene when the transcript
|
||||
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
|
||||
scenes over speculative or fine-grained boundaries.
|
||||
|
||||
Return only inclusive `start_unit_id` and `end_unit_id` endpoints for each
|
||||
scene. Do not return titles, modes, participants, summaries, boundary notes,
|
||||
confidence, caveats, final chunk IDs, or chunk indexes.
|
||||
Cover the complete transcript from its first source unit to its last. Return
|
||||
scenes in source-unit order with no gaps or overlaps. Use only positive integer
|
||||
source-unit IDs from the transcript, and give every scene one inclusive
|
||||
`start_unit_id` and one inclusive `end_unit_id`.
|
||||
@@ -21,16 +21,14 @@ messages:
|
||||
content_file: ./sharedassets/common-dnd-references.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
content_file: ./sharedassets/common-dnd-transcript-full.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: dnd_scenes.v1.json
|
||||
schema_path: dnd_scenes_llm.v1.json
|
||||
repair_attempts: 0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.scenes",
|
||||
"$id": "notarius.dnd.scenes.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["scenes"],
|
||||
@@ -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.
|
||||
@@ -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
|
||||
are never evidence for the current transcript.
|
||||
|
||||
{{ input "npcs" }}
|
||||
{{ input "npc_registry" }}
|
||||
5
assets/dnd/shared/prompts/common-dnd-system.md
Normal file
5
assets/dnd/shared/prompts/common-dnd-system.md
Normal 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.
|
||||
3
assets/dnd/shared/prompts/common-dnd-transcript-chunk.md
Normal file
3
assets/dnd/shared/prompts/common-dnd-transcript-chunk.md
Normal 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" }}
|
||||
3
assets/dnd/shared/prompts/common-dnd-transcript-full.md
Normal file
3
assets/dnd/shared/prompts/common-dnd-transcript-full.md
Normal file
@@ -0,0 +1,3 @@
|
||||
The complete ordered transcript of this Dungeons & Dragons gameplay session is provided below.
|
||||
|
||||
{{ input "transcript" }}
|
||||
14
assets/dnd/spells/prompts/instructions.md
Normal file
14
assets/dnd/spells/prompts/instructions.md
Normal 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.
|
||||
@@ -17,7 +17,7 @@ inputs:
|
||||
- name: glossary
|
||||
required: false
|
||||
content_type: text/plain
|
||||
- name: npcs
|
||||
- name: npc_registry
|
||||
required: false
|
||||
content_type: application/json
|
||||
messages:
|
||||
@@ -30,17 +30,15 @@ messages:
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-transcript.md
|
||||
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-npcs.md
|
||||
content_file: ./sharedassets/common-dnd-npc-registry.md
|
||||
- role: user
|
||||
content_file: ./catalog.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./spell-catalog.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
6
assets/dnd/spells/prompts/spell-catalog.md
Normal file
6
assets/dnd/spells/prompts/spell-catalog.md
Normal 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" }}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "notarius.dnd.spells",
|
||||
"$id": "notarius.dnd.spells.llm",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["spell_casts"],
|
||||
@@ -1,2 +1,3 @@
|
||||
Location candidates:
|
||||
Candidate material:
|
||||
|
||||
{{ input "candidates" }}
|
||||
@@ -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.
|
||||
27
assets/generic/normalize/deduplication/prompts/prompt.yaml
Normal file
27
assets/generic/normalize/deduplication/prompts/prompt.yaml
Normal 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
|
||||
@@ -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.
|
||||
2
assets/generic/normalize/deduplication/prompts/system.md
Normal file
2
assets/generic/normalize/deduplication/prompts/system.md
Normal 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.
|
||||
@@ -0,0 +1,3 @@
|
||||
Transcript evidence windows:
|
||||
|
||||
{{ input "transcript" }}
|
||||
@@ -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
15
assets/package.go
Normal 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
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
## Context
|
||||
|
||||
69
docs/adr/0011-centralize-llm-assets.md
Normal file
69
docs/adr/0011-centralize-llm-assets.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -298,7 +298,7 @@ output:
|
||||
enabled: true
|
||||
window_units: 3
|
||||
lanes:
|
||||
- npcs
|
||||
- npc-registry
|
||||
- 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
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -332,15 +333,15 @@ step:
|
||||
steps:
|
||||
- id: describe-session
|
||||
artifacts:
|
||||
npcs:
|
||||
extract: dnd/npcs
|
||||
normalize: dnd/npcs
|
||||
npc-registry:
|
||||
extract: dnd/npc-registry
|
||||
normalize: dnd/npc-registry
|
||||
- id: extract-events
|
||||
references:
|
||||
npcs:
|
||||
npc_registry:
|
||||
artifact:
|
||||
step: describe-session
|
||||
lane: npcs
|
||||
lane: npc-registry
|
||||
artifacts:
|
||||
spells:
|
||||
extract: dnd/spells
|
||||
@@ -370,31 +371,36 @@ selected target declares them:
|
||||
| **players** | Optional text player context. |
|
||||
| **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). |
|
||||
| **locations** | Required normalized location registry for location-occurrence extraction and normalization. |
|
||||
| **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. |
|
||||
| **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. |
|
||||
|
||||
Location-occurrence and enemy-event artifact slots have the following exact
|
||||
binding contracts. Durable semantics and wire shapes remain in their
|
||||
[location-occurrence](integrations/dnd-location-occurrence-artifacts.md) and
|
||||
Registry-backed occurrence and enemy-event artifact slots have the following
|
||||
exact binding contracts. Durable semantics and wire shapes remain in their
|
||||
[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 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `locations` | `dnd/location-list` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize |
|
||||
| `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
|
||||
**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
|
||||
normalized combat-turn module may use optional **npcs**. Location occurrences
|
||||
require **locations** for extraction and normalization. Enemy-event extraction
|
||||
requires all four of its JSON artifact slots; its normalizer requires **npcs**.
|
||||
normalized combat-turn module may use optional **npc_registry**. Location occurrences
|
||||
require **location_registry** for extraction and normalization. Item occurrences require
|
||||
**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.
|
||||
|
||||
@@ -404,26 +410,28 @@ generated bindings.
|
||||
| --- | --- |
|
||||
| Input | **seriatim** |
|
||||
| Chunk | **generic**, **dnd/scenes** |
|
||||
| Extract | **dnd/spells**, **dnd/npcs**, **dnd/combat-turns**, **dnd/item-events**, **dnd/npc-interactions**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/locations**, **dnd/location-occurrences** |
|
||||
| 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** |
|
||||
| Normalize | **noop**, **dnd/spells**, **dnd/npcs**, **dnd/combat-turns**, **dnd/item-events**, **dnd/npc-interactions**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/locations**, **dnd/location-occurrences** |
|
||||
| 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** |
|
||||
|
||||
`dnd/locations` extraction and normalization are `llm_backed`; location
|
||||
normalization may use the pipeline's selected LLM profile for bounded duplicate
|
||||
proposals. `dnd/location-occurrences` extraction is `llm_backed`, while its
|
||||
normalizer is `deterministic`. The complete example binds the registry in one
|
||||
step and the occurrence lane in the next.
|
||||
`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:
|
||||
[spells](integrations/dnd-spell-artifacts.md),
|
||||
[NPCs](integrations/dnd-npc-artifacts.md),
|
||||
[NPC interactions](integrations/dnd-npc-interaction-artifacts.md),
|
||||
[NPC registry](integrations/dnd-npc-registry-artifacts.md),
|
||||
[NPC occurrences](integrations/dnd-npc-occurrence-artifacts.md),
|
||||
[combat turns](integrations/dnd-combat-turn-artifacts.md),
|
||||
[item events](integrations/dnd-item-event-artifacts.md),
|
||||
[scene descriptions](integrations/dnd-scene-description-artifacts.md), and
|
||||
[item registry](integrations/dnd-item-registry-artifacts.md),
|
||||
[item occurrences](integrations/dnd-item-occurrence-artifacts.md),
|
||||
[scene descriptions](integrations/dnd-scene-description-artifacts.md),
|
||||
[enemy events](integrations/dnd-enemy-event-artifacts.md),
|
||||
[locations](integrations/dnd-location-artifacts.md), and
|
||||
[location registry](integrations/dnd-location-registry-artifacts.md), and
|
||||
[location occurrences](integrations/dnd-location-occurrence-artifacts.md).
|
||||
|
||||
## Production Validator Keys And Default Chains
|
||||
@@ -434,13 +442,14 @@ Available validator keys are:
|
||||
| --- | --- |
|
||||
| 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** |
|
||||
| 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** |
|
||||
| Item events | **extract/dnd/item-events/shape**, **extract/dnd/item-events/source_refs**, **extract/dnd/item-events/source_relatedness**, **normalize/dnd/item-events/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 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** |
|
||||
| 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** |
|
||||
| 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** |
|
||||
| Locations | **extract/dnd/locations/shape**, **extract/dnd/locations/source_refs**, **extract/dnd/locations/source_relatedness**, **normalize/dnd/locations/identity** |
|
||||
| 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
|
||||
@@ -450,13 +459,14 @@ same at both stages.
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Locations | generic/valid_json, extract/dnd/locations/shape, extract/dnd/locations/source_refs, generic/valid_json_schema, extract/dnd/locations/source_relatedness | generic/valid_json, extract/dnd/locations/shape, normalize/dnd/locations/identity, extract/dnd/locations/source_refs, generic/valid_json_schema, extract/dnd/locations/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
|
||||
|
||||
@@ -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
|
||||
pipeline-wide artifact rather than a lane entry. Verify its six descriptor
|
||||
fields before decoding the linked file according to the [Published Evidence
|
||||
Context contract](../integrations/evidence-context.md). Use each
|
||||
`evidence_refs` entry as the citation to source material. Its surrounding
|
||||
context range and included units explain the citation, but do not widen or
|
||||
replace the cited source reference.
|
||||
Context contract](../integrations/evidence-context.md). Decode its top-level
|
||||
source-unit array as a reading excerpt. Obtain authoritative citations and lane
|
||||
provenance from the normalized lane artifacts; the excerpt has neither and its
|
||||
nearby units do not widen a lane artifact's cited source reference.
|
||||
|
||||
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
|
||||
@@ -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
|
||||
retention policy, and avoid copying secrets into arguments, logs, or
|
||||
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
|
||||
only when that source content is authorized for the recipient.
|
||||
metadata and can cover most of an input; preserve and share it only when that
|
||||
source content is authorized for the recipient.
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
Normalization trims and, where possible, canonicalizes actor names; orders and
|
||||
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
|
||||
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)
|
||||
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,
|
||||
|
||||
@@ -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. |
|
||||
|
||||
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
|
||||
evidence. An outcome may share evidence with an engagement, in which case both
|
||||
events are retained.
|
||||
|
||||
@@ -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.
|
||||
72
docs/integrations/dnd-item-occurrence-artifacts.md
Normal file
72
docs/integrations/dnd-item-occurrence-artifacts.md
Normal 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.
|
||||
95
docs/integrations/dnd-item-registry-artifacts.md
Normal file
95
docs/integrations/dnd-item-registry-artifacts.md
Normal 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 registry’s 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.
|
||||
@@ -25,7 +25,7 @@ Each occurrence has these required fields:
|
||||
|
||||
| Field | Contract |
|
||||
| --- | --- |
|
||||
| `location_id` | Exact ID from the required normalized [location registry](dnd-location-artifacts.md). |
|
||||
| `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. |
|
||||
@@ -38,7 +38,7 @@ positive and the start may not follow the end.
|
||||
{
|
||||
"occurrences": [
|
||||
{
|
||||
"location_id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa",
|
||||
"location_id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
|
||||
"name": "Moon Gate",
|
||||
"kind": "visited",
|
||||
"source_refs": [
|
||||
@@ -71,12 +71,14 @@ complete canonical evidence sequence.
|
||||
|
||||
## Required grounding and evidence
|
||||
|
||||
Both extraction and normalization require exactly one `locations` reference of
|
||||
kind `dnd/location-list`, media type `application/json`, and at most 1 MiB. The
|
||||
registry provides identity grounding only: unknown IDs and mismatched ID/name
|
||||
pairs 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.
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# D&D Location Artifact
|
||||
# D&D Location Registry Artifact
|
||||
|
||||
This contract defines the durable, source-grounded location registry produced
|
||||
by `dnd/locations`. It records transcript-established physical places for one
|
||||
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.
|
||||
|
||||
@@ -9,12 +9,12 @@ registry, or location description.
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Artifact kind | `dnd/location-list` |
|
||||
| Schema ID | `notarius.dnd.locations` |
|
||||
| Schema name | `notarius_dnd_locations_v1` |
|
||||
| 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.locations.identity.v1` |
|
||||
| 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
|
||||
@@ -39,7 +39,7 @@ identifiers, and the start may not follow the end.
|
||||
{
|
||||
"locations": [
|
||||
{
|
||||
"id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa",
|
||||
"id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
|
||||
"name": "Moon Gate",
|
||||
"source_refs": [
|
||||
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
|
||||
@@ -55,7 +55,7 @@ normalization, collapsed whitespace, and case folding. It hashes compact JSON
|
||||
for this array, using the earliest canonical source reference as the anchor:
|
||||
|
||||
```text
|
||||
["dnd.locations.identity.v1", comparison_name, source_id, start_unit_id, end_unit_id]
|
||||
["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
|
||||
@@ -64,12 +64,14 @@ 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, such
|
||||
as planes, regions, settlements, districts, buildings, rooms, landmarks,
|
||||
routes, and geographic features. A generic label is permitted only when it
|
||||
identifies a specific place in the transcript. Notarius does not infer an
|
||||
unstated place or add hierarchy, coordinates, descriptions, participants, or
|
||||
ownership.
|
||||
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
|
||||
@@ -81,9 +83,11 @@ not evidence for later artifacts.
|
||||
## Consumers and publication
|
||||
|
||||
`dnd/location-occurrences` requires one approved location registry through its
|
||||
`locations` reference slot. Its prompt receives an ordered source-free `{id,
|
||||
name}` projection and must not treat registry references as occurrence
|
||||
evidence. See the [location-occurrence artifact](dnd-location-occurrence-artifacts.md)
|
||||
`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.
|
||||
@@ -1,7 +1,7 @@
|
||||
# D&D NPC Interaction Artifact
|
||||
# D&D NPC Occurrence Artifact
|
||||
|
||||
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
|
||||
or summarize the session.
|
||||
|
||||
@@ -9,35 +9,37 @@ or summarize the session.
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Artifact kind | `dnd/npc-interaction-list` |
|
||||
| Schema ID | `notarius.dnd.npc_interactions` |
|
||||
| Schema name | `notarius_dnd_npc_interactions_v1` |
|
||||
| Artifact kind | `dnd/npc-occurrence-list` |
|
||||
| Schema ID | `notarius.dnd.npc_occurrences` |
|
||||
| Schema name | `notarius_dnd_npc_occurrences_v1` |
|
||||
| Schema version | `v1` |
|
||||
| Media type | `application/json` |
|
||||
|
||||
`v1` is a strict JSON object with required `interactions`; the array may be
|
||||
empty. Interaction and source-reference objects reject unknown fields. An
|
||||
`v1` is a 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 interaction has these required fields:
|
||||
Each occurrence has these required fields:
|
||||
|
||||
| Field | Contract |
|
||||
| --- | --- |
|
||||
| `npc_id` | Exact durable ID 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. |
|
||||
|
||||
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. Extraction evidence
|
||||
for an interaction is confined to its accepted chunk.
|
||||
for an occurrence is confined to its accepted chunk.
|
||||
|
||||
```json
|
||||
{
|
||||
"interactions": [
|
||||
"occurrences": [
|
||||
{
|
||||
"npc_id": "npc:sha256:example",
|
||||
"name": "Mira Thorn",
|
||||
"kind": "dialogue",
|
||||
"source_refs": [
|
||||
@@ -48,7 +50,7 @@ for an interaction is confined to its accepted chunk.
|
||||
}
|
||||
```
|
||||
|
||||
## Interaction categories
|
||||
## Occurrence categories
|
||||
|
||||
| Kind | Meaning |
|
||||
| --- | --- |
|
||||
@@ -65,17 +67,24 @@ for uncertain classification.
|
||||
|
||||
## Identity, evidence, and order
|
||||
|
||||
The required normalized [NPC artifact](dnd-npc-artifacts.md) resolves `name`.
|
||||
Registry references are provenance only and never replace an interaction's own
|
||||
evidence. Normalization canonicalizes recognized registry names, orders and
|
||||
deduplicates exact source references, then orders interactions by valid source
|
||||
The required normalized [NPC registry artifact](dnd-npc-registry-artifacts.md)
|
||||
supplies names-only contextual grounding to the model. Notarius resolves the
|
||||
selected name and writes the exact `{npc_id, name}` pair. An unknown or
|
||||
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.
|
||||
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.
|
||||
|
||||
See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action
|
||||
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
|
||||
publication. Pipeline mechanics are described in
|
||||
[D&D module internals](../internal/dnd.md).
|
||||
@@ -1,6 +1,6 @@
|
||||
# D&D NPC Artifact
|
||||
# D&D NPC Registry Artifact
|
||||
|
||||
This contract defines the durable NPC registry produced by `dnd/npcs`. It is a
|
||||
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.
|
||||
|
||||
@@ -8,12 +8,12 @@ character sheet or a relationship summary.
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Artifact kind | `dnd/npc-list` |
|
||||
| Schema ID | `notarius.dnd.npcs` |
|
||||
| Schema name | `notarius_dnd_npcs_v1` |
|
||||
| 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.npcs.identity.v1` |
|
||||
| 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
|
||||
@@ -37,7 +37,7 @@ identifiers, and the start may not follow the end.
|
||||
{
|
||||
"npcs": [
|
||||
{
|
||||
"id": "npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7",
|
||||
"id": "npc:sha256:35ba5f679aee69e07ae3bd65c44278f29539d5dc9bb5225db1c0060555b23221",
|
||||
"name": "Mira Thorn",
|
||||
"source_refs": [
|
||||
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
|
||||
@@ -48,10 +48,12 @@ identifiers, and the start may not follow the end.
|
||||
```
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -66,15 +68,25 @@ descriptor.
|
||||
## 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 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 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.
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
into `source_refs`.
|
||||
|
||||
|
||||
@@ -67,6 +67,12 @@ including a collision with the embedded catalog. Matching uses the catalog’s
|
||||
case, whitespace, and apostrophe normalization, so authors should avoid names
|
||||
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
|
||||
source-evidence rules are defined by the
|
||||
[D&D spell artifact contract](dnd-spell-artifacts.md).
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Published Evidence Context
|
||||
|
||||
This contract defines the optional `source/evidence-context` artifact emitted
|
||||
by the production JSON output. Its configuration is owned by
|
||||
[Configuration](../config.md#module-bindings-and-validators); its logical-file
|
||||
discovery is owned by [Published JSON Output](json-output.md).
|
||||
by the production JSON output. It is a selected source-unit excerpt for
|
||||
convenient reading alongside normalized lane artifacts; it is not a second
|
||||
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
|
||||
|
||||
@@ -26,91 +28,80 @@ its absence means evidence publication was not enabled for that bundle.
|
||||
|
||||
## Payload
|
||||
|
||||
The v1 payload is a JSON object with required `source_id`, `source_digest`,
|
||||
`window_units`, `selected_lanes`, and `contexts` fields. `selected_lanes` and
|
||||
`contexts` are always arrays; an enabled configuration with no accepted direct
|
||||
evidence publishes `contexts: []`.
|
||||
The v1 payload is a top-level JSON array of generic source units. There is no
|
||||
wrapper, source-level metadata, context grouping, lane identifier, or evidence
|
||||
reference in the payload. An enabled configuration with no contributing
|
||||
accepted evidence publishes `[]`.
|
||||
|
||||
```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,
|
||||
"kind": "transcript_segment",
|
||||
"text": "Aria casts Cure Wounds.",
|
||||
"ref": {
|
||||
"source_id": "session-alpha",
|
||||
"start_unit_id": 10,
|
||||
"end_unit_id": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"kind": "transcript_segment",
|
||||
"text": "The party regroups.",
|
||||
"ref": {
|
||||
"source_id": "session-alpha",
|
||||
"start_unit_id": 20,
|
||||
"end_unit_id": 20
|
||||
}
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"id": 10,
|
||||
"kind": "transcript_segment",
|
||||
"text": "Aria casts Cure Wounds.",
|
||||
"ref": {
|
||||
"source_id": "session-alpha",
|
||||
"start_unit_id": 10,
|
||||
"end_unit_id": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"kind": "transcript_segment",
|
||||
"text": "The party regroups.",
|
||||
"ref": {
|
||||
"source_id": "session-alpha",
|
||||
"start_unit_id": 20,
|
||||
"end_unit_id": 20
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Each context requires a `context_ref` object and `evidence_refs` and `units`
|
||||
arrays. `context_ref` identifies the first and last included unit. Each
|
||||
evidence entry contains a selected `lane_id` and an original `source_ref`. A
|
||||
unit uses the existing source-unit shape: required `id`, `kind`, `text`, and
|
||||
self `ref`, plus optional JSON-object `metadata`. Fixed payload objects reject
|
||||
unknown fields; unit metadata may contain application-defined JSON values.
|
||||
Each source unit has required `id`, `kind`, `text`, and self `ref` fields.
|
||||
`ref` contains `source_id`, `start_unit_id`, and `end_unit_id`, and both unit
|
||||
endpoints identify that unit's `id`. A unit may also contain source-owned
|
||||
`metadata`, an open-ended JSON object. Fixed unit and reference fields are
|
||||
strict: consumers must reject unknown fixed fields, malformed units, invalid
|
||||
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
|
||||
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.
|
||||
## Selection And Citations
|
||||
|
||||
Only accepted outputs from the configured lane allowlist contribute. Rejected,
|
||||
failed, absent, and lane-filtered outputs do not contribute. The artifact never
|
||||
contains raw input bytes, prompts, model responses, auxiliary reference
|
||||
content, credentials, or filesystem paths.
|
||||
The framework obtains direct source references only through typed evidence
|
||||
projections of accepted normalized artifacts in the configured lane allowlist.
|
||||
It validates each reference against the current source document, expands its
|
||||
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
|
||||
document position order, not numeric unit-ID order. Direct evidence entries
|
||||
are deterministically ordered by lane and source reference. Overlapping or
|
||||
contiguous windows merge, and each source unit appears at most once in the
|
||||
resulting contexts.
|
||||
The excerpt contains at most every generic source unit once. It can therefore
|
||||
equal the complete generic source document when coverage is broad or the
|
||||
window is large. No byte-, token-, or compression-size guarantee is made, and
|
||||
the framework does not truncate the excerpt to meet an arbitrary size limit.
|
||||
|
||||
## Consumer Responsibilities And Data Handling
|
||||
|
||||
The artifact is additive to the JSON bundle and is not a lane payload,
|
||||
normalized-output count, checkpoint, or generated reference. Consumers that
|
||||
do not need it must tolerate the absent optional descriptor. Consumers that do
|
||||
use it should preserve the artifact and its schema identity with the run
|
||||
provenance, and should treat its source text and metadata as sensitive durable
|
||||
content.
|
||||
do not need it must tolerate an absent descriptor. Consumers that do use it
|
||||
should validate the descriptor and payload before use, retain the artifact with
|
||||
its schema identity when needed for a run record, and read citations from the
|
||||
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.
|
||||
|
||||
@@ -24,7 +24,7 @@ root for the logical discovery described here.
|
||||
| `warnings.json` | Accepted-output and run warnings. |
|
||||
| `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. |
|
||||
| `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
|
||||
accepted only when their media type is `application/json`.
|
||||
@@ -71,13 +71,14 @@ output encoding fail.
|
||||
Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for
|
||||
that lane. Consumers should use the index descriptor’s schema identity rather
|
||||
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),
|
||||
[NPC interactions](dnd-npc-interaction-artifacts.md),
|
||||
[spells](dnd-spell-artifacts.md), [NPC registry](dnd-npc-registry-artifacts.md),
|
||||
[NPC occurrences](dnd-npc-occurrence-artifacts.md),
|
||||
[combat turns](dnd-combat-turn-artifacts.md),
|
||||
[item events](dnd-item-event-artifacts.md),
|
||||
[scene descriptions](dnd-scene-description-artifacts.md), and
|
||||
[item registry](dnd-item-registry-artifacts.md),
|
||||
[item occurrences](dnd-item-occurrence-artifacts.md),
|
||||
[scene descriptions](dnd-scene-description-artifacts.md),
|
||||
[enemy events](dnd-enemy-event-artifacts.md),
|
||||
[locations](dnd-location-artifacts.md), and
|
||||
[location registry](dnd-location-registry-artifacts.md), and
|
||||
[location occurrences](dnd-location-occurrence-artifacts.md).
|
||||
|
||||
## `manifest.json`
|
||||
|
||||
@@ -7,19 +7,20 @@ selectable keys, bindings, reference syntax, and default validator chains.
|
||||
|
||||
## Durable Artifact Contracts
|
||||
|
||||
The nine 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.
|
||||
|
||||
| Lane | Durable contract |
|
||||
| --- | --- |
|
||||
| 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) |
|
||||
| Item events | [item-event artifacts](../integrations/dnd-item-event-artifacts.md) |
|
||||
| NPC interactions | [NPC-interaction artifacts](../integrations/dnd-npc-interaction-artifacts.md) |
|
||||
| Item occurrences | [item-occurrence artifacts](../integrations/dnd-item-occurrence-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) |
|
||||
| Enemy events | [enemy-event artifacts](../integrations/dnd-enemy-event-artifacts.md) |
|
||||
| Locations | [location artifacts](../integrations/dnd-location-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
|
||||
@@ -28,7 +29,7 @@ The D&D registrar registers the family’s artifact codecs, extractors, typed
|
||||
append-order mergers, normalizers, validators, prompt assets, fallback LLM
|
||||
profile asset, and default validator chains. Each extractor and normalizer has
|
||||
a stable module spec, explicit execution class, strict option decoding, and a
|
||||
typed builder. Scene chunking, every extractor, NPC normalization, and location
|
||||
typed builder. Scene chunking, every extractor, and NPC, location, and item-registry
|
||||
normalization are registered as `llm_backed`; the remaining current D&D mergers
|
||||
and normalizers are `deterministic`. The metadata is available to catalog inspection and
|
||||
resolved-pipeline debug data and determines which selected bindings inherit the
|
||||
@@ -43,52 +44,69 @@ the contracts above define durable data.
|
||||
|
||||
## Prompt Construction
|
||||
|
||||
D&D extractors assemble prompts from an ordered manifest of shared and
|
||||
module-owned assets. The location extractor and occurrence extractor reuse the
|
||||
shared D&D system, evidence, identity, reference, and transcript assets instead
|
||||
of copying their text into individual modules. A manifest’s declared sequence,
|
||||
including cache-control placement, is part of the prompt behavior.
|
||||
D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a
|
||||
local `prompt.yaml` declaration and `instructions.md`; input-specific files
|
||||
such as a catalog, registry, grounding projection, or candidate collection are
|
||||
local only when that module needs them. New extractor content uses its feature
|
||||
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 module’s 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 module’s `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
|
||||
profile. The D&D registrar embeds that fallback profile with the maintained
|
||||
OpenRouter model, timeout, and service-tier policy. An operator may provide a
|
||||
complete profile with the same ID through the configured PromptKit source; that
|
||||
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
|
||||
profile. The D&D registrar registers the fallback, while an operator can
|
||||
replace it with a complete profile of the same ID from the configured PromptKit
|
||||
source. Deployment profile selection is documented in
|
||||
[Configuration](../config.md#promptkit-profiles).
|
||||
|
||||
All extraction prompts share this four-message rendered prefix: the system
|
||||
message without cache control, the identity message without cache control, the
|
||||
campaign-reference message with ephemeral cache control, and the chunk
|
||||
transcript message with ephemeral cache control. This gives equivalent
|
||||
extraction requests the same reusable prefix through their source material.
|
||||
The D&D transcript assets have distinct consumers. Scene chunking consumes the
|
||||
complete-session `common-dnd-transcript-full.md`, while extraction prompts
|
||||
consume the current-chunk `common-dnd-transcript-chunk.md`. NPC, location, and
|
||||
item normalization instead mount the generic semantic-reconciliation
|
||||
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
|
||||
tasks, and instructions follow the transcript because they are not universal
|
||||
across all extraction lanes. The final instructions message carries ephemeral
|
||||
cache control; evidence, registry, catalog, and task messages do not. Preserve
|
||||
this division when changing an extractor or its assets so prompt-cache behavior
|
||||
remains stable.
|
||||
Extraction prompts render the common system and identity messages first, then
|
||||
cached campaign references and the cached chunk transcript. Evidence policy and
|
||||
any lane-specific registry, catalog, or grounding projection follow that
|
||||
prefix. The final module instructions message is ephemeral. This keeps the
|
||||
reusable extraction prefix identical while preserving the lane-specific suffix.
|
||||
|
||||
The other D&D LLM prompts intentionally follow different patterns. Scene
|
||||
chunking has no sibling extraction lane with which to share its full transcript,
|
||||
so it renders campaign references before its task and instructions, then places
|
||||
the cacheable full transcript last. NPC and location normalization share the
|
||||
entity-reconciliation response schema and safety boundary while retaining their
|
||||
own task and identity rules. NPC normalization keeps its task and
|
||||
cacheable instructions before the candidate collection, followed by the
|
||||
cacheable transcript windows: candidates must be available before their
|
||||
supporting evidence is evaluated, and those windows are not a cross-lane
|
||||
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.
|
||||
Scene chunking intentionally uses a different order: system, cached campaign
|
||||
references, uncached module instructions, then the final ephemeral full
|
||||
transcript. Entity normalization also has its own order: D&D system, mandatory
|
||||
generic protocol, ephemeral domain semantic instructions, generic candidate
|
||||
presentation, and final ephemeral generic transcript windows. These orders and
|
||||
cache controls are prompt behavior; change them only through the owning
|
||||
manifest and prompt declaration.
|
||||
|
||||
## Evidence, Candidates, And Normalization
|
||||
|
||||
@@ -101,7 +119,8 @@ result.
|
||||
|
||||
Default chains keep responsibilities separate: structural validators assess the
|
||||
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
|
||||
is documented in
|
||||
[Configuration](../config.md#production-validator-keys-and-default-chains).
|
||||
@@ -111,15 +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
|
||||
rule. Configuration owns the exact validator key and chain position.
|
||||
|
||||
Normalizers are deterministic for spells, combat turns, item events, NPC
|
||||
interactions, scene descriptions, enemy events, and location occurrences. They canonicalize display
|
||||
values and evidence, use source-document order for stable output, and issue
|
||||
bounded warnings for changes or collapsed duplicates. The NPC and location
|
||||
normalizers are intentional exceptions: each first produces a deterministic
|
||||
candidate set, then may use a bounded structured-LLM proposal to reconcile
|
||||
identity groups. Invalid or unusable proposals retain the deterministic result
|
||||
and surface retry or fallback diagnostics; the model does not directly replace
|
||||
durable records.
|
||||
Normalizers are deterministic for spells, combat turns, item occurrences, NPC
|
||||
occurrences, scene descriptions, enemy events, and location occurrences. They
|
||||
canonicalize display values and evidence, use source-document order for stable
|
||||
output, and issue bounded warnings for changes or collapsed duplicates. NPC,
|
||||
item, and location registry normalizers are intentional exceptions: each first
|
||||
produces a deterministic candidate set, then may use a bounded structured-LLM
|
||||
proposal to reconcile identity groups.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -129,33 +185,46 @@ producer provenance; consumers resolve the handed-off artifact into an
|
||||
immutable, validated projection for each operation. External files are checked
|
||||
during preparation, while generated artifacts are resolved at the handoff.
|
||||
|
||||
NPC registries are names-only grounding projections: they may canonicalize
|
||||
actors for spells and combat turns and are required for NPC interactions, but
|
||||
they do not supply evidence. Scene-description registries are eligibility-only
|
||||
projections: they retain the current chunk’s 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
|
||||
interactions to `combat_opponent` names and kinds. Location registries project
|
||||
ordered `{id, name}` pairs to location-occurrence extraction and normalization;
|
||||
exact ID/name matching keeps same-name locations distinguishable. These compact
|
||||
projections, like NPC grounding, are source-free guidance and never event
|
||||
evidence.
|
||||
NPC and item registry consumers receive names-only grounding. Location
|
||||
consumers receive a contextual selector containing the canonical name and the
|
||||
registry references needed to distinguish same-name places. The calling module
|
||||
resolves those supplied selections locally and maps them into the unchanged
|
||||
durable ID/name pair; an unknown or ambiguous selection rejects the complete
|
||||
occurrence result rather than accepting a partial mapping. The NPC registry
|
||||
additionally supplies names-only actor grounding to spells, combat turns, and
|
||||
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
|
||||
|
||||
The following differences are intentional and should remain explicit when a
|
||||
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 |
|
||||
| --- | --- |
|
||||
| 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. |
|
||||
| Item events | Uses campaign context for disambiguation but has no NPC-registry or scene-description dependency. |
|
||||
| NPC interactions | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| Locations | Produces a source-anchored, session-scoped registry. Its LLM-assisted reconciliation is proposal-only and never collapses same-name places without validated identity and evidence rules. |
|
||||
| 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
|
||||
|
||||
@@ -24,6 +24,9 @@ adapter does not own source evidence, artifact conversion, normalization, or
|
||||
durable schemas. Those responsibilities remain with the module and its
|
||||
[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
|
||||
named material to a PromptKit inline artifact while preserving its origin URI,
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
Fallback assets receive a safe content digest for checkpoint identity; raw
|
||||
paths and bytes are never included. The framework’s `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.
|
||||
|
||||
Each LLM-backed module owns its prompt declaration, package-specific assets,
|
||||
and private response schema. Shared D&D wording is owned by the D&D shared
|
||||
asset package; the detailed D&D conventions are in
|
||||
[D&D Module Internals](dnd.md). The mounted prompt assets used by a module also
|
||||
determine its prompt fingerprint. Schema loaders validate JSON, attach identity
|
||||
and digest metadata, make defensive copies, and expose diagnostics without raw
|
||||
schema bytes.
|
||||
LLM-facing content is embedded once by the root `assets` package. Each consumer
|
||||
uses only its scoped subtree, while the module retains ownership of its prompt
|
||||
declaration, ordered manifest, private response-schema identity, and
|
||||
registration. Shared D&D fragments are selected by D&D's shared implementation;
|
||||
the detailed convention is in [D&D Module Internals](dnd.md). This physical
|
||||
arrangement and its data-only boundary are defined by
|
||||
[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
|
||||
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
|
||||
`ErrInvalidStructuredOutput`, while retaining the returned raw bytes and debug
|
||||
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
|
||||
`promptkit.ErrCapacityExceeded` to
|
||||
|
||||
@@ -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
|
||||
capability during preparation and the output boundary owns publication. See
|
||||
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 is intentionally split by family:
|
||||
|
||||
- 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
|
||||
input behavior is defined by the [Seriatim contract](../integrations/seriatim.md).
|
||||
- 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
|
||||
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
|
||||
|
||||
1. Choose the pipeline stage and the typed artifact boundary. Put external
|
||||
|
||||
@@ -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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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.
|
||||
- [LLM Runtime](llm.md): structured completion, scheduling, prompt assets,
|
||||
profiles, and secret handling.
|
||||
- [Module Internals](modules.md): generic extension registration, module
|
||||
construction, validation, and reference mechanics.
|
||||
- [Module Internals](modules.md): generic extension registration, artifact
|
||||
families, module construction, semantic reconciliation, validation, and
|
||||
reference mechanics.
|
||||
- [D&D Module Internals](dnd.md): shared D&D extractor conventions, generated
|
||||
reference projections, and lane-specific exceptions. Durable D&D and
|
||||
Seriatim data shapes remain in the [integration contracts](../integrations/).
|
||||
|
||||
@@ -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
|
||||
relative to the correct configuration or working-directory origin, reads
|
||||
UTF-8 text, verifies media type and size limits, and retains bounded
|
||||
provenance. A generated-artifact selector remains declared but has no bytes
|
||||
until its producing step completes.
|
||||
provenance. For a positive slot limit, it reads at most the limit plus one byte
|
||||
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
|
||||
registry set, clones the resolved data, then constructs the input adapter,
|
||||
chunker, stage-local validators, every typed lane, and output encoder with
|
||||
cloned options, references, and shared dependencies. It also collects stable
|
||||
checkpoint fingerprints. Missing registrations, incompatible typed entries,
|
||||
nil implementations, and constructor failures are reported before source
|
||||
parsing or any stage operation begins.
|
||||
chunker, stage-local validators, every typed lane, and output encoder. Each
|
||||
registered builder receives its own cloned build request immediately before its
|
||||
module-owned code runs. Preparation also collects stable checkpoint
|
||||
fingerprints. Missing registrations, incompatible typed entries, nil
|
||||
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
|
||||
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
|
||||
validator chain. Validators receive immutable canonical input appropriate to
|
||||
their target: chunks, typed values, or serialized codec bytes. They may
|
||||
approve, approve with warnings, reject, or fail. A rejection is an ordinary
|
||||
pipeline result; a validator error is a framework error.
|
||||
their target: chunks, codec-decoded typed candidates, or serialized codec
|
||||
bytes. Each typed validator receives a newly decoded value from the one
|
||||
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
|
||||
complete validation chain. It preserves warnings only from the final accepted
|
||||
|
||||
@@ -47,6 +47,9 @@ The serialized
|
||||
they do not describe a current public state surface.
|
||||
|
||||
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
|
||||
include the producer's artifact kind, complete schema identity, media type,
|
||||
canonical content digest, and size. Ordinary resume compares those fingerprints
|
||||
|
||||
@@ -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
|
||||
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
|
||||
controls to the entire bundle. Selected lanes may collectively cite most of a
|
||||
transcript, so a broad allowlist can make the evidence artifact nearly as
|
||||
sensitive and large as the source itself.
|
||||
controls to the entire bundle. Its selected source-unit excerpt may include
|
||||
every source unit once when coverage is broad or its configured window is
|
||||
large, so do not assume a byte or token reduction or reduced sensitivity.
|
||||
|
||||
## Chunk-Plan Cache
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
extract, merge, and normalize stages, and the output stage aggregates the run's
|
||||
lane outcomes.
|
||||
@@ -39,11 +45,24 @@ implementations. Domain-neutral model and framework layers provide reusable
|
||||
policy, contracts, and orchestration. Concrete input, pipeline, output, and
|
||||
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
|
||||
must not compose the application or take ownership of process behavior. The
|
||||
current packages implementing these layers are inventoried in
|
||||
[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:
|
||||
|
||||
- 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,
|
||||
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
|
||||
type from extraction through merge, normalization, and semantic validation.
|
||||
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-
|
||||
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
|
||||
timeouts. Concurrency control belongs in shared runtime plumbing rather than in
|
||||
individual modules.
|
||||
|
||||
2474
docs/roadmap/archive/audit.md
Normal file
2474
docs/roadmap/archive/audit.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,26 +24,52 @@ not as committed release dates.
|
||||
|
||||
## 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
|
||||
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.
|
||||
### Large-Collection Semantic Reconciliation
|
||||
|
||||
The model may use its own domain knowledge to judge semantic duplication; the
|
||||
generic implementation is responsible only for the common proposal contract,
|
||||
safety checks, and deterministic application of accepted changes.
|
||||
- Evaluate deterministic candidate blocking only after representative registry
|
||||
inputs exceed the active roadmap's bounded single-request limits. Blocking
|
||||
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
|
||||
|
||||
@@ -100,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
|
||||
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
|
||||
|
||||
These ideas are intentionally less specified. Promote one into an earlier
|
||||
|
||||
@@ -35,39 +35,40 @@ pipelines:
|
||||
enabled: true
|
||||
window_units: 3
|
||||
lanes:
|
||||
- item-events
|
||||
- locations
|
||||
- item-occurrences
|
||||
- item-registry
|
||||
- location-registry
|
||||
- location-occurrences
|
||||
- npcs
|
||||
- npc-registry
|
||||
- spells
|
||||
- combat-turns
|
||||
- npc-interactions
|
||||
- npc-occurrences
|
||||
- enemy-events
|
||||
steps:
|
||||
# Establish session-wide reference artifacts alongside independent item events.
|
||||
# Establish session-wide reference artifacts before their consumers.
|
||||
- id: describe-session
|
||||
artifacts:
|
||||
item-events:
|
||||
item-registry:
|
||||
extract:
|
||||
module: dnd/item-events
|
||||
module: dnd/item-registry
|
||||
retries: 2
|
||||
merge: appendorder
|
||||
normalize: dnd/item-events
|
||||
npcs:
|
||||
normalize: dnd/item-registry
|
||||
npc-registry:
|
||||
extract:
|
||||
module: dnd/npcs
|
||||
module: dnd/npc-registry
|
||||
retries: 2
|
||||
merge: appendorder
|
||||
normalize:
|
||||
module: dnd/npcs
|
||||
module: dnd/npc-registry
|
||||
retries: 2
|
||||
locations:
|
||||
location-registry:
|
||||
extract:
|
||||
module: dnd/locations
|
||||
module: dnd/location-registry
|
||||
retries: 2
|
||||
merge: appendorder
|
||||
normalize:
|
||||
module: dnd/locations
|
||||
module: dnd/location-registry
|
||||
retries: 2
|
||||
scene-descriptions:
|
||||
extract:
|
||||
@@ -79,19 +80,29 @@ pipelines:
|
||||
# Accepted registry artifacts and scene-description eligibility artifacts
|
||||
# are supplied in memory to their compatible consumers in this step.
|
||||
references:
|
||||
locations:
|
||||
location_registry:
|
||||
artifact:
|
||||
step: describe-session
|
||||
lane: locations
|
||||
npcs:
|
||||
lane: location-registry
|
||||
npc_registry:
|
||||
artifact:
|
||||
step: describe-session
|
||||
lane: npcs
|
||||
lane: npc-registry
|
||||
scene_descriptions:
|
||||
artifact:
|
||||
step: describe-session
|
||||
lane: scene-descriptions
|
||||
item_registry:
|
||||
artifact:
|
||||
step: describe-session
|
||||
lane: item-registry
|
||||
artifacts:
|
||||
item-occurrences:
|
||||
extract:
|
||||
module: dnd/item-occurrences
|
||||
retries: 2
|
||||
merge: appendorder
|
||||
normalize: dnd/item-occurrences
|
||||
spells:
|
||||
extract:
|
||||
module: dnd/spells
|
||||
@@ -110,12 +121,12 @@ pipelines:
|
||||
retries: 2
|
||||
merge: appendorder
|
||||
normalize: dnd/combat-turns
|
||||
npc-interactions:
|
||||
npc-occurrences:
|
||||
extract:
|
||||
module: dnd/npc-interactions
|
||||
module: dnd/npc-occurrences
|
||||
retries: 2
|
||||
merge: appendorder
|
||||
normalize: dnd/npc-interactions
|
||||
normalize: dnd/npc-occurrences
|
||||
location-occurrences:
|
||||
extract:
|
||||
module: dnd/location-occurrences
|
||||
@@ -124,10 +135,10 @@ pipelines:
|
||||
normalize: dnd/location-occurrences
|
||||
- id: track-enemies
|
||||
references:
|
||||
npcs:
|
||||
npc_registry:
|
||||
artifact:
|
||||
step: describe-session
|
||||
lane: npcs
|
||||
lane: npc-registry
|
||||
scene_descriptions:
|
||||
artifact:
|
||||
step: describe-session
|
||||
@@ -136,10 +147,10 @@ pipelines:
|
||||
artifact:
|
||||
step: extract-events
|
||||
lane: combat-turns
|
||||
npc_interactions:
|
||||
npc_occurrences:
|
||||
artifact:
|
||||
step: extract-events
|
||||
lane: npc-interactions
|
||||
lane: npc-occurrences
|
||||
artifacts:
|
||||
enemy-events:
|
||||
extract:
|
||||
|
||||
81
internal/cli/assembled_enemy_event_codec_contract_test.go
Normal file
81
internal/cli/assembled_enemy_event_codec_contract_test.go
Normal 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
|
||||
}
|
||||
@@ -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})
|
||||
prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{})
|
||||
if err != nil {
|
||||
@@ -161,10 +161,8 @@ func TestAssembledSpellPipelineRejectsUnknownSpellWithoutPromotingAttemptWarning
|
||||
if !reflect.DeepEqual(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 warning.ReasonCode == spellnormalize.ReasonCodeSpellNameUnresolved {
|
||||
t.Fatalf("warnings = %#v, want rejected-attempt warning to remain non-durable", output.Warnings)
|
||||
}
|
||||
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Warnings[0].Scope != "spell_casts[0]" {
|
||||
t.Fatalf("warnings = %#v, want terminal normalize catalog warning", output.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
|
||||
if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" {
|
||||
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)
|
||||
}
|
||||
sceneSlot := referenceSlot(extractSpec.ReferenceSlots, "scene_descriptions")
|
||||
@@ -85,15 +85,15 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
|
||||
PipelineID: "dnd-combat",
|
||||
Catalog: catalog,
|
||||
ReferenceOverrides: []pipeline.ReferenceBinding{
|
||||
{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||
{Stage: pipeline.StageNormalize, 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: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve(bound references) error = %v, want nil", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,20 +20,26 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"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/locations"
|
||||
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"
|
||||
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"
|
||||
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
|
||||
locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
||||
locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locations"
|
||||
npcinteractions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions"
|
||||
npcs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
||||
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"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
|
||||
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locations"
|
||||
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) {
|
||||
@@ -55,17 +61,17 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
|
||||
t.Fatalf("enemy event lane = %#v, want typed production composition", lane)
|
||||
}
|
||||
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"},
|
||||
"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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -78,24 +84,24 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t *testing.T) {
|
||||
func TestMaintainedCompleteExamplePublishesRegistryBackedEntityOccurrences(t *testing.T) {
|
||||
t.Chdir(repositoryPath())
|
||||
outputRoot := filepath.Join(t.TempDir(), "output")
|
||||
configPath := completeExampleConfigWithTemporaryCache(t)
|
||||
@@ -121,18 +127,17 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
||||
|
||||
runRoot := filepath.Join(outputRoot, productionRunID)
|
||||
index := readProductionJSON[exampleOutputIndex](t, filepath.Join(runRoot, "index.json"))
|
||||
var enemyOutput exampleOutputIndexEntry
|
||||
var locationOutput, occurrenceOutput exampleOutputIndexEntry
|
||||
outputs := make(map[string]exampleOutputIndexEntry)
|
||||
for _, entry := range index.OutputFiles {
|
||||
switch entry.LaneID {
|
||||
case "enemy-events":
|
||||
enemyOutput = entry
|
||||
case "locations":
|
||||
locationOutput = entry
|
||||
case "location-occurrences":
|
||||
occurrenceOutput = entry
|
||||
}
|
||||
outputs[entry.LaneID] = entry
|
||||
}
|
||||
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" {
|
||||
t.Fatalf("enemy event output = %#v, want typed enemy-event JSON", enemyOutput)
|
||||
}
|
||||
@@ -140,26 +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 {
|
||||
t.Fatalf("enemy event artifact = %#v, want source-linked Kesh fleeing event", value)
|
||||
}
|
||||
if locationOutput.File != "lanes/locations.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion {
|
||||
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.LocationList](t, filepath.Join(runRoot, locationOutput.File))
|
||||
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 occurrenceOutput.File != "lanes/location-occurrences.json" || occurrenceOutput.SchemaID != locationoccurrencecodec.SchemaID || occurrenceOutput.SchemaVersion != locationoccurrencecodec.SchemaVersion {
|
||||
t.Fatalf("location occurrence output = %#v, want typed occurrence JSON", occurrenceOutput)
|
||||
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, occurrenceOutput.File))
|
||||
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"))
|
||||
for _, laneID := range []string{"enemy-events", "locations", "location-occurrences"} {
|
||||
if !containsString(evidence.SelectedLanes, laneID) || !evidenceHasLane(evidence, laneID) {
|
||||
t.Fatalf("evidence context = %#v, want direct %s evidence", evidence, laneID)
|
||||
if len(evidence) == 0 {
|
||||
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)
|
||||
@@ -171,9 +212,9 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
||||
t.Fatalf("enemy event session = %q, want shared session", request.SessionID)
|
||||
}
|
||||
for slot, required := range map[string]string{
|
||||
"npcs": "Kesh",
|
||||
"combat_turns": "Kesh",
|
||||
"npc_interactions": "Kesh",
|
||||
"npc_registry": "Kesh",
|
||||
"combat_turns": "Kesh",
|
||||
"npc_occurrences": "Kesh",
|
||||
} {
|
||||
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") {
|
||||
@@ -185,9 +226,30 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
||||
t.Fatalf("location occurrence requests = %#v, want one request per scene", locationRequests)
|
||||
}
|
||||
for _, request := range locationRequests {
|
||||
registryInput := request.Inputs["locations"]
|
||||
if !strings.Contains(string(registryInput.Content), "Moon Gate") || !strings.Contains(string(registryInput.Content), `"id"`) || strings.Contains(string(registryInput.Content), "source_refs") {
|
||||
t.Fatalf("location occurrence registry input = %q, want source-free ID grounding", registryInput.Content)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +291,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
|
||||
switch request.PromptID {
|
||||
case scenes.PromptID:
|
||||
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 {
|
||||
content = []byte(`{"npcs":[{"name":"Kesh","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
|
||||
} else {
|
||||
@@ -253,37 +315,79 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
|
||||
content = []byte(`{"duplicate_groups":[]}`)
|
||||
case spells.PromptID:
|
||||
content = []byte(`{"spell_casts":[]}`)
|
||||
case itemevents.PromptID:
|
||||
content = []byte(`{"events":[]}`)
|
||||
case itemregistry.PromptID:
|
||||
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, ®istry); 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:
|
||||
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 {
|
||||
content = []byte(`{"interactions":[{"name":"Kesh","kind":"combat_opponent","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
|
||||
var registry struct {
|
||||
NPCs []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"npcs"`
|
||||
}
|
||||
if err := json.Unmarshal(request.Inputs["npc_registry"].Content, ®istry); 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(`{"interactions":[]}`)
|
||||
content = []byte(`{"occurrences":[]}`)
|
||||
}
|
||||
case locationoccurrences.PromptID:
|
||||
var registry struct {
|
||||
Locations []struct {
|
||||
ID string `json:"id"`
|
||||
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["locations"].Content, ®istry); err != nil {
|
||||
if err := json.Unmarshal(request.Inputs["location_registry"].Content, ®istry); 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
|
||||
locationID := registry.Locations[0].ID
|
||||
location := registry.Locations[0]
|
||||
if combatScene {
|
||||
unitID = 7
|
||||
if len(registry.Locations) > 1 {
|
||||
locationID = registry.Locations[1].ID
|
||||
location = registry.Locations[1]
|
||||
}
|
||||
}
|
||||
content = []byte(fmt.Sprintf(`{"occurrences":[{"location_id":%q,"name":"Moon Gate","kind":"visited","source_refs":[{"start_unit_id":%d,"end_unit_id":%d}]}]}`, locationID, unitID, unitID))
|
||||
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:
|
||||
content = []byte(`{"events":[{"name":"Kesh","kind":"fled","source_refs":[{"start_unit_id":10,"end_unit_id":10}]}]}`)
|
||||
default:
|
||||
@@ -319,17 +423,6 @@ func containsString(values []string, want string) bool {
|
||||
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) {
|
||||
for _, binding := range bindings {
|
||||
if binding.SlotName == slotName && binding.Artifact != nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
|
||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
|
||||
)
|
||||
|
||||
func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
||||
@@ -28,7 +28,7 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
||||
t.Fatalf("artifact lanes = %#v, want one NPC lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
|
||||
}
|
||||
lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
||||
if lane.ID != "npcs" || lane.ArtifactKind != dnd.NPCListKind || lane.Extract.Module != npcextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != npcnormalize.Key {
|
||||
if lane.ID != "npc_registry" || lane.ArtifactKind != dnd.NPCRegistryKind || lane.Extract.Module != npcextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != npcnormalize.Key {
|
||||
t.Fatalf("resolved NPC lane = %#v, want typed production composition", lane)
|
||||
}
|
||||
if len(lane.ExtractReferences.Bindings) != 0 || len(lane.NormalizeReferences.Bindings) != 0 {
|
||||
@@ -36,36 +36,36 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
||||
}
|
||||
|
||||
extractSpec, ok := catalog.Extractors.Spec(npcextract.Key)
|
||||
if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.npcs"}) {
|
||||
if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.npc_registry"}) {
|
||||
t.Fatalf("NPC extractor spec = %#v, want source and artifact capabilities", extractSpec)
|
||||
}
|
||||
mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.NPCListKind)
|
||||
mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.NPCRegistryKind)
|
||||
if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
|
||||
t.Fatalf("NPC merger spec = %#v, want merged capability", mergeSpec)
|
||||
}
|
||||
normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(npcnormalize.Key, dnd.NPCListKind)
|
||||
normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(npcnormalize.Key, dnd.NPCRegistryKind)
|
||||
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
||||
t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
||||
}
|
||||
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCListKind)
|
||||
if !ok || codecSpec.Kind != dnd.NPCListKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
|
||||
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCRegistryKind)
|
||||
if !ok || codecSpec.Kind != dnd.NPCRegistryKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
|
||||
t.Fatalf("NPC codec spec = %#v, want typed v1 durable schema", codecSpec)
|
||||
}
|
||||
|
||||
wantExtractChain := []pipeline.ModuleBinding{
|
||||
pipeline.Binding("generic/valid_json"),
|
||||
pipeline.Binding("extract/dnd/npcs/shape"),
|
||||
pipeline.Binding("extract/dnd/npcs/source_refs"),
|
||||
pipeline.Binding("extract/dnd/npc-registry/shape"),
|
||||
pipeline.Binding("extract/dnd/npc-registry/source_refs"),
|
||||
pipeline.Binding("generic/valid_json_schema"),
|
||||
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
|
||||
pipeline.Binding("extract/dnd/npc-registry/source_relatedness"),
|
||||
}
|
||||
wantNormalizeChain := []pipeline.ModuleBinding{
|
||||
pipeline.Binding("generic/valid_json"),
|
||||
pipeline.Binding("extract/dnd/npcs/shape"),
|
||||
pipeline.Binding("normalize/dnd/npcs/identity"),
|
||||
pipeline.Binding("extract/dnd/npcs/source_refs"),
|
||||
pipeline.Binding("extract/dnd/npc-registry/shape"),
|
||||
pipeline.Binding("normalize/dnd/npc-registry/identity"),
|
||||
pipeline.Binding("extract/dnd/npc-registry/source_refs"),
|
||||
pipeline.Binding("generic/valid_json_schema"),
|
||||
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
|
||||
pipeline.Binding("extract/dnd/npc-registry/source_relatedness"),
|
||||
}
|
||||
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, npcextract.Key); !reflect.DeepEqual(got, wantExtractChain) {
|
||||
t.Fatalf("NPC extract chain = %#v, want %#v", got, wantExtractChain)
|
||||
@@ -90,16 +90,16 @@ func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *tes
|
||||
}
|
||||
|
||||
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
||||
lane := profile.Artifacts["npcs"]
|
||||
lane := profile.Artifacts["npc_registry"]
|
||||
lane.Extract.Options = map[string]any{"unexpected": true}
|
||||
profile.Artifacts["npcs"] = lane
|
||||
profile.Artifacts["npc_registry"] = lane
|
||||
}); err == nil || !strings.Contains(err.Error(), "unknown option") {
|
||||
t.Fatalf("unknown extractor option error = %v, want strict option rejection", err)
|
||||
}
|
||||
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
||||
lane := profile.Artifacts["npcs"]
|
||||
lane := profile.Artifacts["npc_registry"]
|
||||
lane.Normalize.Options = map[string]any{"unexpected": true}
|
||||
profile.Artifacts["npcs"] = lane
|
||||
profile.Artifacts["npc_registry"] = lane
|
||||
}); err == nil || !strings.Contains(err.Error(), "unknown option") {
|
||||
t.Fatalf("unknown normalizer option error = %v, want strict option rejection", err)
|
||||
}
|
||||
@@ -113,9 +113,9 @@ func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *tes
|
||||
t.Fatalf("optional NPC references error = %v, want resolution success", err)
|
||||
}
|
||||
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
||||
lane := profile.Artifacts["npcs"]
|
||||
lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("normalize/dnd/npcs/identity")}
|
||||
profile.Artifacts["npcs"] = lane
|
||||
lane := profile.Artifacts["npc_registry"]
|
||||
lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("normalize/dnd/npc-registry/identity")}
|
||||
profile.Artifacts["npc_registry"] = lane
|
||||
}); err == nil || !strings.Contains(err.Error(), "artifact lane level") {
|
||||
t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func productionNPCContractConfig() config.Config {
|
||||
Input: pipeline.Binding("seriatim"),
|
||||
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
|
||||
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"npcs": {
|
||||
"npc_registry": {
|
||||
Extract: pipeline.ModuleBinding{Module: npcextract.Key, Retries: 2},
|
||||
Normalize: pipeline.Binding(npcnormalize.Key),
|
||||
},
|
||||
|
||||
217
internal/cli/dnd_occurrences_contract_test.go
Normal file
217
internal/cli/dnd_occurrences_contract_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
|
||||
locationregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
|
||||
occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
|
||||
npcregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
|
||||
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
||||
occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
|
||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
|
||||
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
|
||||
locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
||||
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
|
||||
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
|
||||
occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
|
||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
|
||||
npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
)
|
||||
|
||||
func TestProductionNPCOccurrencePipelineResolvesAndPrepares(t *testing.T) {
|
||||
components := productionTestComponents(t)
|
||||
resolved, err := pipeline.ResolvePipeline(npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), 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.NPCOccurrenceListKind || lane.Extract.Module != occurrenceextract.Key || lane.Normalize.Module != occurrencenormalize.Key {
|
||||
t.Fatalf("occurrence lane = %#v", lane)
|
||||
}
|
||||
for _, bindings := range [][]pipeline.ReferenceBinding{lane.ExtractReferences.Bindings, lane.NormalizeReferences.Bindings} {
|
||||
if len(bindings) != 1 || bindings[0].SlotName != "npc_registry" || bindings[0].Artifact == nil || bindings[0].Artifact.Step != "npc-registry" || bindings[0].Artifact.Lane != "npc_registry" {
|
||||
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.NPCOccurrenceListKind)
|
||||
if !ok || codecSpec.Schema.ID != occurrencecodec.SchemaID || codecSpec.Schema.Version != occurrencecodec.SchemaVersion {
|
||||
t.Fatalf("NPC occurrence codec spec = %#v", codecSpec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionNPCOccurrenceReferencesRequireEarlierCompatibleProducer(t *testing.T) {
|
||||
components := productionTestComponents(t)
|
||||
catalog := catalogFromRegistries(components.registries)
|
||||
laterProfile := npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry"))
|
||||
laterProfile.Steps[0].ID = "seed"
|
||||
laterProfile.Steps[0].Artifacts["seed"] = laterProfile.Steps[0].Artifacts["npc_registry"]
|
||||
delete(laterProfile.Steps[0].Artifacts, "npc_registry")
|
||||
laterProfile.Steps = append(laterProfile.Steps, pipeline.PipelineStepProfile{ID: "future", Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"npc_registry": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
|
||||
}})
|
||||
laterProfile.Steps[1].References["npc_registry"] = pipeline.GeneratedReference("future", "npc_registry")
|
||||
tests := []struct {
|
||||
name string
|
||||
profile pipeline.PipelineProfile
|
||||
want string
|
||||
}{
|
||||
{name: "missing", profile: npcOccurrenceProfile(pipeline.ReferenceSource{}), want: "source must not be empty"},
|
||||
{name: "same step", profile: npcOccurrenceProfile(pipeline.GeneratedReference("occurrences", "occurrences")), want: "earlier step"},
|
||||
{name: "later step", profile: laterProfile, want: "earlier step"},
|
||||
{name: "wrong artifact kind", profile: npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), want: "does not accept artifact kind"},
|
||||
}
|
||||
tests[3].profile.Steps[0].Artifacts["npc_registry"] = 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 TestProductionNPCOccurrenceReferencesRejectIncompatibleExternalRegistries(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(npcOccurrenceProfile(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 TestProductionOccurrencePipelinesAcceptCompatibleExternalRegistries(t *testing.T) {
|
||||
components := productionTestComponents(t)
|
||||
catalog := catalogFromRegistries(components.registries)
|
||||
reference := source.SourceRef{SourceID: "external-registry", StartUnitID: 1, EndUnitID: 1}
|
||||
npcContent, err := npcregistrycodec.New().Encode(dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||
ID: npcidentity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{reference},
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
locationContent, err := locationregistrycodec.New().Encode(dnd.LocationRegistry{Locations: []dnd.Location{{
|
||||
ID: locationidentity.DeriveID("Moon Gate", []source.SourceRef{reference}), Name: "Moon Gate", SourceRefs: []source.SourceRef{reference},
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
itemContent, err := itemregistrycodec.New().Encode(dnd.ItemRegistry{Items: []dnd.Item{{
|
||||
ID: itemidentity.DeriveID("Moonblade"), Name: "Moonblade", SourceRefs: []source.SourceRef{reference},
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
slot string
|
||||
extractor string
|
||||
normalizer string
|
||||
content []byte
|
||||
}{
|
||||
{name: "NPC", slot: "npc_registry", extractor: occurrenceextract.Key, normalizer: occurrencenormalize.Key, content: npcContent},
|
||||
{name: "location", slot: "location_registry", extractor: locationoccurrenceextract.Key, normalizer: locationoccurrencenormalize.Key, content: locationContent},
|
||||
{name: "item", slot: "item_registry", extractor: itemoccurrenceextract.Key, normalizer: itemoccurrencenormalize.Key, content: itemContent},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
path := filepath.Join(root, strings.ToLower(test.name)+"-registry.json")
|
||||
if err := os.WriteFile(path, test.content, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolved, err := pipeline.ResolvePipeline(externalOccurrenceProfile(test.slot, test.extractor, test.normalizer, pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolvePipeline() error = %v", err)
|
||||
}
|
||||
materialized, warnings, err := pipeline.MaterializeReferences(resolved, catalog, pipeline.ReferenceMaterializationOptions{})
|
||||
if err != nil || len(warnings) != 0 {
|
||||
t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings)
|
||||
}
|
||||
if _, err := pipeline.Prepare(materialized, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err != nil {
|
||||
t.Fatalf("Prepare() error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func externalOccurrenceProfile(slot, extractor, normalizer string, reference pipeline.ReferenceSource) pipeline.PipelineProfile {
|
||||
return pipeline.PipelineProfile{
|
||||
ID: "external-registry-occurrences",
|
||||
Input: pipeline.Binding("seriatim"),
|
||||
Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
|
||||
Output: pipeline.Binding("json"),
|
||||
Steps: []pipeline.PipelineStepProfile{{
|
||||
ID: "occurrences",
|
||||
References: map[string]pipeline.ReferenceSource{slot: reference},
|
||||
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"occurrences": {Extract: pipeline.Binding(extractor), Normalize: pipeline.Binding(normalizer)},
|
||||
},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func npcOccurrenceProfile(reference pipeline.ReferenceSource) pipeline.PipelineProfile {
|
||||
profile := pipeline.PipelineProfile{
|
||||
ID: "dnd-npc-occurrences",
|
||||
Input: pipeline.Binding("seriatim"),
|
||||
Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
|
||||
Output: pipeline.Binding("json"),
|
||||
Steps: []pipeline.PipelineStepProfile{
|
||||
{ID: "npc-registry", Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"npc_registry": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
|
||||
}},
|
||||
{ID: "occurrences", References: map[string]pipeline.ReferenceSource{"npc_registry": reference}, Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"occurrences": {Extract: pipeline.Binding(occurrenceextract.Key), Normalize: pipeline.Binding(occurrencenormalize.Key)},
|
||||
}},
|
||||
},
|
||||
}
|
||||
return profile
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
||||
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locations"
|
||||
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
|
||||
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
|
||||
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locations"
|
||||
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
|
||||
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
||||
)
|
||||
@@ -58,11 +58,11 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
|
||||
t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err)
|
||||
}
|
||||
if example.name == "complete" {
|
||||
if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-events,locations,npcs,scene-descriptions|extract-events:combat-turns,location-occurrences,npc-interactions,spells|track-enemies:enemy-events" {
|
||||
if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-registry,location-registry,npc-registry,scene-descriptions|extract-events:combat-turns,item-occurrences,location-occurrences,npc-occurrences,spells|track-enemies:enemy-events" {
|
||||
t.Fatalf("complete example steps and lanes = %v, want the documented D&D extractor composition", got)
|
||||
}
|
||||
locationLane := referenceContractLane(t, materialized, "locations")
|
||||
if locationLane.ArtifactKind != dnd.LocationListKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 {
|
||||
locationLane := referenceContractLane(t, materialized, "location-registry")
|
||||
if locationLane.ArtifactKind != dnd.LocationRegistryKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 {
|
||||
t.Fatalf("location lane = %#v, want typed registry composition", locationLane)
|
||||
}
|
||||
occurrenceLane := referenceContractLane(t, materialized, "location-occurrences")
|
||||
@@ -70,8 +70,8 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
|
||||
t.Fatalf("location occurrence lane = %#v, want typed occurrence composition", occurrenceLane)
|
||||
}
|
||||
for _, target := range []pipeline.ResolvedReferenceTarget{occurrenceLane.ExtractReferences, occurrenceLane.NormalizeReferences} {
|
||||
binding, found := generatedReferenceBinding(target.Bindings, "locations")
|
||||
if !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "locations" {
|
||||
binding, found := generatedReferenceBinding(target.Bindings, "location_registry")
|
||||
if !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "location-registry" {
|
||||
t.Fatalf("location occurrence %s reference = %#v, want generated location registry", target.Stage, binding)
|
||||
}
|
||||
}
|
||||
@@ -88,21 +88,21 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
|
||||
len(spellLane.NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
|
||||
t.Fatalf("complete example spell catalog reference was not materialized: %#v", spellLane)
|
||||
}
|
||||
itemEventLane := referenceContractLane(t, materialized, "item-events")
|
||||
for _, references := range []pipeline.ResolvedReferenceTarget{itemEventLane.ExtractReferences, itemEventLane.NormalizeReferences} {
|
||||
if _, found := references.ReferenceSet.Slots["npcs"]; found {
|
||||
t.Fatalf("item event lane unexpectedly depends on generated NPCs: %#v", itemEventLane)
|
||||
itemOccurrenceLane := referenceContractLane(t, materialized, "item-occurrences")
|
||||
for _, references := range []pipeline.ResolvedReferenceTarget{itemOccurrenceLane.ExtractReferences, itemOccurrenceLane.NormalizeReferences} {
|
||||
if _, found := references.ReferenceSet.Slots["npc_registry"]; found {
|
||||
t.Fatalf("item occurrence lane unexpectedly depends on generated NPCs: %#v", itemOccurrenceLane)
|
||||
}
|
||||
if _, found := references.ReferenceSet.Slots["scene_descriptions"]; found {
|
||||
t.Fatalf("item event lane unexpectedly depends on generated scene descriptions: %#v", itemEventLane)
|
||||
t.Fatalf("item occurrence lane unexpectedly depends on generated scene descriptions: %#v", itemOccurrenceLane)
|
||||
}
|
||||
}
|
||||
enemyEventLane := referenceContractLane(t, materialized, "enemy-events")
|
||||
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"},
|
||||
"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(enemyEventLane.ExtractReferences.Bindings, slot)
|
||||
if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user