Compare commits
30 Commits
406ad1d362
...
516af12916
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
Notarius is a Go CLI for turning source material into structured artifacts with
|
Notarius is a Go CLI for turning source material into structured artifacts with
|
||||||
configured extraction pipelines. The implemented D&D workflow reads Seriatim
|
configured extraction pipelines. The implemented D&D workflow reads Seriatim
|
||||||
transcript JSON and can produce location registries and occurrences, scene
|
transcript JSON and can produce NPC, location, and item registries; their
|
||||||
descriptions, item and currency events, NPC identities, combat turns, NPC
|
source-grounded occurrences; scene descriptions, combat turns, enemy events,
|
||||||
interactions, enemy events, and spell casts.
|
and spell casts.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
Return the combat_turns array even when no combat turn is established. Return
|
Extract Dungeons & Dragons combat-turn artifacts from the supplied transcript.
|
||||||
actor, turn_kind, and source_refs for every record. For turn_kind, use exactly
|
Include a record only when the transcript establishes that an in-world
|
||||||
one of: turn, reaction, legendary_action, lair_action, or other. Cite the
|
participant takes a combat turn or performs a discrete interrupting combat
|
||||||
transcript ranges that establish both the actor and the combat event. Use the
|
event. Keep events in transcript chronology; place an interrupting event where
|
||||||
players, party, and transcript context to map speakers to in-world actors. NPC
|
it occurs.
|
||||||
names may help disambiguate identity but do not replace transcript evidence.
|
|
||||||
|
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
|
- name: glossary
|
||||||
required: false
|
required: false
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
- name: npcs
|
- name: npc_registry
|
||||||
required: false
|
required: false
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
@@ -27,15 +27,13 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-npcs.md
|
content_file: ./sharedassets/common-dnd-npc-registry.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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. Interrupting events belong at the point where they occur in transcript
|
|
||||||
chronology.
|
|
||||||
|
|
||||||
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 D&D rules knowledge. Preserve the session as
|
|
||||||
played and attribute relevant nonstandard rulings to the GM or table.
|
|
||||||
|
|
||||||
Unmatched actors remain permitted.
|
|
||||||
|
|
||||||
Place all supporting transcript ranges for a turn in its turn-level source_refs
|
|
||||||
collection.
|
|
||||||
@@ -9,4 +9,4 @@ Combat-turn grounding:
|
|||||||
|
|
||||||
Named combat-opponent grounding:
|
Named combat-opponent grounding:
|
||||||
|
|
||||||
{{ input "npc_interactions" }}
|
{{ input "npc_occurrences" }}
|
||||||
@@ -1,14 +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,
|
Exclude party members, allies, neutral observers, mentioned-but-absent enemies,
|
||||||
hazards, traps, environmental effects, uncertain allegiance, table talk,
|
hazards, traps, environmental effects, uncertain allegiance, table talk,
|
||||||
planning, hypotheses, recaps outside this passage, and downstream inference.
|
planning, hypotheses, recaps outside this passage, and downstream inference.
|
||||||
|
|
||||||
Do not infer an engagement or outcome from initiative, turn absence, damage,
|
Do not infer an engagement or outcome from initiative, turn absence, damage,
|
||||||
defeat, movement, a scene ending, combat-opponent grounding, or any auxiliary
|
defeat, movement, or a scene ending.
|
||||||
artifact. Auxiliary inputs can guide attention but cannot prove or supply an
|
|
||||||
event. Cite only narrow current-transcript ranges that establish each event.
|
|
||||||
|
|
||||||
Return the `events` array even when no enemy event is established. Every event
|
|
||||||
must contain only `name`, `kind`, and `source_refs`. Use exactly one kind:
|
|
||||||
`engaged`, `killed`, `fled`, `captured`, or `incapacitated`. Each source range
|
|
||||||
uses integer `start_unit_id` and `end_unit_id`; omit `source_id` because
|
|
||||||
Notarius assigns the current source identity.
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ inputs:
|
|||||||
- name: glossary
|
- name: glossary
|
||||||
required: false
|
required: false
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
- name: npcs
|
- name: npc_registry
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
- name: combat_turns
|
- name: combat_turns
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
- name: npc_interactions
|
- name: npc_occurrences
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
@@ -33,17 +33,15 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-npcs.md
|
content_file: ./sharedassets/common-dnd-npc-registry.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./grounding.md
|
content_file: ./combat-grounding.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Extract Dungeons & Dragons enemy events from the supplied combat transcript.
|
|
||||||
|
|
||||||
Return an `engaged` event only when the transcript directly establishes that a
|
|
||||||
subject is actively opposing the party in combat. Return `killed`, `fled`,
|
|
||||||
`captured`, or `incapacitated` only when the transcript explicitly establishes
|
|
||||||
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 engagement 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.
|
|
||||||
|
|
||||||
Use a normalized NPC registry spelling when the transcript identifies that
|
|
||||||
named NPC. 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.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Extract meaningful Dungeons & Dragons item and currency events established by
|
|
||||||
the transcript. Record only discoveries and changes in party possession, with
|
|
||||||
the transcript ranges that support each event.
|
|
||||||
|
|
||||||
This is an event history, not an inventory or ledger. Do not calculate balances,
|
|
||||||
resolve item identity across records, or infer ownership that the transcript
|
|
||||||
does not establish.
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
Return one event only when the transcript establishes a meaningful item or
|
Extract meaningful Dungeons & Dragons item and currency occurrences: discoveries and changes
|
||||||
currency occurrence. Use a concise observed item name and preserve the stated
|
in party possession established by the transcript. This is an occurrence history,
|
||||||
currency denomination. Set `quantity` to the explicitly stated integer, or to
|
not an inventory or ledger: do not calculate balances, resolve item identity
|
||||||
`null` when the transcript does not state one.
|
across records, or infer ownership that the transcript does not establish.
|
||||||
|
|
||||||
|
For every occurrence, copy the exact `item_id` and `name` pair from the supplied
|
||||||
|
item registry. Record a stated quantity as an integer and leave it null when the transcript
|
||||||
|
does not state one. Use a concise observed item name and preserve the stated
|
||||||
|
currency denomination.
|
||||||
|
|
||||||
Use `discovered` when the party learns of or encounters an item without
|
Use `discovered` when the party learns of or encounters an item without
|
||||||
establishing possession. Use `acquired` when the party or a party member gains
|
establishing possession. Use `acquired` when the party or a party member gains
|
||||||
@@ -13,14 +18,13 @@ when the transcript explicitly describes it being physically destroyed or
|
|||||||
expended as a non-payment component. Use `transferred` only when possession
|
expended as a non-payment component. Use `transferred` only when possession
|
||||||
moves between two distinct named party members.
|
moves between two distinct named party members.
|
||||||
|
|
||||||
Return both `from` and `to` for every event, using `null` when a holder does not
|
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`
|
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 `null` and provide `to`; for `lost` and `consumed`, provide `from` and set
|
||||||
`to` to `null`; and for `transferred`, provide both holders. Use `party` only
|
`to` to `null`; and for `transferred`, provide both holders. Use `party` only
|
||||||
for collective or unresolved party possession, never for either side of a
|
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.
|
transfer. Do not emit a transfer for a gift, sale, or payment outside the party.
|
||||||
|
|
||||||
Ordinary non-depleting use is not an event. Do not infer acquisition from a
|
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
|
discovery, or discovery from an acquisition: emit both only when each is
|
||||||
independently established. Every event needs at least one narrow transcript
|
independently established.
|
||||||
range. Return no lore, inventory totals, aliases, or unstated holder details.
|
|
||||||
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 copy one registry item's exact `id` and exact `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"
|
version: "v1"
|
||||||
default_profile: dnd-extraction
|
default_profile: dnd-extraction
|
||||||
inputs:
|
inputs:
|
||||||
@@ -14,7 +14,7 @@ inputs:
|
|||||||
- name: glossary
|
- name: glossary
|
||||||
required: false
|
required: false
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
- name: npcs
|
- name: item_registry
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
@@ -27,15 +27,13 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-npcs.md
|
content_file: ./item-registry.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -43,5 +41,5 @@ messages:
|
|||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: dnd_npc_interactions_llm.v1.json
|
schema_path: dnd_item_occurrences_llm.v1.json
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.item_events.llm",
|
"$id": "notarius.dnd.item_occurrences.llm",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["events"],
|
"required": ["occurrences"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"events": {
|
"occurrences": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["name", "kind", "quantity", "from", "to", "source_refs"],
|
"required": ["item_id", "name", "kind", "quantity", "from", "to", "source_refs"],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"item_id": {"type": "string"},
|
||||||
"name": {"type": "string"},
|
"name": {"type": "string"},
|
||||||
"kind": {"type": "string"},
|
"kind": {"type": "string"},
|
||||||
"quantity": {"type": ["integer", "null"]},
|
"quantity": {"type": ["integer", "null"]},
|
||||||
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"
|
version: "v1"
|
||||||
default_profile: dnd-extraction
|
default_profile: dnd-extraction
|
||||||
inputs:
|
inputs:
|
||||||
@@ -24,13 +24,11 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -38,5 +36,5 @@ messages:
|
|||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: dnd_npcs_llm.v1.json
|
schema_path: dnd_item_registry_llm.v1.json
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
@@ -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"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
assets/dnd/item-registry/normalize/prompts/candidates.md
Normal file
2
assets/dnd/item-registry/normalize/prompts/candidates.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Item candidates:
|
||||||
|
{{ input "candidates" }}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Use candidate names and cited transcript windows only to determine whether
|
||||||
|
candidates identify the same item type or unique designation. Do not treat
|
||||||
|
nearby evidence, similar objects, or a shared owner as sufficient. Keep
|
||||||
|
currency denominations, materially different item types, and 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"
|
version: "v1"
|
||||||
default_profile: dnd-extraction
|
default_profile: dnd-extraction
|
||||||
inputs:
|
inputs:
|
||||||
@@ -12,7 +12,7 @@ messages:
|
|||||||
- role: system
|
- role: system
|
||||||
content_file: ./sharedassets/common-dnd-system.md
|
content_file: ./sharedassets/common-dnd-system.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./task.md
|
content_file: ./instructions.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -20,7 +20,7 @@ messages:
|
|||||||
- role: user
|
- role: user
|
||||||
content_file: ./candidates.md
|
content_file: ./candidates.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-windows.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
output:
|
output:
|
||||||
@@ -1,8 +1,29 @@
|
|||||||
Return the occurrences array even when no occurrence is established. Every
|
Extract Dungeons & Dragons location occurrences from the supplied transcript.
|
||||||
record must contain location_id, name, kind, and source_refs. Copy location_id
|
Include an occurrence only when the transcript establishes one supplied
|
||||||
and name from one supplied registry record, and cite only narrow transcript
|
location, one occurrence kind, and a coherent passage supporting both.
|
||||||
ranges that support both that location and its classified occurrence.
|
|
||||||
|
|
||||||
Do not summarize location descriptions, infer a missing registry record, or
|
Use exactly one kind per occurrence:
|
||||||
use registry context as evidence. Omit source_id; Notarius assigns the current
|
|
||||||
transcript source identity.
|
- visited: party members are physically present, arrive, remain, or depart;
|
||||||
|
- planned: the party explicitly proposes, intends, or agrees to future travel;
|
||||||
|
- recalled: the transcript explicitly recounts an earlier party visit; or
|
||||||
|
- mentioned: the location is explicitly referenced without stronger support,
|
||||||
|
including non-actionable speculation or a mere hypothetical reference.
|
||||||
|
|
||||||
|
A mere hypothetical or speculative reference is not planned unless the
|
||||||
|
transcript also establishes an actual proposal, intention, or agreement to
|
||||||
|
travel. When the hypothetical explicitly names a supplied location, it may be
|
||||||
|
mentioned.
|
||||||
|
|
||||||
|
A generic phrase in the current chunk may refer to a supplied named registry
|
||||||
|
location only when the chunk's context supports that coreference. It must not
|
||||||
|
create a registry location, and registry content or provenance must never
|
||||||
|
replace current-chunk evidence.
|
||||||
|
|
||||||
|
For overlapping support, visited outranks planned, recalled, and mentioned;
|
||||||
|
planned outranks recalled and mentioned; recalled outranks mentioned. A passage
|
||||||
|
may produce multiple records when it independently establishes separate facts,
|
||||||
|
such as recalling an earlier visit while planning a return. Omit inferred,
|
||||||
|
unstated, uncertain, or unsupported places and occurrences. Do not infer a
|
||||||
|
location or occurrence from surrounding events when the transcript does not
|
||||||
|
state it. Do not summarize location descriptions.
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ Registry content is context, not occurrence evidence. Do not derive an
|
|||||||
occurrence or a source range from the registry, and do not infer a location
|
occurrence or a source range from the registry, and do not infer a location
|
||||||
that is absent from it.
|
that is absent from it.
|
||||||
|
|
||||||
{{ input "locations" }}
|
{{ input "location_registry" }}
|
||||||
@@ -14,7 +14,7 @@ inputs:
|
|||||||
- name: glossary
|
- name: glossary
|
||||||
required: false
|
required: false
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
- name: locations
|
- name: location_registry
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
@@ -27,15 +27,13 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./locations.md
|
content_file: ./location-registry.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
||||||
Use exactly one kind per occurrence:
|
|
||||||
|
|
||||||
- visited: party members are physically present, arrive, remain, or depart;
|
|
||||||
- planned: the party explicitly proposes, intends, or agrees to future travel;
|
|
||||||
- recalled: the transcript explicitly recounts an earlier party visit; or
|
|
||||||
- mentioned: the location is explicitly referenced without stronger support,
|
|
||||||
including non-actionable speculation or a mere hypothetical reference.
|
|
||||||
|
|
||||||
A mere hypothetical or speculative reference is not planned unless the
|
|
||||||
transcript also establishes an actual proposal, intention, or agreement to
|
|
||||||
travel. When the hypothetical itself explicitly names a supplied registry
|
|
||||||
location, it may be mentioned using the narrow passage that supports that
|
|
||||||
reference.
|
|
||||||
|
|
||||||
For overlapping support, visited outranks planned, recalled, and mentioned;
|
|
||||||
planned outranks recalled and mentioned; recalled outranks mentioned. A passage
|
|
||||||
may produce multiple records when it independently establishes separate facts,
|
|
||||||
such as recalling an earlier visit while planning a return. Omit inferred,
|
|
||||||
unstated, uncertain, or unsupported places and occurrences. Do not infer a
|
|
||||||
location or occurrence from surrounding events when the transcript does not
|
|
||||||
state it.
|
|
||||||
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"
|
version: "v1"
|
||||||
default_profile: dnd-extraction
|
default_profile: dnd-extraction
|
||||||
inputs:
|
inputs:
|
||||||
@@ -24,13 +24,11 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -38,5 +36,5 @@ messages:
|
|||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: dnd_locations_llm.v1.json
|
schema_path: dnd_location_registry_llm.v1.json
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.locations.llm",
|
"$id": "notarius.dnd.location_registry.llm",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["locations"],
|
"required": ["locations"],
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Use candidate names and their cited transcript windows to determine whether
|
||||||
|
candidates identify the same physical place. Do not treat matching names,
|
||||||
|
nearby evidence, nested places, or generic labels as sufficient. Keep parent
|
||||||
|
and child places, similarly named places, and uncertain aliases separate.
|
||||||
|
|
||||||
|
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: ./instructions.md
|
||||||
|
- role: user
|
||||||
|
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||||
|
cache_control:
|
||||||
|
type: ephemeral
|
||||||
|
- role: user
|
||||||
|
content_file: ./candidates.md
|
||||||
|
- role: user
|
||||||
|
content_file: ./sharedassets/common-dnd-transcript-windows.md
|
||||||
|
cache_control:
|
||||||
|
type: ephemeral
|
||||||
|
output:
|
||||||
|
format: json
|
||||||
|
validation_mode: json_schema
|
||||||
|
schema_path: dnd_entity_reconcile_llm.v1.json
|
||||||
|
repair_attempts: 0
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Return only observed location display names and narrow transcript source ranges.
|
|
||||||
|
|
||||||
Exclude people, creatures, objects, organizations, abstract concepts, and
|
|
||||||
places merely inferred from an event. Omit uncertain or unsupported places.
|
|
||||||
Campaign references may clarify terms already present in the transcript, but
|
|
||||||
they are not evidence and must never supply a source range.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Extract physical places established by the provided Dungeons & Dragons
|
|
||||||
transcript and cite where each place is identified.
|
|
||||||
|
|
||||||
Include planes, regions, settlements, districts, buildings, rooms, landmarks,
|
|
||||||
routes, and geographic features. A generic label such as "the tavern" is
|
|
||||||
allowed only when the transcript uses it for a specific place. Keep aliases and
|
|
||||||
nested places when the transcript identifies them; do not merge or invent
|
|
||||||
qualifiers for similarly named places.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Review location candidates and cited transcript context. Group candidates only
|
|
||||||
when the evidence clearly identifies one physical place.
|
|
||||||
|
|
||||||
Do not group candidates solely because their names match, their evidence is
|
|
||||||
nearby, one place is nested inside another, or their labels are generic. Keep
|
|
||||||
parent and child places, similarly named places, and uncertain aliases
|
|
||||||
separate. For an accepted group, select the supplied candidate with the
|
|
||||||
clearest established display name as canonical.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
Extract Dungeons & Dragons NPC interaction occurrences from the supplied
|
|
||||||
transcript.
|
|
||||||
|
|
||||||
Include an occurrence only when the transcript establishes one supplied NPC,
|
|
||||||
one interaction kind, and a coherent passage supporting both. Use only names
|
|
||||||
from the supplied NPC registry. The registry helps ground identity but never
|
|
||||||
proves that an interaction occurred.
|
|
||||||
|
|
||||||
Do not summarize, infer relationships, sentiment, factions, motives, aliases,
|
|
||||||
or persistent state. Do not identify player characters, anonymous groups, or
|
|
||||||
invented NPCs. Return an empty interactions array when no supplied NPC has an
|
|
||||||
evidenced interaction in this transcript passage.
|
|
||||||
|
|
||||||
Keep occurrences within this transcript chunk. Split records when an NPC's
|
|
||||||
interaction kind changes, when combat alignment changes, or when an NPC is
|
|
||||||
first mentioned and later becomes present.
|
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
Return the interactions array even when no interaction is established. Every
|
Extract Dungeons & Dragons NPC occurrences from the supplied
|
||||||
record must contain name, kind, and source_refs. Cite transcript ranges that
|
transcript. Include an occurrence only when the transcript establishes one
|
||||||
support both the NPC identity and the interaction kind.
|
supplied NPC, one occurrence kind, and a coherent passage supporting both.
|
||||||
|
Use the exact `npc_id` and matching `name` pair from the supplied NPC registry;
|
||||||
|
never invent an ID or substitute a similar name.
|
||||||
|
|
||||||
|
Do not summarize, infer relationships, sentiment, factions, motives, aliases,
|
||||||
|
or persistent state. Do not identify player characters, anonymous groups, or
|
||||||
|
invented NPCs. Split records when an NPC's occurrence kind changes, when
|
||||||
|
combat alignment changes, or when an NPC is first mentioned and later becomes
|
||||||
|
present.
|
||||||
|
|
||||||
Use exactly one kind per occurrence:
|
Use exactly one kind per occurrence:
|
||||||
|
|
||||||
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,17 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.npc_interactions.llm",
|
"$id": "notarius.dnd.npc_occurrences.llm",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["interactions"],
|
"required": ["occurrences"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"interactions": {
|
"occurrences": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["name", "kind", "source_refs"],
|
"required": ["npc_id", "name", "kind", "source_refs"],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"npc_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
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"
|
version: "v1"
|
||||||
default_profile: dnd-extraction
|
default_profile: dnd-extraction
|
||||||
inputs:
|
inputs:
|
||||||
@@ -24,13 +24,11 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -38,5 +36,5 @@ messages:
|
|||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: dnd_item_events_llm.v1.json
|
schema_path: dnd_npc_registry_llm.v1.json
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.npcs.llm",
|
"$id": "notarius.dnd.npc_registry.llm",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["npcs"],
|
"required": ["npcs"],
|
||||||
3
assets/dnd/npc-registry/normalize/prompts/candidates.md
Normal file
3
assets/dnd/npc-registry/normalize/prompts/candidates.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
NPC candidates for identity comparison:
|
||||||
|
|
||||||
|
{{ input "candidates" }}
|
||||||
10
assets/dnd/npc-registry/normalize/prompts/instructions.md
Normal file
10
assets/dnd/npc-registry/normalize/prompts/instructions.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Use candidate aliases and their cited transcript windows to determine whether
|
||||||
|
candidates refer to the same individual. Preserve distinct individuals even
|
||||||
|
when their names are similar.
|
||||||
|
|
||||||
|
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"
|
version: "v1"
|
||||||
default_profile: dnd-extraction
|
default_profile: dnd-extraction
|
||||||
inputs:
|
inputs:
|
||||||
@@ -12,7 +12,7 @@ messages:
|
|||||||
- role: system
|
- role: system
|
||||||
content_file: ./sharedassets/common-dnd-system.md
|
content_file: ./sharedassets/common-dnd-system.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./task.md
|
content_file: ./instructions.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -20,7 +20,7 @@ messages:
|
|||||||
- role: user
|
- role: user
|
||||||
content_file: ./candidates.md
|
content_file: ./candidates.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-windows.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
output:
|
output:
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
For every NPC record, return only the observed display name and transcript
|
|
||||||
units that support that identity.
|
|
||||||
|
|
||||||
Return no other details or lore inferred from general D&D knowledge. Do not
|
|
||||||
invent a label for an anonymous creature, crowd, or generic role.
|
|
||||||
|
|
||||||
Preserve observed display spelling. Return at least one narrow source range for
|
|
||||||
every record.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
Extract the individually identifiable Dungeons & Dragons non-player characters
|
|
||||||
established by the provided transcript and cite where each identity appears.
|
|
||||||
|
|
||||||
Include an in-world non-PC participant only when the transcript gives it a
|
|
||||||
proper name or a stable, individually distinguishing title or alias.
|
|
||||||
|
|
||||||
Exclude human players, transcript speakers, and the GM as out-of-world people,
|
|
||||||
player characters identified by the player or party references, incidental or
|
|
||||||
hypothetical name drops, 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.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
The supplied NPC candidates are below. Use only these display names in the
|
|
||||||
response.
|
|
||||||
|
|
||||||
{{ input "candidates" }}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
Review NPC candidates and their cited transcript context to identify aliases
|
|
||||||
that refer to the same individual. Propose only groups supported by the
|
|
||||||
transcript, and preserve distinct individuals even when their names are
|
|
||||||
similar.
|
|
||||||
|
|
||||||
For every accepted group, choose as canonical only a supplied candidate from
|
|
||||||
that evidence-supported duplicate group. 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`. Do not
|
|
||||||
invent, edit, or combine display names.
|
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
Choose exactly one kind:
|
Describe exactly one accepted Dungeons & Dragons scene from the supplied
|
||||||
|
transcript chunk. The complete chunk is the evidence boundary: do not split it
|
||||||
|
into multiple scenes or use facts that are not supported by it.
|
||||||
|
|
||||||
|
Return one kind, one concise title, and one concise summary. Choose exactly one
|
||||||
|
kind:
|
||||||
|
|
||||||
- combat: active combat materially organizes the scene, including
|
- combat: active combat materially organizes the scene, including
|
||||||
initiative-like exchanges or sustained hostile action. Planning a fight or
|
initiative-like exchanges or sustained hostile action. Planning a fight or
|
||||||
@@ -37,8 +42,4 @@ must not invent a proper noun.
|
|||||||
The summary must briefly state the main activity and material transition or
|
The summary must briefly state the main activity and material transition or
|
||||||
outcome established within the accepted chunk. Do not add analysis, inferred
|
outcome established within the accepted chunk. Do not add analysis, inferred
|
||||||
motives, hidden state, future consequences, relationship claims, or facts from
|
motives, hidden state, future consequences, relationship claims, or facts from
|
||||||
outside the chunk. Campaign references may disambiguate names but never add
|
outside the chunk.
|
||||||
events or lore.
|
|
||||||
|
|
||||||
Do not return identifiers, source identifiers, source ranges, unit identifiers,
|
|
||||||
participants, confidence, or any fields besides kind, title, and summary.
|
|
||||||
|
|||||||
@@ -24,11 +24,9 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
Cover the complete provided transcript from its first source unit to its last
|
Divide the complete provided transcript into coherent Dungeons & Dragons scenes
|
||||||
source unit. Return scenes in source-unit order with no gaps or overlaps. Use
|
for the `dnd/scenes` chunk module.
|
||||||
only positive integer source-unit IDs from the transcript, and give every scene
|
|
||||||
one inclusive `start_unit_id` and one inclusive `end_unit_id`.
|
|
||||||
|
|
||||||
Return exactly one JSON object and no explanatory text. The object must contain
|
A scene is a coherent unit of play. Start a new scene when the transcript
|
||||||
only a non-empty `scenes` array. Each scene object must contain only
|
establishes a meaningful change in location, objective, threat, activity,
|
||||||
`start_unit_id` and `end_unit_id`.
|
encounter, or mode of play. Good reasons include a material move, beginning or
|
||||||
|
ending combat, a substantially different encounter phase, a shift between
|
||||||
|
combat, exploration, social interaction, planning, travel, rest, or downtime,
|
||||||
|
a change in the central NPC, faction, threat, or objective, or a sustained
|
||||||
|
table-level interruption that materially changes the activity.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
content_file: ./sharedassets/common-dnd-references.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-full.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: dnd_scenes.v1.json
|
schema_path: dnd_scenes_llm.v1.json
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
Divide the 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,
|
|
||||||
encounter, or mode of play. Good reasons include a material move, beginning or
|
|
||||||
ending combat, a substantially different encounter phase, a shift between
|
|
||||||
combat, exploration, social interaction, planning, travel, rest, or downtime,
|
|
||||||
a change in the central NPC, faction, threat, or objective, or a sustained
|
|
||||||
table-level interruption that materially changes the activity.
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.scenes",
|
"$id": "notarius.dnd.scenes.llm",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["scenes"],
|
"required": ["scenes"],
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
Transcript units are the only evidence for extracted events and factual claims.
|
Transcript units are the only evidence for extracted events and factual claims.
|
||||||
Campaign and registry references may disambiguate names, aliases, speakers, or
|
|
||||||
other identities, but they do not establish events, participation, effects, or
|
|
||||||
source evidence.
|
|
||||||
|
|
||||||
Every reported factual claim must be supported by cited transcript units. Use
|
Every reported factual claim must be supported by cited transcript units. Use
|
||||||
integer `start_unit_id` and `end_unit_id` values from the transcript. Omit
|
integer `start_unit_id` and `end_unit_id` values from the transcript. Omit
|
||||||
`source_id`; Notarius assigns the current source identity.
|
`source_id`; Notarius assigns the current source identity.
|
||||||
|
|
||||||
When supporting evidence is non-contiguous, use multiple narrow ranges rather
|
When supporting evidence is non-contiguous, use multiple narrow ranges rather
|
||||||
than a broad range that bridges unrelated conversation.
|
than a broad range that bridges unrelated conversation.
|
||||||
|
|
||||||
Return exactly one JSON object and no explanatory text. Output only the
|
|
||||||
configured JSON object and fields defined by its response schema.
|
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ participants, effects, or source references from the registry. Registry source
|
|||||||
references describe registry provenance and may belong to another session; they
|
references describe registry provenance and may belong to another session; they
|
||||||
are never evidence for the current transcript.
|
are never evidence for the current transcript.
|
||||||
|
|
||||||
{{ input "npcs" }}
|
{{ input "npc_registry" }}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
You work with Dungeons & Dragons gameplay transcripts.
|
You process Dungeons & Dragons gameplay transcripts.
|
||||||
|
|
||||||
Use only the provided transcript and reference material. Source text may contain
|
Rely only on the supplied inputs. They may contain transcription errors,
|
||||||
transcription errors, repeated lines, incomplete sentences, and misheard proper
|
repeated lines, incomplete sentences, and misheard proper nouns.
|
||||||
nouns. Reference material, when present, is supporting context only and must not
|
|
||||||
be treated as a source of extracted events by itself.
|
Return exactly one JSON object that conforms to the configured response schema,
|
||||||
|
with no explanatory prose.
|
||||||
|
|||||||
5
assets/dnd/shared/prompts/common-dnd-transcript-chunk.md
Normal file
5
assets/dnd/shared/prompts/common-dnd-transcript-chunk.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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" }}
|
||||||
4
assets/dnd/shared/prompts/common-dnd-transcript-full.md
Normal file
4
assets/dnd/shared/prompts/common-dnd-transcript-full.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
The complete ordered transcript of this Dungeons & Dragons gameplay session is
|
||||||
|
provided below. It may contain multiple scenes.
|
||||||
|
|
||||||
|
{{ input "transcript" }}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Selected Dungeons & Dragons gameplay transcript evidence windows are provided
|
||||||
|
below. They may be incomplete, non-contiguous, or overlapping. Use them to
|
||||||
|
evaluate candidate identity, but do not treat absence outside these windows as
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
{{ input "transcript" }}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
A transcript of a Dungeons & Dragons gameplay session is provided below.
|
|
||||||
|
|
||||||
{{ input "transcript" }}
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
For each spell cast, source references must collectively support the in-world
|
Extract Dungeons & Dragons spell-cast artifacts from the provided transcript.
|
||||||
caster, spell name, and the fact that the cast or declared attempt occurred.
|
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.
|
||||||
|
|
||||||
Return only D&D spell-cast artifacts. For each record, identify the in-world
|
For every extracted cast, the transcript evidence must collectively support the
|
||||||
caster, canonical spell name, and source references.
|
in-world caster, the spell, and the fact that the cast or declared attempt
|
||||||
|
occurred.
|
||||||
|
|
||||||
Use the player and party references together with transcript context to map
|
Attribute every cast to its in-world caster. Map first-person player speech to
|
||||||
first-person player speech to the associated player character and use the
|
the associated player character, and attribute a spell narrated by the GM to
|
||||||
canonical character name from the references. Likewise, attribute a spell
|
the in-world creature that casts it. If the caster cannot be resolved, use only
|
||||||
narrated by the GM to the in-world creature that casts it. If the caster cannot
|
the most specific in-world identity supported by the transcript; do not invent
|
||||||
be resolved, use only the most specific in-world identity supported by the
|
a name.
|
||||||
transcript; do not invent a name.
|
|
||||||
|
|
||||||
Use the canonical spell-name catalog to select spell names. Do not return a
|
|
||||||
spell name absent from that catalog, even when it is suggested by general D&D
|
|
||||||
knowledge or reference material.
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ inputs:
|
|||||||
- name: glossary
|
- name: glossary
|
||||||
required: false
|
required: false
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
- name: npcs
|
- name: npc_registry
|
||||||
required: false
|
required: false
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
@@ -30,17 +30,15 @@ messages:
|
|||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-transcript.md
|
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
content_file: ./sharedassets/common-dnd-extraction-evidence.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./sharedassets/common-dnd-npcs.md
|
content_file: ./sharedassets/common-dnd-npc-registry.md
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./catalog.md
|
content_file: ./spell-catalog.md
|
||||||
- role: user
|
|
||||||
content_file: ./task.md
|
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./instructions.md
|
content_file: ./instructions.md
|
||||||
cache_control:
|
cache_control:
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
||||||
Use the provided canonical spell-name catalog when naming each extracted spell.
|
|
||||||
Return the canonical catalog spelling exactly. The catalog is a recognition
|
|
||||||
aid and never evidence that a spell was cast.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.spells",
|
"$id": "notarius.dnd.spells.llm",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["spell_casts"],
|
"required": ["spell_casts"],
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ output:
|
|||||||
enabled: true
|
enabled: true
|
||||||
window_units: 3
|
window_units: 3
|
||||||
lanes:
|
lanes:
|
||||||
- npcs
|
- npc-registry
|
||||||
- spells
|
- spells
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
@@ -332,15 +332,15 @@ step:
|
|||||||
steps:
|
steps:
|
||||||
- id: describe-session
|
- id: describe-session
|
||||||
artifacts:
|
artifacts:
|
||||||
npcs:
|
npc-registry:
|
||||||
extract: dnd/npcs
|
extract: dnd/npc-registry
|
||||||
normalize: dnd/npcs
|
normalize: dnd/npc-registry
|
||||||
- id: extract-events
|
- id: extract-events
|
||||||
references:
|
references:
|
||||||
npcs:
|
npc_registry:
|
||||||
artifact:
|
artifact:
|
||||||
step: describe-session
|
step: describe-session
|
||||||
lane: npcs
|
lane: npc-registry
|
||||||
artifacts:
|
artifacts:
|
||||||
spells:
|
spells:
|
||||||
extract: dnd/spells
|
extract: dnd/spells
|
||||||
@@ -370,31 +370,36 @@ selected target declares them:
|
|||||||
| **players** | Optional text player context. |
|
| **players** | Optional text player context. |
|
||||||
| **glossary** | Optional text campaign glossary. |
|
| **glossary** | Optional text campaign glossary. |
|
||||||
| **spell_catalog** | Optional JSON spell-catalog overlay for spell extraction and normalization. See [spell-catalog overlays](integrations/dnd-spell-catalog-overlays.md). |
|
| **spell_catalog** | Optional JSON spell-catalog overlay for spell extraction and normalization. See [spell-catalog overlays](integrations/dnd-spell-catalog-overlays.md). |
|
||||||
| **locations** | Required normalized location registry for location-occurrence extraction and normalization. |
|
| **location_registry** | 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. |
|
| **item_registry** | Required normalized item registry for item-occurrence extraction and normalization. |
|
||||||
|
| **npc_registry** | Normalized NPC registry. Optional for spells and combat turns; required for NPC occurrences and enemy-event extraction and normalization. |
|
||||||
| **scene_descriptions** | Required normalized scene-description artifact for combat-turn and enemy-event extraction. |
|
| **scene_descriptions** | Required normalized scene-description artifact for combat-turn and enemy-event extraction. |
|
||||||
| **combat_turns** | Required normalized combat-turn artifact for enemy-event extraction. |
|
| **combat_turns** | Required normalized combat-turn artifact for enemy-event extraction. |
|
||||||
| **npc_interactions** | Required normalized NPC-interaction artifact for enemy-event extraction. |
|
| **npc_occurrences** | Required normalized NPC-occurrence artifact for enemy-event extraction. |
|
||||||
|
|
||||||
Location-occurrence and enemy-event artifact slots have the following exact
|
Registry-backed occurrence and enemy-event artifact slots have the following
|
||||||
binding contracts. Durable semantics and wire shapes remain in their
|
exact binding contracts. Durable semantics and wire shapes remain in their
|
||||||
[location-occurrence](integrations/dnd-location-occurrence-artifacts.md) and
|
[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.
|
[enemy-event](integrations/dnd-enemy-event-artifacts.md) contracts.
|
||||||
|
|
||||||
| Slot | Accepted artifact kind | Media type | Maximum size | Required stage |
|
| Slot | Accepted artifact kind | Media type | Maximum size | Required stage |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `npcs` | `dnd/npc-list` | `application/json` | 1,048,576 bytes | extract and normalize |
|
| `npc_registry` | `dnd/npc-registry` | `application/json` | 1,048,576 bytes | extract and normalize |
|
||||||
| `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only |
|
| `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only |
|
||||||
| `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only |
|
| `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only |
|
||||||
| `npc_interactions` | `dnd/npc-interaction-list` | `application/json` | 1,048,576 bytes | extract only |
|
| `npc_occurrences` | `dnd/npc-occurrence-list` | `application/json` | 1,048,576 bytes | extract only |
|
||||||
| `locations` | `dnd/location-list` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize |
|
| `location_registry` | `dnd/location-registry` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize |
|
||||||
|
| `item_registry` | `dnd/item-registry` | `application/json` | 1,048,576 bytes | item-occurrence extract and normalize |
|
||||||
|
|
||||||
Scene descriptions accept **party**, **players**, and **glossary**, but not
|
Scene descriptions accept **party**, **players**, and **glossary**, but not
|
||||||
**roster**. NPC interactions require **npcs** for both extraction and
|
**roster**. NPC occurrences require **npc_registry** for both extraction and
|
||||||
normalization. Combat turns require **scene_descriptions** for extraction; the
|
normalization. Combat turns require **scene_descriptions** for extraction; the
|
||||||
normalized combat-turn module may use optional **npcs**. Location occurrences
|
normalized combat-turn module may use optional **npc_registry**. Location occurrences
|
||||||
require **locations** for extraction and normalization. Enemy-event extraction
|
require **location_registry** for extraction and normalization. Item occurrences require
|
||||||
requires all four of its JSON artifact slots; its normalizer requires **npcs**.
|
**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
|
The [complete example](../examples/dnd-complete.config.yml) shows the ordered
|
||||||
generated bindings.
|
generated bindings.
|
||||||
|
|
||||||
@@ -404,26 +409,28 @@ generated bindings.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Input | **seriatim** |
|
| Input | **seriatim** |
|
||||||
| Chunk | **generic**, **dnd/scenes** |
|
| Chunk | **generic**, **dnd/scenes** |
|
||||||
| Extract | **dnd/spells**, **dnd/npcs**, **dnd/combat-turns**, **dnd/item-events**, **dnd/npc-interactions**, **dnd/scene-descriptions**, **dnd/enemy-events**, **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** |
|
| 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** |
|
| Output | **json** |
|
||||||
|
|
||||||
`dnd/locations` extraction and normalization are `llm_backed`; location
|
`dnd/scenes` and every D&D extractor are `llm_backed`. The
|
||||||
normalization may use the pipeline's selected LLM profile for bounded duplicate
|
`dnd/npc-registry`, `dnd/location-registry`, and `dnd/item-registry`
|
||||||
proposals. `dnd/location-occurrences` extraction is `llm_backed`, while its
|
normalizers are also `llm_backed` for bounded duplicate proposals; every other
|
||||||
normalizer is `deterministic`. The complete example binds the registry in one
|
D&D normalizer is `deterministic`. LLM-backed bindings use the effective
|
||||||
step and the occurrence lane in the next.
|
[PromptKit profile](#promptkit-profiles). The complete example binds each
|
||||||
|
registry in an earlier step before its occurrence consumer.
|
||||||
|
|
||||||
The D&D artifact contracts define each emitted schema:
|
The D&D artifact contracts define each emitted schema:
|
||||||
[spells](integrations/dnd-spell-artifacts.md),
|
[spells](integrations/dnd-spell-artifacts.md),
|
||||||
[NPCs](integrations/dnd-npc-artifacts.md),
|
[NPC registry](integrations/dnd-npc-registry-artifacts.md),
|
||||||
[NPC interactions](integrations/dnd-npc-interaction-artifacts.md),
|
[NPC occurrences](integrations/dnd-npc-occurrence-artifacts.md),
|
||||||
[combat turns](integrations/dnd-combat-turn-artifacts.md),
|
[combat turns](integrations/dnd-combat-turn-artifacts.md),
|
||||||
[item events](integrations/dnd-item-event-artifacts.md),
|
[item registry](integrations/dnd-item-registry-artifacts.md),
|
||||||
[scene descriptions](integrations/dnd-scene-description-artifacts.md), and
|
[item occurrences](integrations/dnd-item-occurrence-artifacts.md),
|
||||||
|
[scene descriptions](integrations/dnd-scene-description-artifacts.md),
|
||||||
[enemy events](integrations/dnd-enemy-event-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).
|
[location occurrences](integrations/dnd-location-occurrence-artifacts.md).
|
||||||
|
|
||||||
## Production Validator Keys And Default Chains
|
## Production Validator Keys And Default Chains
|
||||||
@@ -434,13 +441,14 @@ Available validator keys are:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Generic | **generic/always_accept**, **generic/always_reject**, **generic/valid_json**, **generic/valid_json_schema** |
|
| Generic | **generic/always_accept**, **generic/always_reject**, **generic/valid_json**, **generic/valid_json_schema** |
|
||||||
| Spells | **extract/dnd/spells/shape**, **extract/dnd/spells/catalog**, **extract/dnd/spells/source_refs**, **extract/dnd/spells/source_relatedness** |
|
| Spells | **extract/dnd/spells/shape**, **extract/dnd/spells/catalog**, **extract/dnd/spells/source_refs**, **extract/dnd/spells/source_relatedness** |
|
||||||
| NPCs | **extract/dnd/npcs/shape**, **extract/dnd/npcs/source_refs**, **extract/dnd/npcs/source_relatedness**, **normalize/dnd/npcs/identity** |
|
| NPC registry | **extract/dnd/npc-registry/shape**, **extract/dnd/npc-registry/source_refs**, **extract/dnd/npc-registry/source_relatedness**, **normalize/dnd/npc-registry/identity** |
|
||||||
| Combat turns | **extract/dnd/combat-turns/shape**, **extract/dnd/combat-turns/source_refs**, **extract/dnd/combat-turns/source_relatedness**, **normalize/dnd/combat-turns/invariants** |
|
| Combat turns | **extract/dnd/combat-turns/shape**, **extract/dnd/combat-turns/source_refs**, **extract/dnd/combat-turns/source_relatedness**, **normalize/dnd/combat-turns/invariants** |
|
||||||
| Item events | **extract/dnd/item-events/shape**, **extract/dnd/item-events/source_refs**, **extract/dnd/item-events/source_relatedness**, **normalize/dnd/item-events/invariants** |
|
| Item occurrences | **extract/dnd/item-occurrences/shape**, **extract/dnd/item-occurrences/registry**, **extract/dnd/item-occurrences/source_refs**, **extract/dnd/item-occurrences/source_relatedness**, **normalize/dnd/item-occurrences/invariants** |
|
||||||
| NPC interactions | **extract/dnd/npc-interactions/shape**, **extract/dnd/npc-interactions/registry**, **extract/dnd/npc-interactions/source_refs**, **extract/dnd/npc-interactions/source_relatedness**, **normalize/dnd/npc-interactions/invariants** |
|
| Item registry | **extract/dnd/item-registry/shape**, **extract/dnd/item-registry/source_refs**, **extract/dnd/item-registry/source_relatedness**, **normalize/dnd/item-registry/identity** |
|
||||||
|
| NPC occurrences | **extract/dnd/npc-occurrences/shape**, **extract/dnd/npc-occurrences/registry**, **extract/dnd/npc-occurrences/source_refs**, **extract/dnd/npc-occurrences/source_relatedness**, **normalize/dnd/npc-occurrences/invariants** |
|
||||||
| Scene descriptions | **extract/dnd/scene-descriptions/shape**, **extract/dnd/scene-descriptions/source_refs**, **extract/dnd/scene-descriptions/source_relatedness**, **normalize/dnd/scene-descriptions/invariants** |
|
| Scene descriptions | **extract/dnd/scene-descriptions/shape**, **extract/dnd/scene-descriptions/source_refs**, **extract/dnd/scene-descriptions/source_relatedness**, **normalize/dnd/scene-descriptions/invariants** |
|
||||||
| Enemy events | **extract/dnd/enemy-events/shape**, **extract/dnd/enemy-events/engagements**, **extract/dnd/enemy-events/source_refs**, **extract/dnd/enemy-events/source_relatedness**, **normalize/dnd/enemy-events/invariants** |
|
| Enemy events | **extract/dnd/enemy-events/shape**, **extract/dnd/enemy-events/engagements**, **extract/dnd/enemy-events/source_refs**, **extract/dnd/enemy-events/source_relatedness**, **normalize/dnd/enemy-events/invariants** |
|
||||||
| 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** |
|
| Location occurrences | **extract/dnd/location-occurrences/shape**, **extract/dnd/location-occurrences/registry**, **extract/dnd/location-occurrences/source_refs**, **extract/dnd/location-occurrences/source_relatedness**, **normalize/dnd/location-occurrences/invariants** |
|
||||||
|
|
||||||
When no override is configured, production D&D bindings use the following
|
When no override is configured, production D&D bindings use the following
|
||||||
@@ -450,13 +458,14 @@ same at both stages.
|
|||||||
| Lane | Extract | Normalize |
|
| Lane | Extract | Normalize |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Spells | generic/valid_json, extract/dnd/spells/shape, extract/dnd/spells/catalog, extract/dnd/spells/source_refs, generic/valid_json_schema, extract/dnd/spells/source_relatedness | Same as extract |
|
| Spells | generic/valid_json, extract/dnd/spells/shape, extract/dnd/spells/catalog, extract/dnd/spells/source_refs, generic/valid_json_schema, extract/dnd/spells/source_relatedness | Same as extract |
|
||||||
| NPCs | generic/valid_json, extract/dnd/npcs/shape, extract/dnd/npcs/source_refs, generic/valid_json_schema, extract/dnd/npcs/source_relatedness | generic/valid_json, extract/dnd/npcs/shape, normalize/dnd/npcs/identity, extract/dnd/npcs/source_refs, generic/valid_json_schema, extract/dnd/npcs/source_relatedness |
|
| NPC registry | generic/valid_json, extract/dnd/npc-registry/shape, extract/dnd/npc-registry/source_refs, generic/valid_json_schema, extract/dnd/npc-registry/source_relatedness | generic/valid_json, extract/dnd/npc-registry/shape, normalize/dnd/npc-registry/identity, extract/dnd/npc-registry/source_refs, generic/valid_json_schema, extract/dnd/npc-registry/source_relatedness |
|
||||||
| Combat turns | generic/valid_json, extract/dnd/combat-turns/shape, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | generic/valid_json, extract/dnd/combat-turns/shape, normalize/dnd/combat-turns/invariants, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness |
|
| Combat turns | generic/valid_json, extract/dnd/combat-turns/shape, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | generic/valid_json, extract/dnd/combat-turns/shape, normalize/dnd/combat-turns/invariants, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness |
|
||||||
| Item events | generic/valid_json, extract/dnd/item-events/shape, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | generic/valid_json, extract/dnd/item-events/shape, normalize/dnd/item-events/invariants, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness |
|
| Item occurrences | generic/valid_json, extract/dnd/item-occurrences/shape, extract/dnd/item-occurrences/registry, extract/dnd/item-occurrences/source_refs, generic/valid_json_schema, extract/dnd/item-occurrences/source_relatedness | generic/valid_json, extract/dnd/item-occurrences/shape, extract/dnd/item-occurrences/registry, normalize/dnd/item-occurrences/invariants, extract/dnd/item-occurrences/source_refs, generic/valid_json_schema, extract/dnd/item-occurrences/source_relatedness |
|
||||||
| NPC interactions | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, normalize/dnd/npc-interactions/invariants, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness |
|
| Item registry | generic/valid_json, extract/dnd/item-registry/shape, extract/dnd/item-registry/source_refs, generic/valid_json_schema, extract/dnd/item-registry/source_relatedness | generic/valid_json, extract/dnd/item-registry/shape, normalize/dnd/item-registry/identity, extract/dnd/item-registry/source_refs, generic/valid_json_schema, extract/dnd/item-registry/source_relatedness |
|
||||||
|
| NPC occurrences | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, normalize/dnd/npc-occurrences/invariants, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness |
|
||||||
| Scene descriptions | generic/valid_json, extract/dnd/scene-descriptions/shape, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | generic/valid_json, extract/dnd/scene-descriptions/shape, normalize/dnd/scene-descriptions/invariants, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness |
|
| Scene descriptions | generic/valid_json, extract/dnd/scene-descriptions/shape, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | generic/valid_json, extract/dnd/scene-descriptions/shape, normalize/dnd/scene-descriptions/invariants, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness |
|
||||||
| Enemy events | generic/valid_json, extract/dnd/enemy-events/shape, extract/dnd/enemy-events/engagements, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness | generic/valid_json, extract/dnd/enemy-events/shape, normalize/dnd/enemy-events/invariants, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness |
|
| Enemy events | generic/valid_json, extract/dnd/enemy-events/shape, extract/dnd/enemy-events/engagements, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness | generic/valid_json, extract/dnd/enemy-events/shape, normalize/dnd/enemy-events/invariants, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness |
|
||||||
| 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 |
|
| Location occurrences | generic/valid_json, extract/dnd/location-occurrences/shape, extract/dnd/location-occurrences/registry, extract/dnd/location-occurrences/source_refs, generic/valid_json_schema, extract/dnd/location-occurrences/source_relatedness | generic/valid_json, extract/dnd/location-occurrences/shape, extract/dnd/location-occurrences/registry, normalize/dnd/location-occurrences/invariants, extract/dnd/location-occurrences/source_refs, generic/valid_json_schema, extract/dnd/location-occurrences/source_relatedness |
|
||||||
|
|
||||||
Chains are only registered for the D&D extract and normalize modules shown
|
Chains are only registered for the D&D extract and normalize modules shown
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ record controls eligibility only: its title, summary, and reference do not
|
|||||||
become turn evidence. No exact matching scene also produces an empty list and
|
become turn evidence. No exact matching scene also produces an empty list and
|
||||||
the `scene_classification_unavailable` warning.
|
the `scene_classification_unavailable` warning.
|
||||||
|
|
||||||
An optional normalized [NPC artifact](dnd-npc-artifacts.md) can ground an
|
An optional normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) can ground an
|
||||||
actor name. Its registry references are provenance, never combat evidence.
|
actor name. Its registry references are provenance, never combat evidence.
|
||||||
Normalization trims and, where possible, canonicalizes actor names; orders and
|
Normalization trims and, where possible, canonicalizes actor names; orders and
|
||||||
deduplicates exact source references; orders valid-evidence turns by source
|
deduplicates exact source references; orders valid-evidence turns by source
|
||||||
@@ -63,7 +63,7 @@ chronology; and collapses only duplicates with the same actor identity, turn
|
|||||||
kind, and complete valid evidence. It does not infer turns, initiative, or
|
kind, and complete valid evidence. It does not infer turns, initiative, or
|
||||||
actions from registry or scene data.
|
actions from registry or scene data.
|
||||||
|
|
||||||
The [NPC-interaction artifact](dnd-npc-interaction-artifacts.md) records
|
The [NPC-occurrence artifact](dnd-npc-occurrence-artifacts.md) records
|
||||||
broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md)
|
broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md)
|
||||||
uses combat turns as grounding only; turns do not establish an enemy event or
|
uses combat turns as grounding only; turns do not establish an enemy event or
|
||||||
its outcome. The [JSON output contract](json-output.md) defines publication,
|
its outcome. The [JSON output contract](json-output.md) defines publication,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ are positive and the start may not follow the end.
|
|||||||
| `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. A missed turn is insufficient. |
|
| `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. A missed turn is insufficient. |
|
||||||
|
|
||||||
The current transcript is the only event evidence. Campaign context and
|
The current transcript is the only event evidence. Campaign context and
|
||||||
normalized NPC, scene-description, combat-turn, and NPC-interaction artifacts
|
normalized NPC, scene-description, combat-turn, and NPC-occurrence artifacts
|
||||||
can ground names or control combat eligibility, but none may supply event
|
can ground names or control combat eligibility, but none may supply event
|
||||||
evidence. An outcome may share evidence with an engagement, in which case both
|
evidence. An outcome may share evidence with an engagement, in which case both
|
||||||
events are retained.
|
events are retained.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
71
docs/integrations/dnd-item-occurrence-artifacts.md
Normal file
71
docs/integrations/dnd-item-occurrence-artifacts.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# 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 only its ordered `{id,name}` projection after
|
||||||
|
the shared evidence message. It is never occurrence evidence.
|
||||||
|
|
||||||
|
Each occurrence must use one exact registry ID/name pair. An extraction response
|
||||||
|
with an unknown ID or mismatched 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.
|
||||||
94
docs/integrations/dnd-item-registry-artifacts.md
Normal file
94
docs/integrations/dnd-item-registry-artifacts.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 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 only an ordered, source-free `{id,name}` projection; the
|
||||||
|
registry’s source references are never occurrence evidence. Unknown IDs and
|
||||||
|
mismatched pairs 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 |
|
| 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. |
|
| `name` | Exact canonical display name for `location_id` in that registry. |
|
||||||
| `kind` | One of `visited`, `planned`, `recalled`, or `mentioned`. |
|
| `kind` | One of `visited`, `planned`, `recalled`, or `mentioned`. |
|
||||||
| `source_refs` | One or more current-transcript evidence ranges for this occurrence. |
|
| `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": [
|
"occurrences": [
|
||||||
{
|
{
|
||||||
"location_id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa",
|
"location_id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
|
||||||
"name": "Moon Gate",
|
"name": "Moon Gate",
|
||||||
"kind": "visited",
|
"kind": "visited",
|
||||||
"source_refs": [
|
"source_refs": [
|
||||||
@@ -71,8 +71,8 @@ complete canonical evidence sequence.
|
|||||||
|
|
||||||
## Required grounding and evidence
|
## Required grounding and evidence
|
||||||
|
|
||||||
Both extraction and normalization require exactly one `locations` reference of
|
Both extraction and normalization require exactly one `location_registry` reference of
|
||||||
kind `dnd/location-list`, media type `application/json`, and at most 1 MiB. The
|
kind `dnd/location-registry`, media type `application/json`, and at most 1 MiB. The
|
||||||
registry provides identity grounding only: unknown IDs and mismatched ID/name
|
registry provides identity grounding only: unknown IDs and mismatched ID/name
|
||||||
pairs are rejected rather than guessed or reassigned. The current transcript is
|
pairs are rejected rather than guessed or reassigned. The current transcript is
|
||||||
the only evidence source for an occurrence; registry evidence and provenance
|
the only evidence source for an occurrence; registry evidence and provenance
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# D&D Location Artifact
|
# D&D Location Registry Artifact
|
||||||
|
|
||||||
This contract defines the durable, source-grounded location registry produced
|
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
|
source document; it is not a map, location hierarchy, campaign-wide world
|
||||||
registry, or location description.
|
registry, or location description.
|
||||||
|
|
||||||
@@ -9,12 +9,12 @@ registry, or location description.
|
|||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Artifact kind | `dnd/location-list` |
|
| Artifact kind | `dnd/location-registry` |
|
||||||
| Schema ID | `notarius.dnd.locations` |
|
| Schema ID | `notarius.dnd.location_registry` |
|
||||||
| Schema name | `notarius_dnd_locations_v1` |
|
| Schema name | `notarius_dnd_location_registry_v1` |
|
||||||
| Schema version | `v1` |
|
| Schema version | `v1` |
|
||||||
| Media type | `application/json` |
|
| 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
|
`v1` accepts one strict JSON object with required `locations`; the array may be
|
||||||
empty. Location and source-reference objects reject unknown fields. An
|
empty. Location and source-reference objects reject unknown fields. An
|
||||||
@@ -39,7 +39,7 @@ identifiers, and the start may not follow the end.
|
|||||||
{
|
{
|
||||||
"locations": [
|
"locations": [
|
||||||
{
|
{
|
||||||
"id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa",
|
"id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
|
||||||
"name": "Moon Gate",
|
"name": "Moon Gate",
|
||||||
"source_refs": [
|
"source_refs": [
|
||||||
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
|
{"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:
|
for this array, using the earliest canonical source reference as the anchor:
|
||||||
|
|
||||||
```text
|
```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
|
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
|
## Scope, reconciliation, and evidence
|
||||||
|
|
||||||
Locations are physical or spatial places established by the transcript, such
|
Locations are physical or spatial places established by the transcript with a
|
||||||
as planes, regions, settlements, districts, buildings, rooms, landmarks,
|
stable proper name or unique in-world designation, such as named planes,
|
||||||
routes, and geographic features. A generic label is permitted only when it
|
regions, settlements, districts, buildings, rooms, landmarks, routes, and
|
||||||
identifies a specific place in the transcript. Notarius does not infer an
|
geographic features. Generic, temporary, relative, and descriptive phrases
|
||||||
unstated place or add hierarchy, coordinates, descriptions, participants, or
|
such as “the room,” “the bar,” “the hallway,” “outside,” and “upstairs” are not
|
||||||
ownership.
|
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
|
Normalization first applies deterministic display, evidence, and ID rules. It
|
||||||
then may use a bounded LLM-assisted proposal to reconcile semantically duplicate
|
then may use a bounded LLM-assisted proposal to reconcile semantically duplicate
|
||||||
@@ -81,7 +83,7 @@ not evidence for later artifacts.
|
|||||||
## Consumers and publication
|
## Consumers and publication
|
||||||
|
|
||||||
`dnd/location-occurrences` requires one approved location registry through its
|
`dnd/location-occurrences` requires one approved location registry through its
|
||||||
`locations` reference slot. Its prompt receives an ordered source-free `{id,
|
`location_registry` reference slot. Its prompt receives an ordered source-free `{id,
|
||||||
name}` projection and must not treat registry references as occurrence
|
name}` projection and must not treat registry references as occurrence
|
||||||
evidence. See the [location-occurrence artifact](dnd-location-occurrence-artifacts.md)
|
evidence. See the [location-occurrence artifact](dnd-location-occurrence-artifacts.md)
|
||||||
for that contract, [Configuration](../config.md#references-and-ordered-handoffs)
|
for that contract, [Configuration](../config.md#references-and-ordered-handoffs)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# D&D NPC Interaction Artifact
|
# D&D NPC Occurrence Artifact
|
||||||
|
|
||||||
This contract defines the durable occurrence list produced by
|
This contract defines the durable occurrence list produced by
|
||||||
`dnd/npc-interactions`. It records discrete, source-grounded interactions with
|
`dnd/npc-occurrences`. It records discrete, source-grounded occurrences with
|
||||||
NPCs already present in a normalized registry; it does not extend that registry
|
NPCs already present in a normalized registry; it does not extend that registry
|
||||||
or summarize the session.
|
or summarize the session.
|
||||||
|
|
||||||
@@ -9,35 +9,37 @@ or summarize the session.
|
|||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Artifact kind | `dnd/npc-interaction-list` |
|
| Artifact kind | `dnd/npc-occurrence-list` |
|
||||||
| Schema ID | `notarius.dnd.npc_interactions` |
|
| Schema ID | `notarius.dnd.npc_occurrences` |
|
||||||
| Schema name | `notarius_dnd_npc_interactions_v1` |
|
| Schema name | `notarius_dnd_npc_occurrences_v1` |
|
||||||
| Schema version | `v1` |
|
| Schema version | `v1` |
|
||||||
| Media type | `application/json` |
|
| Media type | `application/json` |
|
||||||
|
|
||||||
`v1` is a strict JSON object with required `interactions`; the array may be
|
`v1` is a strict JSON object with required `occurrences`; the array may be
|
||||||
empty. Interaction and source-reference objects reject unknown fields. An
|
empty. Occurrence and source-reference objects reject unknown fields. An
|
||||||
incompatible shape change requires a new schema version.
|
incompatible shape change requires a new schema version.
|
||||||
|
|
||||||
## Wire shape
|
## Wire shape
|
||||||
|
|
||||||
Each interaction has these required fields:
|
Each occurrence has these required fields:
|
||||||
|
|
||||||
| Field | Contract |
|
| Field | Contract |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `npc_id` | Exact durable ID from the required NPC registry. |
|
||||||
| `name` | Non-empty canonical display name from the required NPC registry. |
|
| `name` | Non-empty canonical display name from the required NPC registry. |
|
||||||
| `kind` | One of the interaction categories below. |
|
| `kind` | One of the occurrence categories below. |
|
||||||
| `source_refs` | One or more transcript evidence ranges. |
|
| `source_refs` | One or more transcript evidence ranges. |
|
||||||
|
|
||||||
Each source reference has exactly `source_id`, `start_unit_id`, and
|
Each source reference has exactly `source_id`, `start_unit_id`, and
|
||||||
`end_unit_id`. It identifies an inclusive range in the current transcript;
|
`end_unit_id`. It identifies an inclusive range in the current transcript;
|
||||||
unit IDs are positive and the start may not follow the end. Extraction evidence
|
unit IDs are positive and the start may not follow the end. Extraction evidence
|
||||||
for an interaction is confined to its accepted chunk.
|
for an occurrence is confined to its accepted chunk.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"interactions": [
|
"occurrences": [
|
||||||
{
|
{
|
||||||
|
"npc_id": "npc:sha256:example",
|
||||||
"name": "Mira Thorn",
|
"name": "Mira Thorn",
|
||||||
"kind": "dialogue",
|
"kind": "dialogue",
|
||||||
"source_refs": [
|
"source_refs": [
|
||||||
@@ -48,7 +50,7 @@ for an interaction is confined to its accepted chunk.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Interaction categories
|
## Occurrence categories
|
||||||
|
|
||||||
| Kind | Meaning |
|
| Kind | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -65,17 +67,22 @@ for uncertain classification.
|
|||||||
|
|
||||||
## Identity, evidence, and order
|
## Identity, evidence, and order
|
||||||
|
|
||||||
The required normalized [NPC artifact](dnd-npc-artifacts.md) resolves `name`.
|
The required normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) resolves
|
||||||
Registry references are provenance only and never replace an interaction's own
|
the exact `{npc_id, name}` pair. Unknown IDs and names that do not match their
|
||||||
evidence. Normalization canonicalizes recognized registry names, orders and
|
ID are rejected; normalization does not repair names by similarity. Registry
|
||||||
deduplicates exact source references, then orders interactions by valid source
|
references are provenance only and never replace an occurrence's own evidence.
|
||||||
|
The registry may include an identity established by a factual third-party
|
||||||
|
mention; that provenance alone does not create a `mentioned` occurrence. Each
|
||||||
|
occurrence remains a separately cited fact in the current transcript.
|
||||||
|
Normalization validates the exact pair, orders and
|
||||||
|
deduplicates exact source references, then orders occurrences by valid source
|
||||||
chronology, NPC comparison identity, display name, kind, and reference sequence.
|
chronology, NPC comparison identity, display name, kind, and reference sequence.
|
||||||
Only entries with the same canonical name, kind, and complete valid evidence
|
Only entries with the same NPC ID, canonical name, kind, and complete valid evidence
|
||||||
sequence are collapsed; distinct categories or evidence remain separate.
|
sequence are collapsed; distinct categories or evidence remain separate.
|
||||||
|
|
||||||
See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action
|
See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action
|
||||||
occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes
|
occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes
|
||||||
only `combat_opponent` interactions as grounding; they never establish an enemy
|
only `combat_opponent` occurrences as grounding; they never establish an enemy
|
||||||
event or outcome. The [JSON output contract](json-output.md) defines
|
event or outcome. The [JSON output contract](json-output.md) defines
|
||||||
publication. Pipeline mechanics are described in
|
publication. Pipeline mechanics are described in
|
||||||
[D&D module internals](../internal/dnd.md).
|
[D&D module internals](../internal/dnd.md).
|
||||||
@@ -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
|
minimal, source-grounded identity registry for other D&D artifacts, not a
|
||||||
character sheet or a relationship summary.
|
character sheet or a relationship summary.
|
||||||
|
|
||||||
@@ -8,12 +8,12 @@ character sheet or a relationship summary.
|
|||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Artifact kind | `dnd/npc-list` |
|
| Artifact kind | `dnd/npc-registry` |
|
||||||
| Schema ID | `notarius.dnd.npcs` |
|
| Schema ID | `notarius.dnd.npc_registry` |
|
||||||
| Schema name | `notarius_dnd_npcs_v1` |
|
| Schema name | `notarius_dnd_npc_registry_v1` |
|
||||||
| Schema version | `v1` |
|
| Schema version | `v1` |
|
||||||
| Media type | `application/json` |
|
| 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
|
`v1` accepts one strict JSON object with required `npcs`; the array may be
|
||||||
empty. NPC and source-reference objects reject unknown fields. An incompatible
|
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": [
|
"npcs": [
|
||||||
{
|
{
|
||||||
"id": "npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7",
|
"id": "npc:sha256:35ba5f679aee69e07ae3bd65c44278f29539d5dc9bb5225db1c0060555b23221",
|
||||||
"name": "Mira Thorn",
|
"name": "Mira Thorn",
|
||||||
"source_refs": [
|
"source_refs": [
|
||||||
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
|
{"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
|
The ID is deterministic: normalize the name to Unicode NFKC, normalize the
|
||||||
supported apostrophe forms, collapse whitespace, case-fold it, SHA-256 the
|
supported apostrophe forms, collapse whitespace, case-fold it, then serialize
|
||||||
result, then prefix the lowercase hexadecimal digest with `npc:sha256:`. Each
|
`["dnd.npc_registry.identity.v1", comparison_name]` as compact JSON. SHA-256
|
||||||
canonical identity and ID appears at most once. Normalization collapses records
|
those UTF-8 bytes and prefix the lowercase hexadecimal digest with
|
||||||
with the same canonical identity, retains their earliest position, and merges
|
`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
|
their canonicalized evidence; it does not add aliases, roles, descriptions, or
|
||||||
relationship fields.
|
relationship fields.
|
||||||
|
|
||||||
@@ -66,15 +68,24 @@ descriptor.
|
|||||||
## Scope and consumers
|
## Scope and consumers
|
||||||
|
|
||||||
Only individually identifiable NPC names with transcript evidence belong in
|
Only individually identifiable NPC names with transcript evidence belong in
|
||||||
this artifact. Groups, generic roles, invented labels, and descriptive
|
this artifact. A factual third-party mention can establish an identity even if
|
||||||
enrichment are excluded. Its source references prove registry provenance; they
|
the NPC is not present, speaking, or acting in the cited passage. Names used
|
||||||
do not become evidence for a spell, interaction, combat, or enemy-event
|
only in hypothetical, speculative, or imagined examples are excluded, as are
|
||||||
occurrence.
|
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)
|
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
|
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).
|
resolve the canonical `name` in an [NPC occurrence](dnd-npc-occurrence-artifacts.md).
|
||||||
The [enemy-event artifact](dnd-enemy-event-artifacts.md) also uses it only for
|
Occurrence consumers receive an ordered source-free `{id,name}` projection;
|
||||||
subject grounding and canonical display names.
|
spells, combat turns, and the [enemy-event artifact](dnd-enemy-event-artifacts.md)
|
||||||
|
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
|
The [JSON output contract](json-output.md) defines publication, and
|
||||||
[D&D module internals](../internal/dnd.md) owns pipeline mechanics.
|
[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
|
whitespace-insensitive caster identity, and the same complete valid reference
|
||||||
sequence. Remaining entries retain their merged order.
|
sequence. Remaining entries retain their merged order.
|
||||||
|
|
||||||
The optional normalized [NPC artifact](dnd-npc-artifacts.md) can ground a
|
The optional normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) can ground a
|
||||||
caster name. Its own references remain registry provenance and are never copied
|
caster name. Its own references remain registry provenance and are never copied
|
||||||
into `source_refs`.
|
into `source_refs`.
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ evidence publishes `contexts: []`.
|
|||||||
"source_id": "session-alpha",
|
"source_id": "session-alpha",
|
||||||
"source_digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
"source_digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||||
"window_units": 1,
|
"window_units": 1,
|
||||||
"selected_lanes": ["npcs", "spells"],
|
"selected_lanes": ["npc_registry", "spells"],
|
||||||
"contexts": [
|
"contexts": [
|
||||||
{
|
{
|
||||||
"context_ref": {
|
"context_ref": {
|
||||||
|
|||||||
@@ -71,13 +71,14 @@ output encoding fail.
|
|||||||
Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for
|
Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for
|
||||||
that lane. Consumers should use the index descriptor’s schema identity rather
|
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
|
than infer a lane schema from its name. The current D&D payload contracts are
|
||||||
[spells](dnd-spell-artifacts.md), [NPCs](dnd-npc-artifacts.md),
|
[spells](dnd-spell-artifacts.md), [NPC registry](dnd-npc-registry-artifacts.md),
|
||||||
[NPC interactions](dnd-npc-interaction-artifacts.md),
|
[NPC occurrences](dnd-npc-occurrence-artifacts.md),
|
||||||
[combat turns](dnd-combat-turn-artifacts.md),
|
[combat turns](dnd-combat-turn-artifacts.md),
|
||||||
[item events](dnd-item-event-artifacts.md),
|
[item registry](dnd-item-registry-artifacts.md),
|
||||||
[scene descriptions](dnd-scene-description-artifacts.md), and
|
[item occurrences](dnd-item-occurrence-artifacts.md),
|
||||||
|
[scene descriptions](dnd-scene-description-artifacts.md),
|
||||||
[enemy events](dnd-enemy-event-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).
|
[location occurrences](dnd-location-occurrence-artifacts.md).
|
||||||
|
|
||||||
## `manifest.json`
|
## `manifest.json`
|
||||||
|
|||||||
@@ -7,19 +7,20 @@ selectable keys, bindings, reference syntax, and default validator chains.
|
|||||||
|
|
||||||
## Durable Artifact Contracts
|
## 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.
|
does not repeat their JSON shapes or schemas.
|
||||||
|
|
||||||
| Lane | Durable contract |
|
| Lane | Durable contract |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Spells | [spell artifacts](../integrations/dnd-spell-artifacts.md) |
|
| Spells | [spell artifacts](../integrations/dnd-spell-artifacts.md) |
|
||||||
| NPCs | [NPC artifacts](../integrations/dnd-npc-artifacts.md) |
|
| NPC registry | [NPC registry artifacts](../integrations/dnd-npc-registry-artifacts.md) |
|
||||||
| Combat turns | [combat-turn artifacts](../integrations/dnd-combat-turn-artifacts.md) |
|
| Combat turns | [combat-turn artifacts](../integrations/dnd-combat-turn-artifacts.md) |
|
||||||
| Item events | [item-event artifacts](../integrations/dnd-item-event-artifacts.md) |
|
| Item occurrences | [item-occurrence artifacts](../integrations/dnd-item-occurrence-artifacts.md) |
|
||||||
| NPC interactions | [NPC-interaction artifacts](../integrations/dnd-npc-interaction-artifacts.md) |
|
| Item registry | [item-registry artifacts](../integrations/dnd-item-registry-artifacts.md) |
|
||||||
|
| NPC occurrences | [NPC-occurrence artifacts](../integrations/dnd-npc-occurrence-artifacts.md) |
|
||||||
| Scene descriptions | [scene-description artifacts](../integrations/dnd-scene-description-artifacts.md) |
|
| Scene descriptions | [scene-description artifacts](../integrations/dnd-scene-description-artifacts.md) |
|
||||||
| Enemy events | [enemy-event artifacts](../integrations/dnd-enemy-event-artifacts.md) |
|
| Enemy events | [enemy-event artifacts](../integrations/dnd-enemy-event-artifacts.md) |
|
||||||
| 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) |
|
| Location occurrences | [location-occurrence artifacts](../integrations/dnd-location-occurrence-artifacts.md) |
|
||||||
|
|
||||||
## Family Composition
|
## 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
|
append-order mergers, normalizers, validators, prompt assets, fallback LLM
|
||||||
profile asset, and default validator chains. Each extractor and normalizer has
|
profile asset, and default validator chains. Each extractor and normalizer has
|
||||||
a stable module spec, explicit execution class, strict option decoding, and a
|
a stable module spec, explicit execution class, strict option decoding, and a
|
||||||
typed builder. Scene chunking, every extractor, 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
|
normalization are registered as `llm_backed`; the remaining current D&D mergers
|
||||||
and normalizers are `deterministic`. The metadata is available to catalog inspection and
|
and normalizers are `deterministic`. The metadata is available to catalog inspection and
|
||||||
resolved-pipeline debug data and determines which selected bindings inherit the
|
resolved-pipeline debug data and determines which selected bindings inherit the
|
||||||
@@ -43,59 +44,66 @@ the contracts above define durable data.
|
|||||||
|
|
||||||
## Prompt Construction
|
## Prompt Construction
|
||||||
|
|
||||||
D&D LLM-facing content lives beneath `assets/dnd/`. New extractor content uses
|
D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a
|
||||||
its feature subtree; when a family has both extraction and normalization
|
local `prompt.yaml` declaration and `instructions.md`; input-specific files
|
||||||
content, keep those in its `extract` and `normalize` subtrees. The owning module
|
such as a catalog, registry, grounding projection, or candidate collection are
|
||||||
still defines the ordered manifest and registers the resulting scoped filesystem.
|
local only when that module needs them. New extractor content uses its feature
|
||||||
Shared fragments belong to the D&D shared implementation and are selected by
|
subtree, while families with both extraction and normalization content use their
|
||||||
name, never copied into individual module subtrees.
|
`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.
|
||||||
|
|
||||||
D&D extractors assemble prompts from an ordered manifest of shared and
|
The owning module’s manifest is the source of truth for which local and shared
|
||||||
module-selected assets. The location extractor and occurrence extractor reuse
|
assets are selected, their mount paths, their message order, cache controls,
|
||||||
the shared D&D system, evidence, identity, reference, and transcript assets
|
and the files included in its prompt fingerprint. Shared fragments belong to
|
||||||
instead of copying their text into individual modules. A manifest’s declared
|
the D&D shared implementation and are selected by name rather than copied into
|
||||||
sequence, including cache-control placement, is part of the prompt behavior.
|
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
|
Every maintained D&D LLM prompt selects `dnd-extraction` as its default
|
||||||
profile. The D&D registrar registers that fallback profile with the maintained
|
profile. The D&D registrar registers the fallback, while an operator can
|
||||||
OpenRouter model, timeout, and service-tier policy. An operator may provide a
|
replace it with a complete profile of the same ID from the configured PromptKit
|
||||||
complete profile with the same ID through the configured PromptKit source; that
|
source. Deployment profile selection is documented in
|
||||||
definition replaces the fallback rather than merging with it. The fallback
|
|
||||||
leaves reasoning and optional sampling controls unspecified. Deployment profile
|
|
||||||
selection and the maintained operator example are documented in
|
|
||||||
[Configuration](../config.md#promptkit-profiles).
|
[Configuration](../config.md#promptkit-profiles).
|
||||||
|
|
||||||
All extraction prompts share this four-message rendered prefix: the system
|
The transcript assets have distinct consumers. Scene chunking consumes the
|
||||||
message without cache control, the identity message without cache control, the
|
complete-session `common-dnd-transcript-full.md`; extraction prompts consume
|
||||||
campaign-reference message with ephemeral cache control, and the chunk
|
the current-chunk `common-dnd-transcript-chunk.md`; and NPC, location, and item
|
||||||
transcript message with ephemeral cache control. This gives equivalent
|
normalization consume `common-dnd-transcript-windows.md` alongside their
|
||||||
extraction requests the same reusable prefix through their source material.
|
candidate collections. Player, party, glossary, and compatible campaign
|
||||||
|
references provide disambiguating context, not evidence. Reference material is
|
||||||
|
canonically ordered before rendering so equivalent inputs remain stable.
|
||||||
|
|
||||||
Extraction-evidence policy, generated NPC registries, spell catalogs, module
|
Extraction prompts render the common system and identity messages first, then
|
||||||
tasks, and instructions follow the transcript because they are not universal
|
cached campaign references and the cached chunk transcript. Evidence policy and
|
||||||
across all extraction lanes. The final instructions message carries ephemeral
|
any lane-specific registry, catalog, or grounding projection follow that
|
||||||
cache control; evidence, registry, catalog, and task messages do not. Preserve
|
prefix. The final module instructions message is ephemeral. This keeps the
|
||||||
this division when changing an extractor or its assets so prompt-cache behavior
|
reusable extraction prefix identical while preserving the lane-specific suffix.
|
||||||
remains stable.
|
|
||||||
|
|
||||||
The other D&D LLM prompts intentionally follow different patterns. Scene
|
Scene chunking intentionally uses a different order: system, cached campaign
|
||||||
chunking has no sibling extraction lane with which to share its full transcript,
|
references, uncached module instructions, then the final ephemeral full
|
||||||
so it renders campaign references before its task and instructions, then places
|
transcript. Entity normalization also has its own order: system, uncached
|
||||||
the cacheable full transcript last. NPC and location normalization share the
|
module instructions, ephemeral reconciliation policy, uncached candidates, and
|
||||||
entity-reconciliation response schema and safety boundary while retaining their
|
final ephemeral transcript windows. These orders and cache controls are prompt
|
||||||
own task and identity rules. NPC normalization keeps its task and
|
behavior; change them only through the owning manifest and prompt declaration.
|
||||||
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.
|
|
||||||
|
|
||||||
## Evidence, Candidates, And Normalization
|
## Evidence, Candidates, And Normalization
|
||||||
|
|
||||||
@@ -118,8 +126,8 @@ the same comparison identity within one scene-scoped result. Normalization may
|
|||||||
combine results from distinct scenes, so it intentionally does not apply that
|
combine results from distinct scenes, so it intentionally does not apply that
|
||||||
rule. Configuration owns the exact validator key and chain position.
|
rule. Configuration owns the exact validator key and chain position.
|
||||||
|
|
||||||
Normalizers are deterministic for spells, combat turns, item events, NPC
|
Normalizers are deterministic for spells, combat turns, item occurrences, NPC
|
||||||
interactions, scene descriptions, enemy events, and location occurrences. They canonicalize display
|
occurrences, scene descriptions, enemy events, and location occurrences. They canonicalize display
|
||||||
values and evidence, use source-document order for stable output, and issue
|
values and evidence, use source-document order for stable output, and issue
|
||||||
bounded warnings for changes or collapsed duplicates. The NPC and location
|
bounded warnings for changes or collapsed duplicates. The NPC and location
|
||||||
normalizers are intentional exceptions: each first produces a deterministic
|
normalizers are intentional exceptions: each first produces a deterministic
|
||||||
@@ -136,33 +144,38 @@ producer provenance; consumers resolve the handed-off artifact into an
|
|||||||
immutable, validated projection for each operation. External files are checked
|
immutable, validated projection for each operation. External files are checked
|
||||||
during preparation, while generated artifacts are resolved at the handoff.
|
during preparation, while generated artifacts are resolved at the handoff.
|
||||||
|
|
||||||
NPC registries are names-only grounding projections: they may canonicalize
|
NPC, location, and item registries project ordered, source-free `{id, name}`
|
||||||
actors for spells and combat turns and are required for NPC interactions, but
|
pairs to their respective occurrence extractors and normalizers. Exact ID/name
|
||||||
they do not supply evidence. Scene-description registries are eligibility-only
|
matching preserves every identity the registry recognizes, including same-name
|
||||||
projections: they retain the current chunk’s classification data, not scene
|
locations with distinct source anchors. The NPC registry additionally supplies
|
||||||
prose or evidence, and exist to route combat extraction. Enemy-event extraction
|
names-only actor grounding to spells, combat turns, and enemy events.
|
||||||
also projects combat turns to `actor` and `turn_kind` and filters NPC
|
Scene descriptions are eligibility-only projections: they retain current-chunk
|
||||||
interactions to `combat_opponent` names and kinds. Location registries project
|
classification data, not scene prose or evidence, and exist to route combat
|
||||||
ordered `{id, name}` pairs to location-occurrence extraction and normalization;
|
extraction. Enemy-event extraction also projects combat turns to `actor` and
|
||||||
exact ID/name matching keeps same-name locations distinguishable. These compact
|
`turn_kind` and filters NPC occurrences to `combat_opponent` names and kinds.
|
||||||
projections, like NPC grounding, are source-free guidance and never event
|
These projections are guidance only and never event evidence.
|
||||||
evidence.
|
|
||||||
|
|
||||||
## Lane-Specific Rules
|
## Lane-Specific Rules
|
||||||
|
|
||||||
The following differences are intentional and should remain explicit when a
|
The following differences are intentional and should remain explicit when a
|
||||||
shared helper changes.
|
shared helper changes.
|
||||||
|
|
||||||
|
Shared D&D text comparison is identified by `dnd.text_comparison.v1`. Any
|
||||||
|
semantic change requires an explicit policy-version review for every affected
|
||||||
|
identity, mapping, normalization, and validator policy; helper source is not a
|
||||||
|
checkpoint fingerprint.
|
||||||
|
|
||||||
| Lane | Intentional behavior |
|
| Lane | Intentional behavior |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Spells | May use a spell-catalog overlay and optional NPC grounding; the catalog validator supplies domain-specific semantic checks. |
|
| Spells | May use a spell-catalog overlay and optional NPC grounding; the catalog validator supplies domain-specific semantic checks. |
|
||||||
| NPCs | Does not consume an NPC registry. Its normalizer is the LLM-assisted reconciliation exception described above. |
|
| NPC registry | Establishes transcript-grounded NPC identities, including factual third-party mentions, without assigning occurrence categories. It does not consume an NPC registry, and its normalizer is the LLM-assisted reconciliation exception described above. |
|
||||||
| Combat turns | Requires a scene-description artifact. It calls the LLM only for an exact `combat` classification; exact non-combat classifications return an accepted empty result, while missing or mismatched classifications return an empty result with a bounded warning. Optional NPC grounding never becomes evidence. |
|
| Combat turns | Requires a scene-description artifact. It calls the LLM only for an exact `combat` classification; exact non-combat classifications return an accepted empty result, while missing or mismatched classifications return an empty result with a bounded warning. Optional NPC grounding never becomes evidence. |
|
||||||
| Item events | Uses campaign context for disambiguation but has no NPC-registry or scene-description dependency. |
|
| Item occurrences | Requires the normalized item registry for exact ID/name grounding at extraction and normalization. Campaign context may disambiguate, but the registry never becomes occurrence evidence. |
|
||||||
| NPC interactions | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. |
|
| Item registry | Produces source-grounded item types and unique designations. Its LLM-assisted reconciliation is proposal-only, preserves distinct currency denominations and item types, and does not create per-instance identities. |
|
||||||
|
| NPC occurrences | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. It separately emits cited current-transcript occurrence facts, including `mentioned`, rather than deriving them from registry provenance. |
|
||||||
| Scene descriptions | Produces the classifications consumed by combat routing; it does not consume an NPC registry or provide evidence for combat artifacts. |
|
| Scene descriptions | Produces the classifications consumed by combat routing; it does not consume an NPC registry or provide evidence for combat artifacts. |
|
||||||
| Enemy events | Requires NPC, scene-description, combat-turn, and NPC-interaction artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. |
|
| Enemy events | Requires NPC, scene-description, combat-turn, and NPC-occurrence artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. |
|
||||||
| 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. |
|
| 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. |
|
| Location occurrences | Requires the normalized location registry for both extraction and normalization. Its [durable occurrence categories](../integrations/dnd-location-occurrence-artifacts.md#occurrence-categories) distinguish explicit speculation from unsupported inference; the deterministic normalizer enforces exact registry grounding and never turns registry provenance into occurrence evidence. |
|
||||||
|
|
||||||
The combat and scene-description contracts describe their exact handoff and
|
The combat and scene-description contracts describe their exact handoff and
|
||||||
|
|||||||
@@ -35,39 +35,40 @@ pipelines:
|
|||||||
enabled: true
|
enabled: true
|
||||||
window_units: 3
|
window_units: 3
|
||||||
lanes:
|
lanes:
|
||||||
- item-events
|
- item-occurrences
|
||||||
- locations
|
- item-registry
|
||||||
|
- location-registry
|
||||||
- location-occurrences
|
- location-occurrences
|
||||||
- npcs
|
- npc-registry
|
||||||
- spells
|
- spells
|
||||||
- combat-turns
|
- combat-turns
|
||||||
- npc-interactions
|
- npc-occurrences
|
||||||
- enemy-events
|
- enemy-events
|
||||||
steps:
|
steps:
|
||||||
# Establish session-wide reference artifacts alongside independent item events.
|
# Establish session-wide reference artifacts before their consumers.
|
||||||
- id: describe-session
|
- id: describe-session
|
||||||
artifacts:
|
artifacts:
|
||||||
item-events:
|
item-registry:
|
||||||
extract:
|
extract:
|
||||||
module: dnd/item-events
|
module: dnd/item-registry
|
||||||
retries: 2
|
retries: 2
|
||||||
merge: appendorder
|
merge: appendorder
|
||||||
normalize: dnd/item-events
|
normalize: dnd/item-registry
|
||||||
npcs:
|
npc-registry:
|
||||||
extract:
|
extract:
|
||||||
module: dnd/npcs
|
module: dnd/npc-registry
|
||||||
retries: 2
|
retries: 2
|
||||||
merge: appendorder
|
merge: appendorder
|
||||||
normalize:
|
normalize:
|
||||||
module: dnd/npcs
|
module: dnd/npc-registry
|
||||||
retries: 2
|
retries: 2
|
||||||
locations:
|
location-registry:
|
||||||
extract:
|
extract:
|
||||||
module: dnd/locations
|
module: dnd/location-registry
|
||||||
retries: 2
|
retries: 2
|
||||||
merge: appendorder
|
merge: appendorder
|
||||||
normalize:
|
normalize:
|
||||||
module: dnd/locations
|
module: dnd/location-registry
|
||||||
retries: 2
|
retries: 2
|
||||||
scene-descriptions:
|
scene-descriptions:
|
||||||
extract:
|
extract:
|
||||||
@@ -79,19 +80,29 @@ pipelines:
|
|||||||
# Accepted registry artifacts and scene-description eligibility artifacts
|
# Accepted registry artifacts and scene-description eligibility artifacts
|
||||||
# are supplied in memory to their compatible consumers in this step.
|
# are supplied in memory to their compatible consumers in this step.
|
||||||
references:
|
references:
|
||||||
locations:
|
location_registry:
|
||||||
artifact:
|
artifact:
|
||||||
step: describe-session
|
step: describe-session
|
||||||
lane: locations
|
lane: location-registry
|
||||||
npcs:
|
npc_registry:
|
||||||
artifact:
|
artifact:
|
||||||
step: describe-session
|
step: describe-session
|
||||||
lane: npcs
|
lane: npc-registry
|
||||||
scene_descriptions:
|
scene_descriptions:
|
||||||
artifact:
|
artifact:
|
||||||
step: describe-session
|
step: describe-session
|
||||||
lane: scene-descriptions
|
lane: scene-descriptions
|
||||||
|
item_registry:
|
||||||
|
artifact:
|
||||||
|
step: describe-session
|
||||||
|
lane: item-registry
|
||||||
artifacts:
|
artifacts:
|
||||||
|
item-occurrences:
|
||||||
|
extract:
|
||||||
|
module: dnd/item-occurrences
|
||||||
|
retries: 2
|
||||||
|
merge: appendorder
|
||||||
|
normalize: dnd/item-occurrences
|
||||||
spells:
|
spells:
|
||||||
extract:
|
extract:
|
||||||
module: dnd/spells
|
module: dnd/spells
|
||||||
@@ -110,12 +121,12 @@ pipelines:
|
|||||||
retries: 2
|
retries: 2
|
||||||
merge: appendorder
|
merge: appendorder
|
||||||
normalize: dnd/combat-turns
|
normalize: dnd/combat-turns
|
||||||
npc-interactions:
|
npc-occurrences:
|
||||||
extract:
|
extract:
|
||||||
module: dnd/npc-interactions
|
module: dnd/npc-occurrences
|
||||||
retries: 2
|
retries: 2
|
||||||
merge: appendorder
|
merge: appendorder
|
||||||
normalize: dnd/npc-interactions
|
normalize: dnd/npc-occurrences
|
||||||
location-occurrences:
|
location-occurrences:
|
||||||
extract:
|
extract:
|
||||||
module: dnd/location-occurrences
|
module: dnd/location-occurrences
|
||||||
@@ -124,10 +135,10 @@ pipelines:
|
|||||||
normalize: dnd/location-occurrences
|
normalize: dnd/location-occurrences
|
||||||
- id: track-enemies
|
- id: track-enemies
|
||||||
references:
|
references:
|
||||||
npcs:
|
npc_registry:
|
||||||
artifact:
|
artifact:
|
||||||
step: describe-session
|
step: describe-session
|
||||||
lane: npcs
|
lane: npc-registry
|
||||||
scene_descriptions:
|
scene_descriptions:
|
||||||
artifact:
|
artifact:
|
||||||
step: describe-session
|
step: describe-session
|
||||||
@@ -136,10 +147,10 @@ pipelines:
|
|||||||
artifact:
|
artifact:
|
||||||
step: extract-events
|
step: extract-events
|
||||||
lane: combat-turns
|
lane: combat-turns
|
||||||
npc_interactions:
|
npc_occurrences:
|
||||||
artifact:
|
artifact:
|
||||||
step: extract-events
|
step: extract-events
|
||||||
lane: npc-interactions
|
lane: npc-occurrences
|
||||||
artifacts:
|
artifacts:
|
||||||
enemy-events:
|
enemy-events:
|
||||||
extract:
|
extract:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
|
|||||||
if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" {
|
if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" {
|
||||||
t.Fatalf("combat codec spec = %#v, want compatible durable schema", codecSpec)
|
t.Fatalf("combat codec spec = %#v, want compatible durable schema", codecSpec)
|
||||||
}
|
}
|
||||||
if !hasReferenceSlot(extractSpec.ReferenceSlots, "npcs") || !hasReferenceSlot(extractSpec.ReferenceSlots, "scene_descriptions") || !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npcs") {
|
if !hasReferenceSlot(extractSpec.ReferenceSlots, "npc_registry") || !hasReferenceSlot(extractSpec.ReferenceSlots, "scene_descriptions") || !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npc_registry") {
|
||||||
t.Fatalf("combat reference slots = %#v / %#v, want extraction scene and NPC slots plus normalization NPC slot", extractSpec.ReferenceSlots, normalizeSpec.ReferenceSlots)
|
t.Fatalf("combat reference slots = %#v / %#v, want extraction scene and NPC slots plus normalization NPC slot", extractSpec.ReferenceSlots, normalizeSpec.ReferenceSlots)
|
||||||
}
|
}
|
||||||
sceneSlot := referenceSlot(extractSpec.ReferenceSlots, "scene_descriptions")
|
sceneSlot := referenceSlot(extractSpec.ReferenceSlots, "scene_descriptions")
|
||||||
@@ -85,15 +85,15 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
|
|||||||
PipelineID: "dnd-combat",
|
PipelineID: "dnd-combat",
|
||||||
Catalog: catalog,
|
Catalog: catalog,
|
||||||
ReferenceOverrides: []pipeline.ReferenceBinding{
|
ReferenceOverrides: []pipeline.ReferenceBinding{
|
||||||
{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||||
{Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
{Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resolve(bound references) error = %v, want nil", err)
|
t.Fatalf("Resolve(bound references) error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
boundLane := bound.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
boundLane := bound.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
||||||
if len(boundLane.ExtractReferences.Bindings) != 2 || len(boundLane.NormalizeReferences.Bindings) != 1 || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "npcs") || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "scene_descriptions") || !hasReferenceBinding(boundLane.NormalizeReferences.Bindings, "npcs") {
|
if len(boundLane.ExtractReferences.Bindings) != 2 || len(boundLane.NormalizeReferences.Bindings) != 1 || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "npc_registry") || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "scene_descriptions") || !hasReferenceBinding(boundLane.NormalizeReferences.Bindings, "npc_registry") {
|
||||||
t.Fatalf("bound combat references = %#v / %#v, want extraction scene and NPC bindings plus normalization NPC binding", boundLane.ExtractReferences, boundLane.NormalizeReferences)
|
t.Fatalf("bound combat references = %#v / %#v, want extraction scene and NPC bindings plus normalization NPC binding", boundLane.ExtractReferences, boundLane.NormalizeReferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,20 +20,26 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
|
||||||
|
itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences"
|
||||||
|
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
|
||||||
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
|
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"
|
combat "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
||||||
enemyevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
|
enemyevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
|
||||||
itemevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
|
itemoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
|
||||||
|
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
|
||||||
locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
||||||
locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locations"
|
locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
|
||||||
npcinteractions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions"
|
npcoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
|
||||||
npcs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
|
||||||
scenedescriptions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
|
scenedescriptions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||||
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
|
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
|
||||||
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locations"
|
itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
|
||||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
|
||||||
|
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T) {
|
func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T) {
|
||||||
@@ -55,17 +61,17 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
|
|||||||
t.Fatalf("enemy event lane = %#v, want typed production composition", lane)
|
t.Fatalf("enemy event lane = %#v, want typed production composition", lane)
|
||||||
}
|
}
|
||||||
for slot, want := range map[string]struct{ step, lane string }{
|
for slot, want := range map[string]struct{ step, lane string }{
|
||||||
"npcs": {step: "describe-session", lane: "npcs"},
|
"npc_registry": {step: "describe-session", lane: "npc-registry"},
|
||||||
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
|
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
|
||||||
"combat_turns": {step: "extract-events", lane: "combat-turns"},
|
"combat_turns": {step: "extract-events", lane: "combat-turns"},
|
||||||
"npc_interactions": {step: "extract-events", lane: "npc-interactions"},
|
"npc_occurrences": {step: "extract-events", lane: "npc-occurrences"},
|
||||||
} {
|
} {
|
||||||
binding, found := generatedReferenceBinding(lane.ExtractReferences.Bindings, slot)
|
binding, found := generatedReferenceBinding(lane.ExtractReferences.Bindings, slot)
|
||||||
if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane {
|
if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane {
|
||||||
t.Fatalf("enemy event %s reference = %#v, want generated %s/%s artifact", slot, binding, want.step, want.lane)
|
t.Fatalf("enemy event %s reference = %#v, want generated %s/%s artifact", slot, binding, want.step, want.lane)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if binding, found := generatedReferenceBinding(lane.NormalizeReferences.Bindings, "npcs"); !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "npcs" {
|
if binding, found := generatedReferenceBinding(lane.NormalizeReferences.Bindings, "npc_registry"); !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "npc-registry" {
|
||||||
t.Fatalf("enemy event normalizer NPC reference = %#v, want generated NPC artifact", binding)
|
t.Fatalf("enemy event normalizer NPC reference = %#v, want generated NPC artifact", binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,24 +84,24 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
|
|||||||
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
||||||
t.Fatalf("enemy event normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
t.Fatalf("enemy event normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
||||||
}
|
}
|
||||||
for _, slot := range []string{"npcs", "scene_descriptions", "combat_turns", "npc_interactions"} {
|
for _, slot := range []string{"npc_registry", "scene_descriptions", "combat_turns", "npc_occurrences"} {
|
||||||
if !hasReferenceSlot(extractSpec.ReferenceSlots, slot) {
|
if !hasReferenceSlot(extractSpec.ReferenceSlots, slot) {
|
||||||
t.Fatalf("enemy event extractor slots = %#v, want %q", extractSpec.ReferenceSlots, slot)
|
t.Fatalf("enemy event extractor slots = %#v, want %q", extractSpec.ReferenceSlots, slot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npcs") {
|
if !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npc_registry") {
|
||||||
t.Fatalf("enemy event normalizer slots = %#v, want NPC registry", normalizeSpec.ReferenceSlots)
|
t.Fatalf("enemy event normalizer slots = %#v, want NPC registry", normalizeSpec.ReferenceSlots)
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := cfg.Pipelines["dnd-session"]
|
profile := cfg.Pipelines["dnd-session"]
|
||||||
profile.Steps[2].References["npcs"] = pipeline.GeneratedReference("track-enemies", "enemy-events")
|
profile.Steps[2].References["npc_registry"] = pipeline.GeneratedReference("track-enemies", "enemy-events")
|
||||||
cfg.Pipelines["dnd-session"] = profile
|
cfg.Pipelines["dnd-session"] = profile
|
||||||
if _, err := cfg.Resolve(resolveInputForMaintainedExample(components, "dnd-session")); err == nil || !strings.Contains(err.Error(), "earlier step") {
|
if _, err := cfg.Resolve(resolveInputForMaintainedExample(components, "dnd-session")); err == nil || !strings.Contains(err.Error(), "earlier step") {
|
||||||
t.Fatalf("Resolve() error = %v, want future generated-reference rejection", err)
|
t.Fatalf("Resolve() error = %v, want future generated-reference rejection", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t *testing.T) {
|
func TestMaintainedCompleteExamplePublishesRegistryBackedEntityOccurrences(t *testing.T) {
|
||||||
t.Chdir(repositoryPath())
|
t.Chdir(repositoryPath())
|
||||||
outputRoot := filepath.Join(t.TempDir(), "output")
|
outputRoot := filepath.Join(t.TempDir(), "output")
|
||||||
configPath := completeExampleConfigWithTemporaryCache(t)
|
configPath := completeExampleConfigWithTemporaryCache(t)
|
||||||
@@ -121,18 +127,17 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
|||||||
|
|
||||||
runRoot := filepath.Join(outputRoot, productionRunID)
|
runRoot := filepath.Join(outputRoot, productionRunID)
|
||||||
index := readProductionJSON[exampleOutputIndex](t, filepath.Join(runRoot, "index.json"))
|
index := readProductionJSON[exampleOutputIndex](t, filepath.Join(runRoot, "index.json"))
|
||||||
var enemyOutput exampleOutputIndexEntry
|
outputs := make(map[string]exampleOutputIndexEntry)
|
||||||
var locationOutput, occurrenceOutput exampleOutputIndexEntry
|
|
||||||
for _, entry := range index.OutputFiles {
|
for _, entry := range index.OutputFiles {
|
||||||
switch entry.LaneID {
|
outputs[entry.LaneID] = entry
|
||||||
case "enemy-events":
|
|
||||||
enemyOutput = entry
|
|
||||||
case "locations":
|
|
||||||
locationOutput = entry
|
|
||||||
case "location-occurrences":
|
|
||||||
occurrenceOutput = 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" {
|
if enemyOutput.File != "lanes/enemy-events.json" || enemyOutput.SchemaID != "notarius.dnd.enemy_events" || enemyOutput.SchemaVersion != "v1" {
|
||||||
t.Fatalf("enemy event output = %#v, want typed enemy-event JSON", enemyOutput)
|
t.Fatalf("enemy event output = %#v, want typed enemy-event JSON", enemyOutput)
|
||||||
}
|
}
|
||||||
@@ -140,23 +145,51 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
|||||||
if len(value.Events) != 1 || value.Events[0].Name != "Kesh" || value.Events[0].Kind != dnd.EnemyEventKindFled || len(value.Events[0].SourceRefs) != 1 || value.Events[0].SourceRefs[0].SourceID != "session-ravenfall" || value.Events[0].SourceRefs[0].StartUnitID != 10 {
|
if len(value.Events) != 1 || value.Events[0].Name != "Kesh" || value.Events[0].Kind != dnd.EnemyEventKindFled || len(value.Events[0].SourceRefs) != 1 || value.Events[0].SourceRefs[0].SourceID != "session-ravenfall" || value.Events[0].SourceRefs[0].StartUnitID != 10 {
|
||||||
t.Fatalf("enemy event artifact = %#v, want source-linked Kesh fleeing event", value)
|
t.Fatalf("enemy event artifact = %#v, want source-linked Kesh fleeing event", value)
|
||||||
}
|
}
|
||||||
if locationOutput.File != "lanes/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)
|
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 {
|
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)
|
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 {
|
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", occurrenceOutput)
|
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" {
|
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)
|
t.Fatalf("location occurrences = %#v, want source-grounded references to distinct registry identities", occurrencesValue)
|
||||||
}
|
}
|
||||||
|
if npcRegistryOutput.File != "lanes/npc-registry.json" || npcRegistryOutput.SchemaID != npccodec.SchemaID || npcRegistryOutput.SchemaVersion != npccodec.SchemaVersion {
|
||||||
|
t.Fatalf("NPC registry output = %#v, want typed registry JSON", npcRegistryOutput)
|
||||||
|
}
|
||||||
|
npcRegistry := readProductionJSON[dnd.NPCRegistry](t, filepath.Join(runRoot, npcRegistryOutput.File))
|
||||||
|
if len(npcRegistry.NPCs) != 1 || npcRegistry.NPCs[0].Name != "Kesh" {
|
||||||
|
t.Fatalf("NPC registry = %#v, want Kesh identity", npcRegistry)
|
||||||
|
}
|
||||||
|
if npcOccurrenceOutput.File != "lanes/npc-occurrences.json" || npcOccurrenceOutput.SchemaID != npcoccurrencecodec.SchemaID || npcOccurrenceOutput.SchemaVersion != npcoccurrencecodec.SchemaVersion {
|
||||||
|
t.Fatalf("NPC occurrence output = %#v, want typed occurrence JSON", npcOccurrenceOutput)
|
||||||
|
}
|
||||||
|
npcOccurrences := readProductionJSON[dnd.NPCOccurrenceList](t, filepath.Join(runRoot, npcOccurrenceOutput.File))
|
||||||
|
if len(npcOccurrences.Occurrences) != 1 || npcOccurrences.Occurrences[0].NPCID != npcRegistry.NPCs[0].ID || npcOccurrences.Occurrences[0].Name != "Kesh" || len(npcOccurrences.Occurrences[0].SourceRefs) != 1 || npcOccurrences.Occurrences[0].SourceRefs[0].StartUnitID != 7 {
|
||||||
|
t.Fatalf("NPC occurrences = %#v, want independently evidenced Kesh registry grounding", npcOccurrences)
|
||||||
|
}
|
||||||
|
if itemRegistryOutput.File != "lanes/item-registry.json" || itemRegistryOutput.SchemaID != itemregistrycodec.SchemaID || itemRegistryOutput.SchemaVersion != itemregistrycodec.SchemaVersion {
|
||||||
|
t.Fatalf("item registry output = %#v, want typed registry JSON", itemRegistryOutput)
|
||||||
|
}
|
||||||
|
itemRegistry := readProductionJSON[dnd.ItemRegistry](t, filepath.Join(runRoot, itemRegistryOutput.File))
|
||||||
|
if len(itemRegistry.Items) != 1 || itemRegistry.Items[0].Name != "Moonblade" {
|
||||||
|
t.Fatalf("item registry = %#v, want Moonblade identity", itemRegistry)
|
||||||
|
}
|
||||||
|
if itemOccurrenceOutput.File != "lanes/item-occurrences.json" || itemOccurrenceOutput.SchemaID != itemoccurrencecodec.SchemaID || itemOccurrenceOutput.SchemaVersion != itemoccurrencecodec.SchemaVersion {
|
||||||
|
t.Fatalf("item occurrence output = %#v, want typed occurrence JSON", itemOccurrenceOutput)
|
||||||
|
}
|
||||||
|
itemOccurrences := readProductionJSON[dnd.ItemOccurrenceList](t, filepath.Join(runRoot, itemOccurrenceOutput.File))
|
||||||
|
if len(itemOccurrences.Occurrences) != 1 || itemOccurrences.Occurrences[0].ItemID != itemRegistry.Items[0].ID || itemOccurrences.Occurrences[0].Name != "Moonblade" || len(itemOccurrences.Occurrences[0].SourceRefs) != 1 || itemOccurrences.Occurrences[0].SourceRefs[0].StartUnitID != 5 {
|
||||||
|
t.Fatalf("item occurrences = %#v, want independently evidenced Moonblade registry grounding", itemOccurrences)
|
||||||
|
}
|
||||||
|
|
||||||
evidence := readProductionJSON[evidencecontext.Document](t, filepath.Join(runRoot, "evidence-context.json"))
|
evidence := readProductionJSON[evidencecontext.Document](t, filepath.Join(runRoot, "evidence-context.json"))
|
||||||
for _, laneID := range []string{"enemy-events", "locations", "location-occurrences"} {
|
for _, laneID := range []string{"enemy-events", "npc-registry", "npc-occurrences", "item-registry", "item-occurrences", "location-registry", "location-occurrences"} {
|
||||||
if !containsString(evidence.SelectedLanes, laneID) || !evidenceHasLane(evidence, laneID) {
|
if !containsString(evidence.SelectedLanes, laneID) || !evidenceHasLane(evidence, laneID) {
|
||||||
t.Fatalf("evidence context = %#v, want direct %s evidence", evidence, laneID)
|
t.Fatalf("evidence context = %#v, want direct %s evidence", evidence, laneID)
|
||||||
}
|
}
|
||||||
@@ -171,9 +204,9 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
|||||||
t.Fatalf("enemy event session = %q, want shared session", request.SessionID)
|
t.Fatalf("enemy event session = %q, want shared session", request.SessionID)
|
||||||
}
|
}
|
||||||
for slot, required := range map[string]string{
|
for slot, required := range map[string]string{
|
||||||
"npcs": "Kesh",
|
"npc_registry": "Kesh",
|
||||||
"combat_turns": "Kesh",
|
"combat_turns": "Kesh",
|
||||||
"npc_interactions": "Kesh",
|
"npc_occurrences": "Kesh",
|
||||||
} {
|
} {
|
||||||
input, ok := request.Inputs[slot]
|
input, ok := request.Inputs[slot]
|
||||||
if !ok || !strings.Contains(string(input.Content), required) || strings.Contains(string(input.Content), "source_refs") || strings.Contains(string(input.Content), "start_unit_id") {
|
if !ok || !strings.Contains(string(input.Content), required) || strings.Contains(string(input.Content), "source_refs") || strings.Contains(string(input.Content), "start_unit_id") {
|
||||||
@@ -185,11 +218,30 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
|
|||||||
t.Fatalf("location occurrence requests = %#v, want one request per scene", locationRequests)
|
t.Fatalf("location occurrence requests = %#v, want one request per scene", locationRequests)
|
||||||
}
|
}
|
||||||
for _, request := range locationRequests {
|
for _, request := range locationRequests {
|
||||||
registryInput := request.Inputs["locations"]
|
registryInput := request.Inputs["location_registry"]
|
||||||
if !strings.Contains(string(registryInput.Content), "Moon Gate") || !strings.Contains(string(registryInput.Content), `"id"`) || strings.Contains(string(registryInput.Content), "source_refs") {
|
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)
|
t.Fatalf("location occurrence registry input = %q, want source-free ID grounding", registryInput.Content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, test := range []struct {
|
||||||
|
promptID string
|
||||||
|
slot string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{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]
|
||||||
|
if !strings.Contains(string(registryInput.Content), test.name) || !strings.Contains(string(registryInput.Content), `"id"`) || strings.Contains(string(registryInput.Content), "source_refs") {
|
||||||
|
t.Fatalf("%s registry input = %q, want source-free ID grounding", test.promptID, registryInput.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeExampleConfigWithTemporaryCache(t *testing.T) string {
|
func completeExampleConfigWithTemporaryCache(t *testing.T) string {
|
||||||
@@ -229,7 +281,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
|
|||||||
switch request.PromptID {
|
switch request.PromptID {
|
||||||
case scenes.PromptID:
|
case scenes.PromptID:
|
||||||
content = []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":6},{"start_unit_id":7,"end_unit_id":11}]}`)
|
content = []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":6},{"start_unit_id":7,"end_unit_id":11}]}`)
|
||||||
case npcs.PromptID:
|
case npcregistry.PromptID:
|
||||||
if combatScene {
|
if combatScene {
|
||||||
content = []byte(`{"npcs":[{"name":"Kesh","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
|
content = []byte(`{"npcs":[{"name":"Kesh","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -253,15 +305,49 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
|
|||||||
content = []byte(`{"duplicate_groups":[]}`)
|
content = []byte(`{"duplicate_groups":[]}`)
|
||||||
case spells.PromptID:
|
case spells.PromptID:
|
||||||
content = []byte(`{"spell_casts":[]}`)
|
content = []byte(`{"spell_casts":[]}`)
|
||||||
case itemevents.PromptID:
|
case itemregistry.PromptID:
|
||||||
content = []byte(`{"events":[]}`)
|
if combatScene {
|
||||||
|
content = []byte(`{"items":[]}`)
|
||||||
|
} else {
|
||||||
|
content = []byte(`{"items":[{"name":"Moonblade","source_refs":[{"start_unit_id":5,"end_unit_id":5}]}]}`)
|
||||||
|
}
|
||||||
|
case itemregistrynormalize.PromptID:
|
||||||
|
content = []byte(`{"duplicate_groups":[]}`)
|
||||||
|
case itemoccurrences.PromptID:
|
||||||
|
if combatScene {
|
||||||
|
content = []byte(`{"occurrences":[]}`)
|
||||||
|
} else {
|
||||||
|
var registry struct {
|
||||||
|
Items []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `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 {
|
||||||
|
return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated item registry has %d items, want 1", len(registry.Items))
|
||||||
|
}
|
||||||
|
content = []byte(fmt.Sprintf(`{"occurrences":[{"item_id":%q,"name":"Moonblade","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[{"start_segment":5,"end_segment":5}]}]}`, registry.Items[0].ID))
|
||||||
|
}
|
||||||
case combat.PromptID:
|
case combat.PromptID:
|
||||||
content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`)
|
content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`)
|
||||||
case npcinteractions.PromptID:
|
case npcoccurrences.PromptID:
|
||||||
if combatScene {
|
if combatScene {
|
||||||
content = []byte(`{"interactions":[{"name":"Kesh","kind":"combat_opponent","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`)
|
var registry struct {
|
||||||
|
NPCs []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `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 {
|
||||||
|
return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated NPC registry has no NPCs")
|
||||||
|
}
|
||||||
|
content = []byte(fmt.Sprintf(`{"occurrences":[{"npc_id":%q,"name":"Kesh","kind":"combat_opponent","source_refs":[{"start_unit_id":7,"end_unit_id":7}]}]}`, registry.NPCs[0].ID))
|
||||||
} else {
|
} else {
|
||||||
content = []byte(`{"interactions":[]}`)
|
content = []byte(`{"occurrences":[]}`)
|
||||||
}
|
}
|
||||||
case locationoccurrences.PromptID:
|
case locationoccurrences.PromptID:
|
||||||
var registry struct {
|
var registry struct {
|
||||||
@@ -269,7 +355,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
} `json:"locations"`
|
} `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)
|
return contracts.StructuredCompletionResponse{}, fmt.Errorf("decode generated location registry: %w", err)
|
||||||
}
|
}
|
||||||
if len(registry.Locations) == 0 {
|
if len(registry.Locations) == 0 {
|
||||||
|
|||||||
@@ -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/core/config"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
|
||||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
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)
|
t.Fatalf("artifact lanes = %#v, want one NPC lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
|
||||||
}
|
}
|
||||||
lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
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)
|
t.Fatalf("resolved NPC lane = %#v, want typed production composition", lane)
|
||||||
}
|
}
|
||||||
if len(lane.ExtractReferences.Bindings) != 0 || len(lane.NormalizeReferences.Bindings) != 0 {
|
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)
|
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)
|
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"}) {
|
if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
|
||||||
t.Fatalf("NPC merger spec = %#v, want merged capability", mergeSpec)
|
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"}) {
|
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)
|
t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
||||||
}
|
}
|
||||||
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCListKind)
|
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCRegistryKind)
|
||||||
if !ok || codecSpec.Kind != dnd.NPCListKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
|
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)
|
t.Fatalf("NPC codec spec = %#v, want typed v1 durable schema", codecSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantExtractChain := []pipeline.ModuleBinding{
|
wantExtractChain := []pipeline.ModuleBinding{
|
||||||
pipeline.Binding("generic/valid_json"),
|
pipeline.Binding("generic/valid_json"),
|
||||||
pipeline.Binding("extract/dnd/npcs/shape"),
|
pipeline.Binding("extract/dnd/npc-registry/shape"),
|
||||||
pipeline.Binding("extract/dnd/npcs/source_refs"),
|
pipeline.Binding("extract/dnd/npc-registry/source_refs"),
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
pipeline.Binding("generic/valid_json_schema"),
|
||||||
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
|
pipeline.Binding("extract/dnd/npc-registry/source_relatedness"),
|
||||||
}
|
}
|
||||||
wantNormalizeChain := []pipeline.ModuleBinding{
|
wantNormalizeChain := []pipeline.ModuleBinding{
|
||||||
pipeline.Binding("generic/valid_json"),
|
pipeline.Binding("generic/valid_json"),
|
||||||
pipeline.Binding("extract/dnd/npcs/shape"),
|
pipeline.Binding("extract/dnd/npc-registry/shape"),
|
||||||
pipeline.Binding("normalize/dnd/npcs/identity"),
|
pipeline.Binding("normalize/dnd/npc-registry/identity"),
|
||||||
pipeline.Binding("extract/dnd/npcs/source_refs"),
|
pipeline.Binding("extract/dnd/npc-registry/source_refs"),
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
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) {
|
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, npcextract.Key); !reflect.DeepEqual(got, wantExtractChain) {
|
||||||
t.Fatalf("NPC extract chain = %#v, want %#v", 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) {
|
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
||||||
lane := profile.Artifacts["npcs"]
|
lane := profile.Artifacts["npc_registry"]
|
||||||
lane.Extract.Options = map[string]any{"unexpected": true}
|
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") {
|
}); err == nil || !strings.Contains(err.Error(), "unknown option") {
|
||||||
t.Fatalf("unknown extractor option error = %v, want strict option rejection", err)
|
t.Fatalf("unknown extractor option error = %v, want strict option rejection", err)
|
||||||
}
|
}
|
||||||
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
||||||
lane := profile.Artifacts["npcs"]
|
lane := profile.Artifacts["npc_registry"]
|
||||||
lane.Normalize.Options = map[string]any{"unexpected": true}
|
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") {
|
}); err == nil || !strings.Contains(err.Error(), "unknown option") {
|
||||||
t.Fatalf("unknown normalizer option error = %v, want strict option rejection", err)
|
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)
|
t.Fatalf("optional NPC references error = %v, want resolution success", err)
|
||||||
}
|
}
|
||||||
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
||||||
lane := profile.Artifacts["npcs"]
|
lane := profile.Artifacts["npc_registry"]
|
||||||
lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("normalize/dnd/npcs/identity")}
|
lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("normalize/dnd/npc-registry/identity")}
|
||||||
profile.Artifacts["npcs"] = lane
|
profile.Artifacts["npc_registry"] = lane
|
||||||
}); err == nil || !strings.Contains(err.Error(), "artifact lane level") {
|
}); err == nil || !strings.Contains(err.Error(), "artifact lane level") {
|
||||||
t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err)
|
t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err)
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ func productionNPCContractConfig() config.Config {
|
|||||||
Input: pipeline.Binding("seriatim"),
|
Input: pipeline.Binding("seriatim"),
|
||||||
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
|
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
|
||||||
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||||
"npcs": {
|
"npc_registry": {
|
||||||
Extract: pipeline.ModuleBinding{Module: npcextract.Key, Retries: 2},
|
Extract: pipeline.ModuleBinding{Module: npcextract.Key, Retries: 2},
|
||||||
Normalize: pipeline.Binding(npcnormalize.Key),
|
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/framework/pipeline"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
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"
|
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"
|
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
"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)
|
t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err)
|
||||||
}
|
}
|
||||||
if example.name == "complete" {
|
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)
|
t.Fatalf("complete example steps and lanes = %v, want the documented D&D extractor composition", got)
|
||||||
}
|
}
|
||||||
locationLane := referenceContractLane(t, materialized, "locations")
|
locationLane := referenceContractLane(t, materialized, "location-registry")
|
||||||
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 {
|
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)
|
t.Fatalf("location lane = %#v, want typed registry composition", locationLane)
|
||||||
}
|
}
|
||||||
occurrenceLane := referenceContractLane(t, materialized, "location-occurrences")
|
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)
|
t.Fatalf("location occurrence lane = %#v, want typed occurrence composition", occurrenceLane)
|
||||||
}
|
}
|
||||||
for _, target := range []pipeline.ResolvedReferenceTarget{occurrenceLane.ExtractReferences, occurrenceLane.NormalizeReferences} {
|
for _, target := range []pipeline.ResolvedReferenceTarget{occurrenceLane.ExtractReferences, occurrenceLane.NormalizeReferences} {
|
||||||
binding, found := generatedReferenceBinding(target.Bindings, "locations")
|
binding, found := generatedReferenceBinding(target.Bindings, "location_registry")
|
||||||
if !found || binding.Artifact.Step != "describe-session" || binding.Artifact.Lane != "locations" {
|
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)
|
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 {
|
len(spellLane.NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
|
||||||
t.Fatalf("complete example spell catalog reference was not materialized: %#v", spellLane)
|
t.Fatalf("complete example spell catalog reference was not materialized: %#v", spellLane)
|
||||||
}
|
}
|
||||||
itemEventLane := referenceContractLane(t, materialized, "item-events")
|
itemOccurrenceLane := referenceContractLane(t, materialized, "item-occurrences")
|
||||||
for _, references := range []pipeline.ResolvedReferenceTarget{itemEventLane.ExtractReferences, itemEventLane.NormalizeReferences} {
|
for _, references := range []pipeline.ResolvedReferenceTarget{itemOccurrenceLane.ExtractReferences, itemOccurrenceLane.NormalizeReferences} {
|
||||||
if _, found := references.ReferenceSet.Slots["npcs"]; found {
|
if _, found := references.ReferenceSet.Slots["npc_registry"]; found {
|
||||||
t.Fatalf("item event lane unexpectedly depends on generated NPCs: %#v", itemEventLane)
|
t.Fatalf("item occurrence lane unexpectedly depends on generated NPCs: %#v", itemOccurrenceLane)
|
||||||
}
|
}
|
||||||
if _, found := references.ReferenceSet.Slots["scene_descriptions"]; found {
|
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")
|
enemyEventLane := referenceContractLane(t, materialized, "enemy-events")
|
||||||
for slot, want := range map[string]struct{ step, lane string }{
|
for slot, want := range map[string]struct{ step, lane string }{
|
||||||
"npcs": {step: "describe-session", lane: "npcs"},
|
"npc_registry": {step: "describe-session", lane: "npc-registry"},
|
||||||
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
|
"scene_descriptions": {step: "describe-session", lane: "scene-descriptions"},
|
||||||
"combat_turns": {step: "extract-events", lane: "combat-turns"},
|
"combat_turns": {step: "extract-events", lane: "combat-turns"},
|
||||||
"npc_interactions": {step: "extract-events", lane: "npc-interactions"},
|
"npc_occurrences": {step: "extract-events", lane: "npc-occurrences"},
|
||||||
} {
|
} {
|
||||||
binding, found := generatedReferenceBinding(enemyEventLane.ExtractReferences.Bindings, slot)
|
binding, found := generatedReferenceBinding(enemyEventLane.ExtractReferences.Bindings, slot)
|
||||||
if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane {
|
if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pipelines:
|
|||||||
extract:
|
extract:
|
||||||
module: dnd/spells
|
module: dnd/spells
|
||||||
references:
|
references:
|
||||||
npcs: %q
|
npc_registry: %q
|
||||||
normalize: dnd/spells
|
normalize: dnd/spells
|
||||||
`, checkpointRoot, npcPath)
|
`, checkpointRoot, npcPath)
|
||||||
configPath := filepath.Join(t.TempDir(), "config.yml")
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
||||||
@@ -66,7 +66,7 @@ pipelines:
|
|||||||
"--input", repositoryPath("examples", "seriatim-minimal-transcript.json"),
|
"--input", repositoryPath("examples", "seriatim-minimal-transcript.json"),
|
||||||
"--chunk_cache", "bypass", "--output-dir", t.TempDir(),
|
"--chunk_cache", "bypass", "--output-dir", t.TempDir(),
|
||||||
}, &stdout, &stderr, options)
|
}, &stdout, &stderr, options)
|
||||||
for _, fragment := range []string{`pipeline "dnd-session"`, `reference slot "npcs"`, "1048577 bytes", "limit 1048576"} {
|
for _, fragment := range []string{`pipeline "dnd-session"`, `reference slot "npc_registry"`, "1048577 bytes", "limit 1048576"} {
|
||||||
if code == 0 || !strings.Contains(stderr.String(), fragment) {
|
if code == 0 || !strings.Contains(stderr.String(), fragment) {
|
||||||
t.Fatalf("RunWithOptions() code = %d stderr = %q, want context fragment %q", code, stderr.String(), fragment)
|
t.Fatalf("RunWithOptions() code = %d stderr = %q, want context fragment %q", code, stderr.String(), fragment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -30,15 +29,33 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
|
||||||
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
|
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
|
||||||
enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents"
|
enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents"
|
||||||
itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents"
|
itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences"
|
||||||
|
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
|
||||||
|
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
|
||||||
|
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
|
||||||
|
npcoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
|
||||||
|
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
|
||||||
|
scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
|
||||||
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
|
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
|
||||||
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
||||||
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
|
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
|
||||||
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
|
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
|
||||||
|
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
|
||||||
|
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
|
||||||
|
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
|
||||||
|
npcoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
|
||||||
|
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
|
||||||
|
sceneextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||||
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
|
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
|
||||||
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
|
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
|
||||||
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
|
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
|
||||||
|
itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
|
||||||
|
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
|
||||||
|
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
|
||||||
|
npcoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
|
||||||
|
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
|
||||||
|
scenenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions"
|
||||||
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
|
||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"gitea.maximumdirect.net/eric/promptkit"
|
||||||
@@ -50,9 +67,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
|
|||||||
|
|
||||||
assertProductionContains(t, "inputs", registries.Inputs.RegisteredKeys(), []string{"seriatim"})
|
assertProductionContains(t, "inputs", registries.Inputs.RegisteredKeys(), []string{"seriatim"})
|
||||||
assertProductionContains(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes", "generic"})
|
assertProductionContains(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes", "generic"})
|
||||||
assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", "dnd/npcs", combatextract.Key, itemeventextract.Key, enemyeventextract.Key})
|
assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{spells.Key, npcextract.Key, combatextract.Key, itemoccurrenceextract.Key, itemregistryextract.Key, npcoccurrenceextract.Key, sceneextract.Key, locationextract.Key, locationoccurrenceextract.Key, enemyeventextract.Key})
|
||||||
assertProductionContains(t, "mergers", registries.Mergers.RegisteredKeys(), []string{"appendorder"})
|
assertProductionContains(t, "mergers", registries.Mergers.RegisteredKeys(), []string{"appendorder"})
|
||||||
assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{"noop", spellnormalize.Key, "dnd/npcs", combatnormalize.Key, itemeventnormalize.Key, enemyeventnormalize.Key})
|
assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{pipeline.DefaultNormalizeModule, spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, itemoccurrencenormalize.Key, itemregistrynormalize.Key, npcoccurrencenormalize.Key, scenenormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, enemyeventnormalize.Key})
|
||||||
assertProductionContains(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"})
|
assertProductionContains(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"})
|
||||||
assertProductionContains(t, "validators", registries.Validators.RegisteredKeys(), []string{
|
assertProductionContains(t, "validators", registries.Validators.RegisteredKeys(), []string{
|
||||||
"extract/dnd/spells/catalog",
|
"extract/dnd/spells/catalog",
|
||||||
@@ -63,10 +80,37 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
|
|||||||
"extract/dnd/combat-turns/source_refs",
|
"extract/dnd/combat-turns/source_refs",
|
||||||
"extract/dnd/combat-turns/source_relatedness",
|
"extract/dnd/combat-turns/source_relatedness",
|
||||||
"normalize/dnd/combat-turns/invariants",
|
"normalize/dnd/combat-turns/invariants",
|
||||||
"extract/dnd/item-events/shape",
|
"extract/dnd/item-occurrences/shape",
|
||||||
"extract/dnd/item-events/source_refs",
|
"extract/dnd/item-occurrences/registry",
|
||||||
"extract/dnd/item-events/source_relatedness",
|
"extract/dnd/item-occurrences/source_refs",
|
||||||
"normalize/dnd/item-events/invariants",
|
"extract/dnd/item-occurrences/source_relatedness",
|
||||||
|
"normalize/dnd/item-occurrences/invariants",
|
||||||
|
"extract/dnd/item-registry/shape",
|
||||||
|
"extract/dnd/item-registry/source_refs",
|
||||||
|
"extract/dnd/item-registry/source_relatedness",
|
||||||
|
"normalize/dnd/item-registry/identity",
|
||||||
|
"extract/dnd/npc-registry/shape",
|
||||||
|
"extract/dnd/npc-registry/source_refs",
|
||||||
|
"extract/dnd/npc-registry/source_relatedness",
|
||||||
|
"normalize/dnd/npc-registry/identity",
|
||||||
|
"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",
|
||||||
|
"extract/dnd/scene-descriptions/shape",
|
||||||
|
"extract/dnd/scene-descriptions/source_refs",
|
||||||
|
"extract/dnd/scene-descriptions/source_relatedness",
|
||||||
|
"normalize/dnd/scene-descriptions/invariants",
|
||||||
|
"extract/dnd/location-registry/shape",
|
||||||
|
"extract/dnd/location-registry/source_refs",
|
||||||
|
"extract/dnd/location-registry/source_relatedness",
|
||||||
|
"normalize/dnd/location-registry/identity",
|
||||||
|
"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",
|
||||||
"extract/dnd/enemy-events/shape",
|
"extract/dnd/enemy-events/shape",
|
||||||
"extract/dnd/enemy-events/engagements",
|
"extract/dnd/enemy-events/engagements",
|
||||||
"extract/dnd/enemy-events/source_refs",
|
"extract/dnd/enemy-events/source_refs",
|
||||||
@@ -77,118 +121,21 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
|
|||||||
"generic/valid_json",
|
"generic/valid_json",
|
||||||
"generic/valid_json_schema",
|
"generic/valid_json_schema",
|
||||||
})
|
})
|
||||||
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
allArtifactKinds := []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.NPCOccurrenceListKind, dnd.CombatTurnListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind, dnd.ItemRegistryKind, dnd.ItemOccurrenceListKind, dnd.EnemyEventListKind}
|
||||||
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), allArtifactKinds)
|
||||||
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), allArtifactKinds)
|
||||||
|
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), allArtifactKinds)
|
||||||
assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind})
|
assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind})
|
||||||
assertProductionContains(t, "combat normalizer variants", registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
assertProductionContains(t, "combat normalizer variants", registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
||||||
assertProductionContains(t, "item event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind})
|
assertProductionContains(t, "item occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind})
|
||||||
|
assertProductionContains(t, "item registry normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemregistrynormalize.Key), []contracts.ArtifactKind{dnd.ItemRegistryKind})
|
||||||
|
assertProductionContains(t, "NPC registry normalizer variants", registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind})
|
||||||
|
assertProductionContains(t, "NPC occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(npcoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind})
|
||||||
|
assertProductionContains(t, "scene description normalizer variants", registries.Normalizers.RegisteredArtifactKinds(scenenormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind})
|
||||||
|
assertProductionContains(t, "location registry normalizer variants", registries.Normalizers.RegisteredArtifactKinds(locationnormalize.Key), []contracts.ArtifactKind{dnd.LocationRegistryKind})
|
||||||
|
assertProductionContains(t, "location occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(locationoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.LocationOccurrenceListKind})
|
||||||
assertProductionContains(t, "enemy event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
|
assertProductionContains(t, "enemy event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
|
||||||
|
|
||||||
wantChain := []pipeline.ModuleBinding{
|
|
||||||
pipeline.Binding("generic/valid_json"),
|
|
||||||
pipeline.Binding("extract/dnd/spells/shape"),
|
|
||||||
pipeline.Binding("extract/dnd/spells/catalog"),
|
|
||||||
pipeline.Binding("extract/dnd/spells/source_refs"),
|
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
|
||||||
pipeline.Binding("extract/dnd/spells/source_relatedness"),
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
|
|
||||||
t.Fatalf("spell validator chain = %#v, want %#v", got, wantChain)
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, spellnormalize.Key); !reflect.DeepEqual(got, wantChain) {
|
|
||||||
t.Fatalf("spell normalize validator chain = %#v, want %#v", got, wantChain)
|
|
||||||
}
|
|
||||||
combatExtractChain := []pipeline.ModuleBinding{
|
|
||||||
pipeline.Binding("generic/valid_json"),
|
|
||||||
pipeline.Binding("extract/dnd/combat-turns/shape"),
|
|
||||||
pipeline.Binding("extract/dnd/combat-turns/source_refs"),
|
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
|
||||||
pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
|
|
||||||
}
|
|
||||||
combatNormalizeChain := []pipeline.ModuleBinding{
|
|
||||||
pipeline.Binding("generic/valid_json"),
|
|
||||||
pipeline.Binding("extract/dnd/combat-turns/shape"),
|
|
||||||
pipeline.Binding("normalize/dnd/combat-turns/invariants"),
|
|
||||||
pipeline.Binding("extract/dnd/combat-turns/source_refs"),
|
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
|
||||||
pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, combatextract.Key); !reflect.DeepEqual(got, combatExtractChain) {
|
|
||||||
t.Fatalf("combat extract validator chain = %#v, want %#v", got, combatExtractChain)
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, combatNormalizeChain) {
|
|
||||||
t.Fatalf("combat normalize validator chain = %#v, want %#v", got, combatNormalizeChain)
|
|
||||||
}
|
|
||||||
itemEventExtractChain := []pipeline.ModuleBinding{
|
|
||||||
pipeline.Binding("generic/valid_json"),
|
|
||||||
pipeline.Binding("extract/dnd/item-events/shape"),
|
|
||||||
pipeline.Binding("extract/dnd/item-events/source_refs"),
|
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
|
||||||
pipeline.Binding("extract/dnd/item-events/source_relatedness"),
|
|
||||||
}
|
|
||||||
itemEventNormalizeChain := []pipeline.ModuleBinding{
|
|
||||||
pipeline.Binding("generic/valid_json"),
|
|
||||||
pipeline.Binding("extract/dnd/item-events/shape"),
|
|
||||||
pipeline.Binding("normalize/dnd/item-events/invariants"),
|
|
||||||
pipeline.Binding("extract/dnd/item-events/source_refs"),
|
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
|
||||||
pipeline.Binding("extract/dnd/item-events/source_relatedness"),
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemeventextract.Key); !reflect.DeepEqual(got, itemEventExtractChain) {
|
|
||||||
t.Fatalf("item event extract validator chain = %#v, want %#v", got, itemEventExtractChain)
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemeventnormalize.Key); !reflect.DeepEqual(got, itemEventNormalizeChain) {
|
|
||||||
t.Fatalf("item event normalize validator chain = %#v, want %#v", got, itemEventNormalizeChain)
|
|
||||||
}
|
|
||||||
enemyEventExtractChain := []pipeline.ModuleBinding{
|
|
||||||
pipeline.Binding("generic/valid_json"),
|
|
||||||
pipeline.Binding("extract/dnd/enemy-events/shape"),
|
|
||||||
pipeline.Binding("extract/dnd/enemy-events/engagements"),
|
|
||||||
pipeline.Binding("extract/dnd/enemy-events/source_refs"),
|
|
||||||
pipeline.Binding("generic/valid_json_schema"),
|
|
||||||
pipeline.Binding("extract/dnd/enemy-events/source_relatedness"),
|
|
||||||
}
|
|
||||||
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, enemyeventextract.Key); !reflect.DeepEqual(got, enemyEventExtractChain) {
|
|
||||||
t.Fatalf("enemy event extract validator chain = %#v, want %#v", got, enemyEventExtractChain)
|
|
||||||
}
|
|
||||||
|
|
||||||
assetNames := productionAssetNames(t, components.assets.PromptFS)
|
|
||||||
requiredAssets := []string{
|
|
||||||
"dnd.scenes/dnd.scenes.yaml",
|
|
||||||
"dnd.scenes/instructions.md",
|
|
||||||
"dnd.scenes/sharedassets/common-dnd-references.md",
|
|
||||||
"dnd.scenes/sharedassets/common-dnd-system.md",
|
|
||||||
"dnd.scenes/sharedassets/common-dnd-transcript.md",
|
|
||||||
"dnd.scenes/task.md",
|
|
||||||
"dnd.spells/dnd.spells.yaml",
|
|
||||||
"dnd.spells/catalog.md",
|
|
||||||
"dnd.spells/instructions.md",
|
|
||||||
"dnd.spells/sharedassets/common-dnd-references.md",
|
|
||||||
"dnd.spells/sharedassets/common-dnd-system.md",
|
|
||||||
"dnd.spells/sharedassets/common-dnd-transcript.md",
|
|
||||||
"dnd.spells/task.md",
|
|
||||||
"dnd.combat_turns/dnd.combat_turns.yaml",
|
|
||||||
"dnd.combat_turns/instructions.md",
|
|
||||||
"dnd.combat_turns/sharedassets/common-dnd-references.md",
|
|
||||||
"dnd.combat_turns/sharedassets/common-dnd-system.md",
|
|
||||||
"dnd.combat_turns/sharedassets/common-dnd-transcript.md",
|
|
||||||
"dnd.combat_turns/task.md",
|
|
||||||
"dnd.item_events/dnd.item_events.yaml",
|
|
||||||
"dnd.item_events/instructions.md",
|
|
||||||
"dnd.item_events/sharedassets/common-dnd-extraction-evidence.md",
|
|
||||||
"dnd.item_events/sharedassets/common-dnd-identity.md",
|
|
||||||
"dnd.item_events/sharedassets/common-dnd-references.md",
|
|
||||||
"dnd.item_events/sharedassets/common-dnd-system.md",
|
|
||||||
"dnd.item_events/sharedassets/common-dnd-transcript.md",
|
|
||||||
"dnd.item_events/task.md",
|
|
||||||
"dnd.enemy_events/dnd.enemy_events.yaml",
|
|
||||||
"dnd.enemy_events/grounding.md",
|
|
||||||
"dnd.enemy_events/instructions.md",
|
|
||||||
"dnd.enemy_events/task.md",
|
|
||||||
}
|
|
||||||
assertProductionContains(t, "production prompt assets", assetNames, requiredAssets)
|
|
||||||
|
|
||||||
catalog := catalogFromRegistries(registries)
|
catalog := catalogFromRegistries(registries)
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
stage pipeline.ModuleStage
|
stage pipeline.ModuleStage
|
||||||
@@ -199,19 +146,25 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
|
|||||||
{stage: pipeline.StageChunk, key: "generic", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageChunk, key: "generic", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageChunk, key: "dnd/scenes", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageChunk, key: "dnd/scenes", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: "dnd/spells", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: "dnd/spells", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: "dnd/npcs", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked},
|
||||||
|
{stage: pipeline.StageExtract, key: "dnd/location-registry", want: contracts.ExecutionClassLLMBacked},
|
||||||
|
{stage: pipeline.StageExtract, key: "dnd/item-registry", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: "dnd/combat-turns", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: "dnd/combat-turns", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: "dnd/item-events", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: "dnd/item-occurrences", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: "dnd/npc-interactions", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: "dnd/npc-occurrences", want: contracts.ExecutionClassLLMBacked},
|
||||||
|
{stage: pipeline.StageExtract, key: "dnd/location-occurrences", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: "dnd/scene-descriptions", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: "dnd/scene-descriptions", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageExtract, key: enemyeventextract.Key, want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageExtract, key: enemyeventextract.Key, want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageMerge, key: "appendorder", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageMerge, key: "appendorder", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: "noop", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: "noop", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: "dnd/spells", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: "dnd/spells", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: "dnd/npcs", want: contracts.ExecutionClassLLMBacked},
|
{stage: pipeline.StageNormalize, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked},
|
||||||
|
{stage: pipeline.StageNormalize, key: "dnd/location-registry", want: contracts.ExecutionClassLLMBacked},
|
||||||
|
{stage: pipeline.StageNormalize, key: "dnd/item-registry", want: contracts.ExecutionClassLLMBacked},
|
||||||
{stage: pipeline.StageNormalize, key: "dnd/combat-turns", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: "dnd/combat-turns", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: "dnd/item-events", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: "dnd/item-occurrences", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: "dnd/npc-interactions", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: "dnd/npc-occurrences", want: contracts.ExecutionClassDeterministic},
|
||||||
|
{stage: pipeline.StageNormalize, key: "dnd/location-occurrences", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: "dnd/scene-descriptions", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: "dnd/scene-descriptions", want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageNormalize, key: enemyeventnormalize.Key, want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageNormalize, key: enemyeventnormalize.Key, want: contracts.ExecutionClassDeterministic},
|
||||||
{stage: pipeline.StageOutput, key: "json", want: contracts.ExecutionClassDeterministic},
|
{stage: pipeline.StageOutput, key: "json", want: contracts.ExecutionClassDeterministic},
|
||||||
@@ -233,21 +186,31 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
|
|||||||
if !ok || combatCodecSpec.Kind != dnd.CombatTurnListKind || combatCodecSpec.Schema.ID != combatcodec.SchemaID {
|
if !ok || combatCodecSpec.Kind != dnd.CombatTurnListKind || combatCodecSpec.Schema.ID != combatcodec.SchemaID {
|
||||||
t.Fatalf("combat codec spec = %#v, ok=%t, want typed D&D combat codec", combatCodecSpec, ok)
|
t.Fatalf("combat codec spec = %#v, ok=%t, want typed D&D combat codec", combatCodecSpec, ok)
|
||||||
}
|
}
|
||||||
itemEventCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.ItemEventListKind)
|
itemOccurrenceCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.ItemOccurrenceListKind)
|
||||||
if !ok || itemEventCodecSpec.Kind != dnd.ItemEventListKind || itemEventCodecSpec.Schema.ID != itemeventcodec.SchemaID {
|
if !ok || itemOccurrenceCodecSpec.Kind != dnd.ItemOccurrenceListKind || itemOccurrenceCodecSpec.Schema.ID != itemoccurrencecodec.SchemaID {
|
||||||
t.Fatalf("item event codec spec = %#v, ok=%t, want typed D&D item-event codec", itemEventCodecSpec, ok)
|
t.Fatalf("item occurrence codec spec = %#v, ok=%t, want typed D&D item-occurrence codec", itemOccurrenceCodecSpec, ok)
|
||||||
|
}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
kind contracts.ArtifactKind
|
||||||
|
id string
|
||||||
|
}{
|
||||||
|
{name: "NPC registry", kind: dnd.NPCRegistryKind, id: npccodec.SchemaID},
|
||||||
|
{name: "NPC occurrences", kind: dnd.NPCOccurrenceListKind, id: npcoccurrencecodec.SchemaID},
|
||||||
|
{name: "item registry", kind: dnd.ItemRegistryKind, id: itemregistrycodec.SchemaID},
|
||||||
|
{name: "scene descriptions", kind: dnd.SceneDescriptionListKind, id: scenecodec.SchemaID},
|
||||||
|
{name: "location registry", kind: dnd.LocationRegistryKind, id: locationcodec.SchemaID},
|
||||||
|
{name: "location occurrences", kind: dnd.LocationOccurrenceListKind, id: locationoccurrencecodec.SchemaID},
|
||||||
|
} {
|
||||||
|
spec, found := catalog.ArtifactCodecs.Spec(test.kind)
|
||||||
|
if !found || spec.Kind != test.kind || spec.Schema.ID != test.id {
|
||||||
|
t.Fatalf("%s codec spec = %#v, found=%t, want typed D&D codec", test.name, spec, found)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
enemyEventCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.EnemyEventListKind)
|
enemyEventCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.EnemyEventListKind)
|
||||||
if !ok || enemyEventCodecSpec.Kind != dnd.EnemyEventListKind || enemyEventCodecSpec.Schema.ID != enemyeventcodec.SchemaID {
|
if !ok || enemyEventCodecSpec.Kind != dnd.EnemyEventListKind || enemyEventCodecSpec.Schema.ID != enemyeventcodec.SchemaID {
|
||||||
t.Fatalf("enemy event codec spec = %#v, ok=%t, want typed D&D enemy-event codec", enemyEventCodecSpec, ok)
|
t.Fatalf("enemy event codec spec = %#v, ok=%t, want typed D&D enemy-event codec", enemyEventCodecSpec, ok)
|
||||||
}
|
}
|
||||||
if got := catalog.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
|
|
||||||
t.Fatalf("catalog validator chain = %#v, want %#v", got, wantChain)
|
|
||||||
}
|
|
||||||
if got := catalog.ValidatorChains.Validators(pipeline.StageNormalize, spellnormalize.Key); !reflect.DeepEqual(got, wantChain) {
|
|
||||||
t.Fatalf("catalog spell normalize validator chain = %#v, want %#v", got, wantChain)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultCLICompositionValidatesRepresentativeConfiguration(t *testing.T) {
|
func TestDefaultCLICompositionValidatesRepresentativeConfiguration(t *testing.T) {
|
||||||
@@ -816,8 +779,8 @@ func TestProductionSceneRunRecordsAnnotationFreeChunkPlanAndProvenance(t *testin
|
|||||||
if len(warnings.Warnings) != 0 {
|
if len(warnings.Warnings) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want none", warnings.Warnings)
|
t.Fatalf("warnings = %#v, want none", warnings.Warnings)
|
||||||
}
|
}
|
||||||
if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 || len(fake.requestsFor(itemeventextract.PromptID)) != 1 {
|
if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 || len(fake.requestsFor(itemoccurrenceextract.PromptID)) != 1 {
|
||||||
t.Fatalf("fake prompt requests = %#v, want one scene, spell, and item-event request", fake.requestPrompts())
|
t.Fatalf("fake prompt requests = %#v, want one scene, spell, and item-occurrence request", fake.requestPrompts())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,11 +916,23 @@ pipelines:
|
|||||||
module: json
|
module: json
|
||||||
options:
|
options:
|
||||||
include_chunk_map: true
|
include_chunk_map: true
|
||||||
|
steps:
|
||||||
|
- id: identify-items
|
||||||
|
artifacts:
|
||||||
|
item-registry:
|
||||||
|
extract: dnd/item-registry
|
||||||
|
normalize: dnd/item-registry
|
||||||
|
- id: extract-occurrences
|
||||||
|
references:
|
||||||
|
item_registry:
|
||||||
|
artifact:
|
||||||
|
step: identify-items
|
||||||
|
lane: item-registry
|
||||||
artifacts:
|
artifacts:
|
||||||
spells:
|
spells:
|
||||||
extract: dnd/spells
|
extract: dnd/spells
|
||||||
item-events:
|
item-occurrences:
|
||||||
extract: dnd/item-events
|
extract: dnd/item-occurrences
|
||||||
`, outputRoot, filepath.Join(filepath.Dir(outputRoot), "debug"), chunkModule)
|
`, outputRoot, filepath.Join(filepath.Dir(outputRoot), "debug"), chunkModule)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,28 +959,6 @@ func writeProductionContractConfig(t *testing.T, content string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func productionAssetNames(t *testing.T, getFS func() (fs.FS, error)) []string {
|
|
||||||
t.Helper()
|
|
||||||
fileSystem, err := getFS()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("load production prompt assets: %v", err)
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
if err := fs.WalkDir(fileSystem, ".", func(path string, entry fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !entry.IsDir() {
|
|
||||||
names = append(names, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("walk production prompt assets: %v", err)
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertProductionContains[T comparable](t *testing.T, name string, got, required []T) {
|
func assertProductionContains[T comparable](t *testing.T, name string, got, required []T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
available := make(map[T]struct{}, len(got))
|
available := make(map[T]struct{}, len(got))
|
||||||
@@ -1056,8 +1009,12 @@ func (client *productionFakeLLMClient) CompleteStructured(ctx context.Context, r
|
|||||||
} else {
|
} else {
|
||||||
content = []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`)
|
content = []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`)
|
||||||
}
|
}
|
||||||
case itemeventextract.PromptID:
|
case itemoccurrenceextract.PromptID:
|
||||||
content = []byte(`{"events":[{"name":"Cure Wounds","kind":"acquired","to":"party","source_refs":[{"start_segment":1,"end_segment":1}]}]}`)
|
content = []byte(`{"occurrences":[]}`)
|
||||||
|
case itemregistryextract.PromptID:
|
||||||
|
content = []byte(`{"items":[]}`)
|
||||||
|
case itemregistrynormalize.PromptID:
|
||||||
|
content = []byte(`{"duplicate_groups":[]}`)
|
||||||
default:
|
default:
|
||||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected prompt %q", req.PromptID)
|
return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected prompt %q", req.PromptID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ func TestRunManifestIncludesTopLevelModuleMetadata(t *testing.T) {
|
|||||||
"prompt_id": "dnd.scenes",
|
"prompt_id": "dnd.scenes",
|
||||||
"prompt_version": "v1",
|
"prompt_version": "v1",
|
||||||
"prompt_sha256": "sha256:abc123",
|
"prompt_sha256": "sha256:abc123",
|
||||||
"response_schema_key": "dnd_scenes",
|
"response_schema_key": "dnd_scenes_llm",
|
||||||
"response_schema_name": "dnd_scenes",
|
"response_schema_name": "notarius_dnd_scenes_llm_v1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -639,14 +639,14 @@ pipelines:
|
|||||||
steps:
|
steps:
|
||||||
- id: identify-npcs
|
- id: identify-npcs
|
||||||
artifacts:
|
artifacts:
|
||||||
npcs:
|
npc_registry:
|
||||||
extract: dnd/npcs
|
extract: dnd/npc-registry
|
||||||
- id: grounded-events
|
- id: grounded-events
|
||||||
references:
|
references:
|
||||||
npcs:
|
npc_registry:
|
||||||
artifact:
|
artifact:
|
||||||
step: identify-npcs
|
step: identify-npcs
|
||||||
lane: npcs
|
lane: npc_registry
|
||||||
artifacts:
|
artifacts:
|
||||||
spells:
|
spells:
|
||||||
extract: dnd/spells
|
extract: dnd/spells
|
||||||
@@ -659,8 +659,8 @@ pipelines:
|
|||||||
if len(profile.Steps) != 2 || profile.Steps[0].ID != "identify-npcs" || profile.Steps[1].ID != "grounded-events" {
|
if len(profile.Steps) != 2 || profile.Steps[0].ID != "identify-npcs" || profile.Steps[1].ID != "grounded-events" {
|
||||||
t.Fatalf("steps = %#v", profile.Steps)
|
t.Fatalf("steps = %#v", profile.Steps)
|
||||||
}
|
}
|
||||||
source := profile.Steps[1].References["npcs"]
|
source := profile.Steps[1].References["npc_registry"]
|
||||||
if source.Artifact == nil || source.Artifact.Step != "identify-npcs" || source.Artifact.Lane != "npcs" {
|
if source.Artifact == nil || source.Artifact.Step != "identify-npcs" || source.Artifact.Lane != "npc_registry" {
|
||||||
t.Fatalf("generated source = %#v", source)
|
t.Fatalf("generated source = %#v", source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ func TestCloneReferenceSlotsCopiesAcceptedMediaTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloneReferenceSlotsCopiesAcceptedArtifactKinds(t *testing.T) {
|
func TestCloneReferenceSlotsCopiesAcceptedArtifactKinds(t *testing.T) {
|
||||||
slots := []ReferenceSlot{{Name: "npcs", AcceptedArtifactKinds: []ArtifactKind{"dnd/npc-list"}}}
|
slots := []ReferenceSlot{{Name: "npcs", AcceptedArtifactKinds: []ArtifactKind{"dnd/npc-registry"}}}
|
||||||
clone := CloneReferenceSlots(slots)
|
clone := CloneReferenceSlots(slots)
|
||||||
clone[0].AcceptedArtifactKinds[0] = "changed"
|
clone[0].AcceptedArtifactKinds[0] = "changed"
|
||||||
if slots[0].AcceptedArtifactKinds[0] != "dnd/npc-list" {
|
if slots[0].AcceptedArtifactKinds[0] != "dnd/npc-registry" {
|
||||||
t.Fatalf("source AcceptedArtifactKinds aliased clone: %#v", slots[0].AcceptedArtifactKinds)
|
t.Fatalf("source AcceptedArtifactKinds aliased clone: %#v", slots[0].AcceptedArtifactKinds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func TestLookupResponseSchemaUnknownReturnsFalse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLookupResponseSchemaDNDSpellsIsNotFrameworkRegistered(t *testing.T) {
|
func TestLookupResponseSchemaDNDSpellsLLMIsNotFrameworkRegistered(t *testing.T) {
|
||||||
if schema, ok := LookupResponseSchema("dnd_spells"); ok {
|
if schema, ok := LookupResponseSchema("dnd_spells_llm"); ok {
|
||||||
t.Fatalf("expected D&D spells schema lookup to fail in framework registry, got %+v", schema)
|
t.Fatalf("expected D&D spells schema lookup to fail in framework registry, got %+v", schema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ const promptAssetRoot = "assets/prompts"
|
|||||||
var promptAssetManifest = shared.PromptAssetManifest{
|
var promptAssetManifest = shared.PromptAssetManifest{
|
||||||
ModuleDir: "dnd.scenes",
|
ModuleDir: "dnd.scenes",
|
||||||
ModuleFiles: []promptfs.ModulePromptFile{
|
ModuleFiles: []promptfs.ModulePromptFile{
|
||||||
{Name: "dnd.scenes.yaml", Path: "prompts/dnd.scenes.yaml"},
|
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
|
||||||
{Name: "task.md", Path: "prompts/task.md"},
|
|
||||||
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
||||||
},
|
},
|
||||||
SharedFiles: []string{
|
SharedFiles: []string{
|
||||||
"common-dnd-system.md",
|
"common-dnd-system.md",
|
||||||
"common-dnd-transcript.md",
|
"common-dnd-transcript-full.md",
|
||||||
"common-dnd-references.md",
|
"common-dnd-references.md",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package scenes
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,42 +12,52 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/promptkit"
|
"gitea.maximumdirect.net/eric/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPromptAssetsPrepareTranscriptAndTaskMessages(t *testing.T) {
|
func TestPromptAssetsPrepareTranscriptWithInstructionOrdering(t *testing.T) {
|
||||||
|
if !slices.Contains(promptAssetManifest.SharedFiles, "common-dnd-transcript-full.md") {
|
||||||
|
t.Fatalf("shared prompt assets = %#v, want complete-session transcript scope", promptAssetManifest.SharedFiles)
|
||||||
|
}
|
||||||
transcript := []byte(`{"sentinel":"scene-transcript"}`)
|
transcript := []byte(`{"sentinel":"scene-transcript"}`)
|
||||||
prepared := prepareScenesPrompt(t, transcript, "scene-players", "scene-party", "scene-glossary")
|
prepared := prepareScenesPrompt(t, transcript, "scene-players", "scene-party", "scene-glossary")
|
||||||
|
|
||||||
if prepared.PromptID != PromptID {
|
if prepared.PromptID != PromptID {
|
||||||
t.Fatalf("prompt id = %q, want %q", prepared.PromptID, PromptID)
|
t.Fatalf("prompt id = %q, want %q", prepared.PromptID, PromptID)
|
||||||
}
|
}
|
||||||
if len(prepared.Messages) != 5 {
|
if prepared.OutputContract.SchemaPath != "dnd_scenes_llm.v1.json" {
|
||||||
t.Fatalf("prepared messages = %d, want 5", len(prepared.Messages))
|
t.Fatalf("scene output schema path = %q, want private LLM schema", prepared.OutputContract.SchemaPath)
|
||||||
}
|
}
|
||||||
for index, role := range []string{"system", "user", "user", "user", "user"} {
|
if prepared.Messages[0].Role != "system" || prepared.Messages[1].Role != "user" {
|
||||||
if prepared.Messages[index].Role != role {
|
t.Fatalf("initial prompt roles = %#v, want system followed by campaign references", prepared.Messages[:2])
|
||||||
t.Errorf("message %d role = %q, want %q", index, prepared.Messages[index].Role, role)
|
|
||||||
}
|
}
|
||||||
|
if cache := prepared.Messages[1].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||||
|
t.Fatalf("campaign-reference cache control = %#v, want ephemeral", cache)
|
||||||
}
|
}
|
||||||
for _, index := range []int{1, 4} {
|
if cache := prepared.Messages[0].CacheControl; cache != nil {
|
||||||
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
t.Fatalf("system cache control = %#v, want nil", cache)
|
||||||
t.Errorf("message %d cache control = %#v, want ephemeral", index, cache)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, index := range []int{0, 2, 3} {
|
|
||||||
if cache := prepared.Messages[index].CacheControl; cache != nil {
|
|
||||||
t.Errorf("message %d cache control = %#v, want nil", index, cache)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if references := prepared.Messages[1].Content; !strings.Contains(references, "scene-players") || !strings.Contains(references, "scene-party") || !strings.Contains(references, "scene-glossary") {
|
if references := prepared.Messages[1].Content; !strings.Contains(references, "scene-players") || !strings.Contains(references, "scene-party") || !strings.Contains(references, "scene-glossary") {
|
||||||
t.Fatalf("reference message = %q, want supplied reference inputs", references)
|
t.Fatalf("reference message = %q, want supplied reference inputs", references)
|
||||||
}
|
}
|
||||||
if transcriptMessage := prepared.Messages[4].Content; !strings.Contains(transcriptMessage, "scene-transcript") {
|
transcriptIndex := -1
|
||||||
t.Fatalf("final message = %q, want transcript input", transcriptMessage)
|
for index, message := range prepared.Messages {
|
||||||
|
if strings.Contains(message.Content, "scene-transcript") {
|
||||||
|
transcriptIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transcriptIndex != len(prepared.Messages)-1 {
|
||||||
|
t.Fatalf("transcript message index = %d, want final message in %#v", transcriptIndex, prepared.Messages)
|
||||||
|
}
|
||||||
|
if cache := prepared.Messages[transcriptIndex].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||||
|
t.Fatalf("transcript cache control = %#v, want ephemeral", cache)
|
||||||
|
}
|
||||||
|
instructionsIndex := transcriptIndex - 1
|
||||||
|
if instructionsIndex <= 1 || prepared.Messages[instructionsIndex].CacheControl != nil {
|
||||||
|
t.Fatalf("instruction message = %#v, want uncached message after references and before transcript", prepared.Messages[instructionsIndex])
|
||||||
}
|
}
|
||||||
for index, message := range prepared.Messages {
|
for index, message := range prepared.Messages {
|
||||||
if index != 1 && (strings.Contains(message.Content, "scene-players") || strings.Contains(message.Content, "scene-party") || strings.Contains(message.Content, "scene-glossary")) {
|
if index != 1 && (strings.Contains(message.Content, "scene-players") || strings.Contains(message.Content, "scene-party") || strings.Contains(message.Content, "scene-glossary")) {
|
||||||
t.Errorf("message %d unexpectedly rendered reference input", index)
|
t.Errorf("message %d unexpectedly rendered reference input", index)
|
||||||
}
|
}
|
||||||
if index != 4 && strings.Contains(message.Content, "scene-transcript") {
|
if index != transcriptIndex && strings.Contains(message.Content, "scene-transcript") {
|
||||||
t.Errorf("message %d unexpectedly rendered transcript input", index)
|
t.Errorf("message %d unexpectedly rendered transcript input", index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
PromptID = "dnd.scenes"
|
PromptID = "dnd.scenes"
|
||||||
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_scenes")
|
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_scenes_llm")
|
||||||
ResponseSchemaID = "notarius.dnd.scenes"
|
ResponseSchemaID = "notarius.dnd.scenes.llm"
|
||||||
ResponseSchemaVersion = "v1"
|
ResponseSchemaVersion = "v1"
|
||||||
ResponseSchemaName = "notarius_dnd_scenes_v1"
|
ResponseSchemaName = "notarius_dnd_scenes_llm_v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadResponseSchema() (llm.ResponseSchema, error) {
|
func loadResponseSchema() (llm.ResponseSchema, error) {
|
||||||
@@ -20,6 +20,6 @@ func loadResponseSchema() (llm.ResponseSchema, error) {
|
|||||||
ID: ResponseSchemaID,
|
ID: ResponseSchemaID,
|
||||||
Version: ResponseSchemaVersion,
|
Version: ResponseSchemaVersion,
|
||||||
Name: ResponseSchemaName,
|
Name: ResponseSchemaName,
|
||||||
AssetPath: "schemas/dnd_scenes.v1.json",
|
AssetPath: "schemas/dnd_scenes_llm.v1.json",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ func TestLoadResponseSchemaForScenes(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
if schema.Key != ResponseSchemaKey {
|
if got, want := string(schema.Key), "dnd_scenes_llm"; got != want {
|
||||||
t.Fatalf("schema.Key = %q, want %q", schema.Key, ResponseSchemaKey)
|
t.Fatalf("schema.Key = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
if schema.ID != ResponseSchemaID {
|
if got, want := schema.ID, "notarius.dnd.scenes.llm"; got != want {
|
||||||
t.Fatalf("schema.ID = %q, want %q", schema.ID, ResponseSchemaID)
|
t.Fatalf("schema.ID = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
if schema.Version != ResponseSchemaVersion {
|
if got, want := schema.Version, "v1"; got != want {
|
||||||
t.Fatalf("schema.Version = %q, want %q", schema.Version, ResponseSchemaVersion)
|
t.Fatalf("schema.Version = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
if schema.Name != ResponseSchemaName {
|
if got, want := schema.Name, "notarius_dnd_scenes_llm_v1"; got != want {
|
||||||
t.Fatalf("schema.Name = %q, want %q", schema.Name, ResponseSchemaName)
|
t.Fatalf("schema.Name = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(schema.SHA256, "sha256:") {
|
if !strings.HasPrefix(schema.SHA256, "sha256:") {
|
||||||
t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256)
|
t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256)
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
// Package itemevents encodes durable D&D item-event artifacts.
|
|
||||||
package itemevents
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SchemaID = "notarius.dnd.item_events"
|
|
||||||
SchemaName = "notarius_dnd_item_events_v1"
|
|
||||||
SchemaVersion = "v1"
|
|
||||||
MediaType = "application/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/schemas/dnd_item_events.v1.json
|
|
||||||
var schemaAssets embed.FS
|
|
||||||
|
|
||||||
var _ contracts.ArtifactCodec[dnd.ItemEventList] = (*Codec)(nil)
|
|
||||||
|
|
||||||
type Codec struct{}
|
|
||||||
|
|
||||||
func New() *Codec { return &Codec{} }
|
|
||||||
|
|
||||||
func (c *Codec) Kind() contracts.ArtifactKind { return dnd.ItemEventListKind }
|
|
||||||
|
|
||||||
func (c *Codec) Schema() contracts.ArtifactSchema {
|
|
||||||
raw, err := schemaAssets.ReadFile("assets/schemas/dnd_item_events.v1.json")
|
|
||||||
if err != nil {
|
|
||||||
return contracts.ArtifactSchema{}
|
|
||||||
}
|
|
||||||
return contracts.ArtifactSchema{
|
|
||||||
ID: SchemaID,
|
|
||||||
Name: SchemaName,
|
|
||||||
Version: SchemaVersion,
|
|
||||||
JSONSchema: append([]byte(nil), raw...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) MediaType() string { return MediaType }
|
|
||||||
|
|
||||||
func (c *Codec) Metadata(value dnd.ItemEventList) map[string]any {
|
|
||||||
return map[string]any{"event_count": len(value.Events)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) Encode(value dnd.ItemEventList) ([]byte, error) {
|
|
||||||
if err := validate(value); err != nil {
|
|
||||||
return nil, fmt.Errorf("encode dnd item event list: %w", err)
|
|
||||||
}
|
|
||||||
return c.EncodeCandidate(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeCandidate provides the durable representation before semantic
|
|
||||||
// validators have approved a value.
|
|
||||||
func (c *Codec) EncodeCandidate(value dnd.ItemEventList) ([]byte, error) {
|
|
||||||
return candidatejson.EncodeCandidate("dnd item event list", cloneList(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) Decode(content []byte) (dnd.ItemEventList, error) {
|
|
||||||
value, err := c.DecodeCandidate(content)
|
|
||||||
if err != nil {
|
|
||||||
return dnd.ItemEventList{}, err
|
|
||||||
}
|
|
||||||
if err := validate(value); err != nil {
|
|
||||||
return dnd.ItemEventList{}, fmt.Errorf("decode dnd item event list: %w", err)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeCandidate reads one strict durable JSON value before semantic
|
|
||||||
// validators have approved it.
|
|
||||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.ItemEventList, error) {
|
|
||||||
value, err := candidatejson.DecodeCandidate[dnd.ItemEventList]("dnd item event list", content)
|
|
||||||
if err != nil {
|
|
||||||
return dnd.ItemEventList{}, err
|
|
||||||
}
|
|
||||||
return cloneList(value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validate(value dnd.ItemEventList) error {
|
|
||||||
if value.Events == nil {
|
|
||||||
return fmt.Errorf("events must be present")
|
|
||||||
}
|
|
||||||
for index, event := range value.Events {
|
|
||||||
prefix := fmt.Sprintf("events[%d]", index)
|
|
||||||
if strings.TrimSpace(event.Name) == "" {
|
|
||||||
return fmt.Errorf("%s.name must not be empty", prefix)
|
|
||||||
}
|
|
||||||
if !itemevents.SupportedKind(event.Kind) {
|
|
||||||
return fmt.Errorf("%s.kind must be supported", prefix)
|
|
||||||
}
|
|
||||||
if event.From != "" && strings.TrimSpace(event.From) == "" {
|
|
||||||
return fmt.Errorf("%s.from must not be empty when present", prefix)
|
|
||||||
}
|
|
||||||
if event.To != "" && strings.TrimSpace(event.To) == "" {
|
|
||||||
return fmt.Errorf("%s.to must not be empty when present", prefix)
|
|
||||||
}
|
|
||||||
if !itemevents.ValidHolderCombination(event.Kind, event.From, event.To) {
|
|
||||||
return fmt.Errorf("%s holders are incompatible with %q", prefix, event.Kind)
|
|
||||||
}
|
|
||||||
if event.Quantity != nil && *event.Quantity < 1 {
|
|
||||||
return fmt.Errorf("%s.quantity must be positive when present", prefix)
|
|
||||||
}
|
|
||||||
if len(event.SourceRefs) == 0 {
|
|
||||||
return fmt.Errorf("%s.source_refs must contain at least one reference", prefix)
|
|
||||||
}
|
|
||||||
for refIndex, ref := range event.SourceRefs {
|
|
||||||
refPrefix := fmt.Sprintf("%s.source_refs[%d]", prefix, refIndex)
|
|
||||||
if strings.TrimSpace(ref.SourceID) == "" {
|
|
||||||
return fmt.Errorf("%s.source_id must not be empty", refPrefix)
|
|
||||||
}
|
|
||||||
if ref.StartUnitID <= 0 {
|
|
||||||
return fmt.Errorf("%s.start_unit_id must be positive", refPrefix)
|
|
||||||
}
|
|
||||||
if ref.EndUnitID <= 0 {
|
|
||||||
return fmt.Errorf("%s.end_unit_id must be positive", refPrefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneList(value dnd.ItemEventList) dnd.ItemEventList {
|
|
||||||
if value.Events == nil {
|
|
||||||
return dnd.ItemEventList{}
|
|
||||||
}
|
|
||||||
cloned := dnd.ItemEventList{Events: make([]dnd.ItemEvent, len(value.Events))}
|
|
||||||
for index, event := range value.Events {
|
|
||||||
cloned.Events[index] = event
|
|
||||||
if event.Quantity != nil {
|
|
||||||
quantity := *event.Quantity
|
|
||||||
cloned.Events[index].Quantity = &quantity
|
|
||||||
}
|
|
||||||
if event.SourceRefs != nil {
|
|
||||||
cloned.Events[index].SourceRefs = append([]source.SourceRef(nil), event.SourceRefs...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cloned
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.item_events",
|
"$id": "notarius.dnd.item_occurrences",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["events"],
|
"required": ["occurrences"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"events": {
|
"occurrences": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["name", "kind", "source_refs"],
|
"required": ["item_id", "name", "kind", "source_refs"],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"item_id": {"type": "string", "minLength": 1},
|
||||||
"name": {"type": "string", "minLength": 1},
|
"name": {"type": "string", "minLength": 1},
|
||||||
"kind": {"type": "string", "enum": ["discovered", "acquired", "lost", "consumed", "transferred"]},
|
"kind": {"type": "string", "enum": ["discovered", "acquired", "lost", "consumed", "transferred"]},
|
||||||
"quantity": {"type": "integer", "minimum": 1},
|
"quantity": {"type": "integer", "minimum": 1},
|
||||||
149
internal/modules/dnd/codec/itemoccurrences/codec.go
Normal file
149
internal/modules/dnd/codec/itemoccurrences/codec.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Package itemoccurrences encodes durable D&D item-occurrence artifacts.
|
||||||
|
package itemoccurrences
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemaID = "notarius.dnd.item_occurrences"
|
||||||
|
SchemaName = "notarius_dnd_item_occurrences_v1"
|
||||||
|
SchemaVersion = "v1"
|
||||||
|
MediaType = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/schemas/dnd_item_occurrences.v1.json
|
||||||
|
var schemaAssets embed.FS
|
||||||
|
|
||||||
|
var _ contracts.ArtifactCodec[dnd.ItemOccurrenceList] = (*Codec)(nil)
|
||||||
|
|
||||||
|
type Codec struct{}
|
||||||
|
|
||||||
|
func New() *Codec { return &Codec{} }
|
||||||
|
|
||||||
|
func (c *Codec) Kind() contracts.ArtifactKind { return dnd.ItemOccurrenceListKind }
|
||||||
|
|
||||||
|
func (c *Codec) Schema() contracts.ArtifactSchema {
|
||||||
|
raw, err := schemaAssets.ReadFile("assets/schemas/dnd_item_occurrences.v1.json")
|
||||||
|
if err != nil {
|
||||||
|
return contracts.ArtifactSchema{}
|
||||||
|
}
|
||||||
|
return contracts.ArtifactSchema{
|
||||||
|
ID: SchemaID,
|
||||||
|
Name: SchemaName,
|
||||||
|
Version: SchemaVersion,
|
||||||
|
JSONSchema: append([]byte(nil), raw...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) MediaType() string { return MediaType }
|
||||||
|
|
||||||
|
func (c *Codec) Metadata(value dnd.ItemOccurrenceList) map[string]any {
|
||||||
|
return map[string]any{"occurrence_count": len(value.Occurrences)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) Encode(value dnd.ItemOccurrenceList) ([]byte, error) {
|
||||||
|
if err := validate(value); err != nil {
|
||||||
|
return nil, fmt.Errorf("encode dnd item occurrence list: %w", err)
|
||||||
|
}
|
||||||
|
return c.EncodeCandidate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeCandidate provides the durable representation before semantic
|
||||||
|
// validators have approved a value.
|
||||||
|
func (c *Codec) EncodeCandidate(value dnd.ItemOccurrenceList) ([]byte, error) {
|
||||||
|
return candidatejson.EncodeCandidate("dnd item occurrence list", cloneList(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) Decode(content []byte) (dnd.ItemOccurrenceList, error) {
|
||||||
|
value, err := c.DecodeCandidate(content)
|
||||||
|
if err != nil {
|
||||||
|
return dnd.ItemOccurrenceList{}, err
|
||||||
|
}
|
||||||
|
if err := validate(value); err != nil {
|
||||||
|
return dnd.ItemOccurrenceList{}, fmt.Errorf("decode dnd item occurrence list: %w", err)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeCandidate reads one strict durable JSON value before semantic
|
||||||
|
// validators have approved it.
|
||||||
|
func (c *Codec) DecodeCandidate(content []byte) (dnd.ItemOccurrenceList, error) {
|
||||||
|
value, err := candidatejson.DecodeCandidate[dnd.ItemOccurrenceList]("dnd item occurrence list", content)
|
||||||
|
if err != nil {
|
||||||
|
return dnd.ItemOccurrenceList{}, err
|
||||||
|
}
|
||||||
|
return cloneList(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(value dnd.ItemOccurrenceList) error {
|
||||||
|
if value.Occurrences == nil {
|
||||||
|
return fmt.Errorf("occurrences must be present")
|
||||||
|
}
|
||||||
|
for index, occurrence := range value.Occurrences {
|
||||||
|
prefix := fmt.Sprintf("occurrences[%d]", index)
|
||||||
|
if strings.TrimSpace(occurrence.ItemID) == "" {
|
||||||
|
return fmt.Errorf("%s.item_id must not be empty", prefix)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(occurrence.Name) == "" {
|
||||||
|
return fmt.Errorf("%s.name must not be empty", prefix)
|
||||||
|
}
|
||||||
|
if !itemoccurrences.SupportedKind(occurrence.Kind) {
|
||||||
|
return fmt.Errorf("%s.kind must be supported", prefix)
|
||||||
|
}
|
||||||
|
if occurrence.From != "" && strings.TrimSpace(occurrence.From) == "" {
|
||||||
|
return fmt.Errorf("%s.from must not be empty when present", prefix)
|
||||||
|
}
|
||||||
|
if occurrence.To != "" && strings.TrimSpace(occurrence.To) == "" {
|
||||||
|
return fmt.Errorf("%s.to must not be empty when present", prefix)
|
||||||
|
}
|
||||||
|
if !itemoccurrences.ValidHolderCombination(occurrence.Kind, occurrence.From, occurrence.To) {
|
||||||
|
return fmt.Errorf("%s holders are incompatible with %q", prefix, occurrence.Kind)
|
||||||
|
}
|
||||||
|
if occurrence.Quantity != nil && *occurrence.Quantity < 1 {
|
||||||
|
return fmt.Errorf("%s.quantity must be positive when present", prefix)
|
||||||
|
}
|
||||||
|
if len(occurrence.SourceRefs) == 0 {
|
||||||
|
return fmt.Errorf("%s.source_refs must contain at least one reference", prefix)
|
||||||
|
}
|
||||||
|
for refIndex, ref := range occurrence.SourceRefs {
|
||||||
|
refPrefix := fmt.Sprintf("%s.source_refs[%d]", prefix, refIndex)
|
||||||
|
if strings.TrimSpace(ref.SourceID) == "" {
|
||||||
|
return fmt.Errorf("%s.source_id must not be empty", refPrefix)
|
||||||
|
}
|
||||||
|
if ref.StartUnitID <= 0 {
|
||||||
|
return fmt.Errorf("%s.start_unit_id must be positive", refPrefix)
|
||||||
|
}
|
||||||
|
if ref.EndUnitID <= 0 {
|
||||||
|
return fmt.Errorf("%s.end_unit_id must be positive", refPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneList(value dnd.ItemOccurrenceList) dnd.ItemOccurrenceList {
|
||||||
|
if value.Occurrences == nil {
|
||||||
|
return dnd.ItemOccurrenceList{}
|
||||||
|
}
|
||||||
|
cloned := dnd.ItemOccurrenceList{Occurrences: make([]dnd.ItemOccurrence, len(value.Occurrences))}
|
||||||
|
for index, occurrence := range value.Occurrences {
|
||||||
|
cloned.Occurrences[index] = occurrence
|
||||||
|
if occurrence.Quantity != nil {
|
||||||
|
quantity := *occurrence.Quantity
|
||||||
|
cloned.Occurrences[index].Quantity = &quantity
|
||||||
|
}
|
||||||
|
if occurrence.SourceRefs != nil {
|
||||||
|
cloned.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), occurrence.SourceRefs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package itemevents
|
package itemoccurrences
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -14,14 +14,13 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validList() dnd.ItemEventList {
|
func validList() dnd.ItemOccurrenceList {
|
||||||
quantity := 12
|
quantity := 12
|
||||||
return dnd.ItemEventList{Events: []dnd.ItemEvent{
|
return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{{ItemID: "item", Name: "Hidden Cache", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: refs(1, 1)},
|
||||||
{Name: "Hidden Cache", Kind: dnd.ItemEventKindDiscovered, SourceRefs: refs(1, 1)},
|
{ItemID: "item", Name: "Gold Pieces", Kind: dnd.ItemOccurrenceKindAcquired, Quantity: &quantity, To: "party", SourceRefs: refs(2, 2)},
|
||||||
{Name: "Gold Pieces", Kind: dnd.ItemEventKindAcquired, Quantity: &quantity, To: "party", SourceRefs: refs(2, 2)},
|
{ItemID: "item", Name: "Torch", Kind: dnd.ItemOccurrenceKindLost, From: "party", SourceRefs: refs(3, 3)},
|
||||||
{Name: "Torch", Kind: dnd.ItemEventKindLost, From: "party", SourceRefs: refs(3, 3)},
|
{ItemID: "item", Name: "Healing Potion", Kind: dnd.ItemOccurrenceKindConsumed, From: "party", SourceRefs: refs(4, 4)},
|
||||||
{Name: "Healing Potion", Kind: dnd.ItemEventKindConsumed, From: "party", SourceRefs: refs(4, 4)},
|
{ItemID: "item", Name: "Moonblade", Kind: dnd.ItemOccurrenceKindTransferred, From: "Aria", To: "Borin", SourceRefs: refs(5, 5)},
|
||||||
{Name: "Moonblade", Kind: dnd.ItemEventKindTransferred, From: "Aria", To: "Borin", SourceRefs: refs(5, 5)},
|
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +41,18 @@ func TestCodecRoundTripAndIdentities(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schema := codec.Schema()
|
schema := codec.Schema()
|
||||||
if codec.Kind() != dnd.ItemEventListKind || codec.MediaType() != MediaType || schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) {
|
if codec.Kind() != dnd.ItemOccurrenceListKind || codec.MediaType() != MediaType || schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) {
|
||||||
t.Fatalf("codec identity/schema = %q/%q %#v", codec.Kind(), codec.MediaType(), schema)
|
t.Fatalf("codec identity/schema = %q/%q %#v", codec.Kind(), codec.MediaType(), schema)
|
||||||
}
|
}
|
||||||
registry := pipeline.NewArtifactCodecRegistry()
|
registry := pipeline.NewArtifactCodecRegistry()
|
||||||
if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil {
|
if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
spec, ok := registry.Spec(dnd.ItemEventListKind)
|
spec, ok := registry.Spec(dnd.ItemOccurrenceListKind)
|
||||||
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
||||||
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
||||||
}
|
}
|
||||||
if _, err := registry.Encode(dnd.ItemEventListKind, dnd.NPCList{}); err == nil {
|
if _, err := registry.Encode(dnd.ItemOccurrenceListKind, dnd.NPCRegistry{}); err == nil {
|
||||||
t.Fatal("Encode() error = nil, want exact type rejection")
|
t.Fatal("Encode() error = nil, want exact type rejection")
|
||||||
} else {
|
} else {
|
||||||
var typeErr *pipeline.ArtifactCodecTypeError
|
var typeErr *pipeline.ArtifactCodecTypeError
|
||||||
@@ -65,13 +64,13 @@ func TestCodecRoundTripAndIdentities(t *testing.T) {
|
|||||||
|
|
||||||
func TestCodecSupportsEmptyListAndPreservesInvalidCandidates(t *testing.T) {
|
func TestCodecSupportsEmptyListAndPreservesInvalidCandidates(t *testing.T) {
|
||||||
codec := New()
|
codec := New()
|
||||||
empty := dnd.ItemEventList{Events: []dnd.ItemEvent{}}
|
empty := dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{}}
|
||||||
if content, err := codec.Encode(empty); err != nil || string(content) != `{"events":[]}` {
|
if content, err := codec.Encode(empty); err != nil || string(content) != `{"occurrences":[]}` {
|
||||||
t.Fatalf("Encode() = %s, %v", content, err)
|
t.Fatalf("Encode() = %s, %v", content, err)
|
||||||
}
|
}
|
||||||
zero := 0
|
zero := 0
|
||||||
candidate := dnd.ItemEventList{Events: []dnd.ItemEvent{{
|
candidate := dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{{
|
||||||
Name: " ", Kind: dnd.ItemEventKindTransferred, Quantity: &zero, From: "party", To: "Party",
|
Name: " ", Kind: dnd.ItemOccurrenceKindTransferred, Quantity: &zero, From: "party", To: "Party",
|
||||||
SourceRefs: []source.SourceRef{{SourceID: "", StartUnitID: 0, EndUnitID: -1}},
|
SourceRefs: []source.SourceRef{{SourceID: "", StartUnitID: 0, EndUnitID: -1}},
|
||||||
}}}
|
}}}
|
||||||
content, err := codec.EncodeCandidate(candidate)
|
content, err := codec.EncodeCandidate(candidate)
|
||||||
@@ -88,16 +87,16 @@ func TestCodecSupportsEmptyListAndPreservesInvalidCandidates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCodecRejectsStrictJSONAndApprovedBoundaries(t *testing.T) {
|
func TestCodecRejectsStrictJSONAndApprovedBoundaries(t *testing.T) {
|
||||||
validJSON := `{"events":[{"name":"Ring","kind":"acquired","to":"party","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]}]}`
|
validJSON := `{"occurrences":[{"item_id":"ring","name":"Ring","kind":"acquired","to":"party","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]}]}`
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name, raw, want string
|
name, raw, want string
|
||||||
}{
|
}{
|
||||||
{"malformed", `{`, "decode dnd item event list"},
|
{"malformed", `{`, "decode dnd item occurrence list"},
|
||||||
{"unknown top level", `{"events":[],"unexpected":true}`, "unknown field"},
|
{"unknown top level", `{"occurrences":[],"unexpected":true}`, "unknown field"},
|
||||||
{"unknown event field", strings.Replace(validJSON, `"to":"party"`, `"to":"party","unexpected":true`, 1), "unknown field"},
|
{"unknown occurrence field", strings.Replace(validJSON, `"to":"party"`, `"to":"party","unexpected":true`, 1), "unknown field"},
|
||||||
{"unknown reference field", strings.Replace(validJSON, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"},
|
{"unknown reference field", strings.Replace(validJSON, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"},
|
||||||
{"trailing", `{"events":[]} {}`, "multiple JSON values"},
|
{"trailing", `{"occurrences":[]} {}`, "multiple JSON values"},
|
||||||
{"missing list", `{}`, "events must be present"},
|
{"missing list", `{}`, "occurrences must be present"},
|
||||||
{"zero quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":0,"to":"party"`, 1), "quantity must be positive"},
|
{"zero quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":0,"to":"party"`, 1), "quantity must be positive"},
|
||||||
{"negative quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":-1,"to":"party"`, 1), "quantity must be positive"},
|
{"negative quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":-1,"to":"party"`, 1), "quantity must be positive"},
|
||||||
{"party transfer", strings.Replace(validJSON, `"kind":"acquired","to":"party"`, `"kind":"transferred","from":"party","to":"Borin"`, 1), "holders are incompatible"},
|
{"party transfer", strings.Replace(validJSON, `"kind":"acquired","to":"party"`, `"kind":"transferred","from":"party","to":"Borin"`, 1), "holders are incompatible"},
|
||||||
@@ -125,13 +124,13 @@ func TestCodecDeepCopiesBoundaryValuesAndMetadata(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if decoded.Events[1].Quantity == value.Events[1].Quantity || &decoded.Events[1].SourceRefs[0] == &value.Events[1].SourceRefs[0] {
|
if decoded.Occurrences[1].Quantity == value.Occurrences[1].Quantity || &decoded.Occurrences[1].SourceRefs[0] == &value.Occurrences[1].SourceRefs[0] {
|
||||||
t.Fatal("DecodeCandidate() retained caller-owned event fields")
|
t.Fatal("DecodeCandidate() retained caller-owned occurrence fields")
|
||||||
}
|
}
|
||||||
*decoded.Events[1].Quantity = 99
|
*decoded.Occurrences[1].Quantity = 99
|
||||||
decoded.Events[1].SourceRefs[0].SourceID = "changed"
|
decoded.Occurrences[1].SourceRefs[0].SourceID = "changed"
|
||||||
if *value.Events[1].Quantity != 12 || value.Events[1].SourceRefs[0].SourceID != "session" {
|
if *value.Occurrences[1].Quantity != 12 || value.Occurrences[1].SourceRefs[0].SourceID != "session" {
|
||||||
t.Fatal("decoded item event aliases input")
|
t.Fatal("decoded item occurrence aliases input")
|
||||||
}
|
}
|
||||||
|
|
||||||
first := codec.Schema()
|
first := codec.Schema()
|
||||||
@@ -141,7 +140,7 @@ func TestCodecDeepCopiesBoundaryValuesAndMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
metadata := codec.Metadata(value)
|
metadata := codec.Metadata(value)
|
||||||
metadata["payload"] = bytes.Repeat([]byte("x"), 10)
|
metadata["payload"] = bytes.Repeat([]byte("x"), 10)
|
||||||
if next := codec.Metadata(value); len(next) != 1 || next["event_count"] != len(value.Events) {
|
if next := codec.Metadata(value); len(next) != 1 || next["occurrence_count"] != len(value.Occurrences) {
|
||||||
t.Fatalf("Metadata() = %#v", next)
|
t.Fatalf("Metadata() = %#v", next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user