Move NPC occurrences to their canonical namespace

This commit is contained in:
2026-08-05 19:00:55 +00:00
parent 5e2ccffc0f
commit 2f61118e78
59 changed files with 518 additions and 518 deletions

View File

@@ -4,7 +4,7 @@ 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 location registries and occurrences, scene
descriptions, item and currency events, NPC identities, combat turns, NPC descriptions, item and currency events, NPC identities, combat turns, NPC
interactions, enemy events, and spell casts. occurrences, enemy events, and spell casts.
## Quickstart ## Quickstart

View File

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

View File

@@ -20,7 +20,7 @@ inputs:
- 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:

View File

@@ -1,12 +1,12 @@
Extract Dungeons & Dragons NPC interaction occurrences from the supplied Extract Dungeons & Dragons NPC occurrence occurrences from the supplied
transcript. Include an occurrence only when the transcript establishes one transcript. Include an occurrence only when the transcript establishes one
supplied NPC, one interaction kind, and a coherent passage supporting both. 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; Use the exact `npc_id` and matching `name` pair from the supplied NPC registry;
never invent an ID or substitute a similar name. never invent an ID or substitute a similar name.
Do not summarize, infer relationships, sentiment, factions, motives, aliases, Do not summarize, infer relationships, sentiment, factions, motives, aliases,
or persistent state. Do not identify player characters, anonymous groups, or or persistent state. Do not identify player characters, anonymous groups, or
invented NPCs. Split records when an NPC's interaction kind changes, when 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 combat alignment changes, or when an NPC is first mentioned and later becomes
present. present.

View File

@@ -1,4 +1,4 @@
id: dnd.npc_interactions id: dnd.npc_occurrences
version: "v1" version: "v1"
default_profile: dnd-extraction default_profile: dnd-extraction
inputs: inputs:

View File

@@ -371,10 +371,10 @@ selected target declares them:
| **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. | | **locations** | Required normalized location registry for location-occurrence extraction and normalization. |
| **npc_registry** | Normalized NPC registry. Optional for spells and combat turns; required for NPC interactions and enemy-event 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 Location-occurrence and enemy-event artifact slots have the following exact
binding contracts. Durable semantics and wire shapes remain in their binding contracts. Durable semantics and wire shapes remain in their
@@ -386,11 +386,11 @@ binding contracts. Durable semantics and wire shapes remain in their
| `npc_registry` | `dnd/npc-registry` | `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 | | `locations` | `dnd/location-list` | `application/json` | 1,048,576 bytes | location-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 **npc_registry** 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 **npc_registry**. Location occurrences normalized combat-turn module may use optional **npc_registry**. Location occurrences
require **locations** for extraction and normalization. Enemy-event extraction require **locations** for extraction and normalization. Enemy-event extraction
@@ -404,9 +404,9 @@ generated bindings.
| --- | --- | | --- | --- |
| Input | **seriatim** | | Input | **seriatim** |
| Chunk | **generic**, **dnd/scenes** | | Chunk | **generic**, **dnd/scenes** |
| Extract | **dnd/spells**, **dnd/npc-registry**, **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-events**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/locations**, **dnd/location-occurrences** |
| Merge | **appendorder** | | Merge | **appendorder** |
| Normalize | **noop**, **dnd/spells**, **dnd/npc-registry**, **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-events**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/locations**, **dnd/location-occurrences** |
| Output | **json** | | Output | **json** |
`dnd/locations` extraction and normalization are `llm_backed`; location `dnd/locations` extraction and normalization are `llm_backed`; location
@@ -418,7 +418,7 @@ step and the occurrence lane in the next.
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),
[NPC registry](integrations/dnd-npc-registry-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 events](integrations/dnd-item-event-artifacts.md),
[scene descriptions](integrations/dnd-scene-description-artifacts.md), and [scene descriptions](integrations/dnd-scene-description-artifacts.md), and
@@ -437,7 +437,7 @@ Available validator keys are:
| NPC registry | **extract/dnd/npc-registry/shape**, **extract/dnd/npc-registry/source_refs**, **extract/dnd/npc-registry/source_relatedness**, **normalize/dnd/npc-registry/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 events | **extract/dnd/item-events/shape**, **extract/dnd/item-events/source_refs**, **extract/dnd/item-events/source_relatedness**, **normalize/dnd/item-events/invariants** |
| NPC interactions | **extract/dnd/npc-interactions/shape**, **extract/dnd/npc-interactions/registry**, **extract/dnd/npc-interactions/source_refs**, **extract/dnd/npc-interactions/source_relatedness**, **normalize/dnd/npc-interactions/invariants** | | 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** | | Locations | **extract/dnd/locations/shape**, **extract/dnd/locations/source_refs**, **extract/dnd/locations/source_relatedness**, **normalize/dnd/locations/identity** |
@@ -453,7 +453,7 @@ same at both stages.
| 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 | | 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 events | generic/valid_json, extract/dnd/item-events/shape, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | generic/valid_json, extract/dnd/item-events/shape, normalize/dnd/item-events/invariants, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness |
| NPC interactions | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, normalize/dnd/npc-interactions/invariants, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | | 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 | | 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 |

View File

@@ -63,7 +63,7 @@ chronology; and collapses only duplicates with the same actor identity, turn
kind, and complete valid evidence. It does not infer turns, initiative, or kind, and complete valid evidence. It does not infer turns, initiative, or
actions from registry or scene data. actions from registry or scene data.
The [NPC-interaction artifact](dnd-npc-interaction-artifacts.md) records The [NPC-occurrence artifact](dnd-npc-occurrence-artifacts.md) records
broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md)
uses combat turns as grounding only; turns do not establish an enemy event or uses combat turns as grounding only; turns do not establish an enemy event or
its outcome. The [JSON output contract](json-output.md) defines publication, its outcome. The [JSON output contract](json-output.md) defines publication,

View File

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

View File

@@ -74,5 +74,5 @@ valid evidence.
The [JSON output contract](json-output.md) defines publication. See The [JSON output contract](json-output.md) defines publication. See
[D&D module internals](../internal/dnd.md) for implementation details and the [D&D module internals](../internal/dnd.md) for implementation details and the
[NPC-interaction artifact](dnd-npc-interaction-artifacts.md) for a distinct [NPC-occurrence artifact](dnd-npc-occurrence-artifacts.md) for a distinct
kind of occurrence. kind of occurrence.

View File

@@ -1,7 +1,7 @@
# D&D NPC Interaction Artifact # D&D NPC Occurrence Artifact
This contract defines the durable occurrence list produced by This contract defines the durable occurrence list produced by
`dnd/npc-interactions`. It records discrete, source-grounded interactions with `dnd/npc-occurrences`. It records discrete, source-grounded occurrences with
NPCs already present in a normalized registry; it does not extend that registry NPCs already present in a normalized registry; it does not extend that registry
or summarize the session. or summarize the session.
@@ -27,13 +27,13 @@ Each occurrence has these required fields:
| --- | --- | | --- | --- |
| `npc_id` | Exact durable ID from the required NPC registry. | | `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
{ {
@@ -50,7 +50,7 @@ for an interaction is confined to its accepted chunk.
} }
``` ```
## Interaction categories ## Occurrence categories
| Kind | Meaning | | Kind | Meaning |
| --- | --- | | --- | --- |
@@ -70,16 +70,16 @@ for uncertain classification.
The required normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) resolves The required normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) resolves
the exact `{npc_id, name}` pair. Unknown IDs and names that do not match their the exact `{npc_id, name}` pair. Unknown IDs and names that do not match their
ID are rejected; normalization does not repair names by similarity. Registry ID are rejected; normalization does not repair names by similarity. Registry
references are provenance only and never replace an interaction's own evidence. references are provenance only and never replace an occurrence's own evidence.
Normalization validates the exact pair, orders and Normalization validates the exact pair, orders and
deduplicates exact source references, then orders interactions by valid source 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 NPC ID, canonical name, kind, and complete valid evidence Only entries with the same NPC ID, canonical name, kind, and complete valid evidence
sequence are collapsed; distinct categories or evidence remain separate. sequence are collapsed; distinct categories or evidence remain separate.
See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action
occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes
only `combat_opponent` interactions as grounding; they never establish an enemy only `combat_opponent` occurrences as grounding; they never establish an enemy
event or outcome. The [JSON output contract](json-output.md) defines event or outcome. The [JSON output contract](json-output.md) defines
publication. Pipeline mechanics are described in publication. Pipeline mechanics are described in
[D&D module internals](../internal/dnd.md). [D&D module internals](../internal/dnd.md).

View File

@@ -70,12 +70,12 @@ descriptor.
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. Groups, generic roles, invented labels, and descriptive
enrichment are excluded. Its source references prove registry provenance; they enrichment are excluded. Its source references prove registry provenance; they
do not become evidence for a spell, interaction, combat, or enemy-event do not become evidence for a spell, occurrence, combat, or enemy-event
occurrence. occurrence.
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 The [enemy-event artifact](dnd-enemy-event-artifacts.md) also uses it only for
subject grounding and canonical display names. subject grounding and canonical display names.
The [JSON output contract](json-output.md) defines publication, and The [JSON output contract](json-output.md) defines publication, and

View File

@@ -72,7 +72,7 @@ Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for
that lane. Consumers should use the index descriptors schema identity rather that lane. Consumers should use the index descriptors schema identity rather
than infer a lane schema from its name. The current D&D payload contracts are than infer a lane schema from its name. The current D&D payload contracts are
[spells](dnd-spell-artifacts.md), [NPC registry](dnd-npc-registry-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 events](dnd-item-event-artifacts.md),
[scene descriptions](dnd-scene-description-artifacts.md), and [scene descriptions](dnd-scene-description-artifacts.md), and

View File

@@ -16,7 +16,7 @@ does not repeat their JSON shapes or schemas.
| NPC registry | [NPC registry artifacts](../integrations/dnd-npc-registry-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 events | [item-event artifacts](../integrations/dnd-item-event-artifacts.md) |
| NPC interactions | [NPC-interaction artifacts](../integrations/dnd-npc-interaction-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) | | Locations | [location artifacts](../integrations/dnd-location-artifacts.md) |
@@ -123,7 +123,7 @@ 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 events, 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
@@ -141,12 +141,12 @@ 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 registries are names-only grounding projections: they may canonicalize
actors for spells and combat turns and are required for NPC interactions, but actors for spells and combat turns and are required for NPC occurrences, but
they do not supply evidence. Scene-description registries are eligibility-only they do not supply evidence. Scene-description registries are eligibility-only
projections: they retain the current chunks classification data, not scene projections: they retain the current chunks classification data, not scene
prose or evidence, and exist to route combat extraction. Enemy-event extraction prose or evidence, and exist to route combat extraction. Enemy-event extraction
also projects combat turns to `actor` and `turn_kind` and filters NPC also projects combat turns to `actor` and `turn_kind` and filters NPC
interactions to `combat_opponent` names and kinds. Location registries project occurrences to `combat_opponent` names and kinds. Location registries project
ordered `{id, name}` pairs to location-occurrence extraction and normalization; ordered `{id, name}` pairs to location-occurrence extraction and normalization;
exact ID/name matching keeps same-name locations distinguishable. These compact exact ID/name matching keeps same-name locations distinguishable. These compact
projections, like NPC grounding, are source-free guidance and never event projections, like NPC grounding, are source-free guidance and never event
@@ -163,9 +163,9 @@ shared helper changes.
| NPCs | Does not consume an NPC registry. Its normalizer is the LLM-assisted reconciliation exception described above. | | NPCs | Does not consume an NPC registry. 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 events | Uses campaign context for disambiguation but has no NPC-registry or scene-description dependency. |
| NPC interactions | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. | | NPC occurrences | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. |
| 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. | | 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 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. |

View File

@@ -41,7 +41,7 @@ pipelines:
- npc_registry - 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 alongside independent item events.
@@ -110,12 +110,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
@@ -136,10 +136,10 @@ pipelines:
artifact: artifact:
step: extract-events step: extract-events
lane: combat-turns lane: combat-turns
npc_interactions: npc_occurrences:
artifact: artifact:
step: extract-events step: extract-events
lane: npc-interactions lane: npc-occurrences
artifacts: artifacts:
enemy-events: enemy-events:
extract: extract:

View File

@@ -27,7 +27,7 @@ import (
itemevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" itemevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
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/locations"
npcinteractions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" npcoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" 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"
@@ -58,7 +58,7 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T
"npc_registry": {step: "describe-session", lane: "npc_registry"}, "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 {
@@ -78,7 +78,7 @@ 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{"npc_registry", "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)
} }
@@ -171,9 +171,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{
"npc_registry": "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") {
@@ -257,7 +257,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque
content = []byte(`{"events":[]}`) content = []byte(`{"events":[]}`)
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 {
var registry struct { var registry struct {
NPCs []struct { NPCs []struct {

View File

@@ -8,16 +8,16 @@ 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"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
) )
func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) { func TestProductionNPCOccurrencePipelineResolvesAndPrepares(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
resolved, err := pipeline.ResolvePipeline(npcInteractionProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), pipeline.ResolveOptions{}, catalogFromRegistries(components.registries)) resolved, err := pipeline.ResolvePipeline(npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), pipeline.ResolveOptions{}, catalogFromRegistries(components.registries))
if err != nil { if err != nil {
t.Fatalf("ResolvePipeline() error = %v", err) t.Fatalf("ResolvePipeline() error = %v", err)
} }
@@ -25,8 +25,8 @@ func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) {
t.Fatalf("resolved pipeline = %#v", resolved) t.Fatalf("resolved pipeline = %#v", resolved)
} }
lane := resolved.Steps[1].ArtifactLanes[0] lane := resolved.Steps[1].ArtifactLanes[0]
if lane.ArtifactKind != dnd.NPCOccurrenceListKind || lane.Extract.Module != interactionextract.Key || lane.Normalize.Module != interactionnormalize.Key { if lane.ArtifactKind != dnd.NPCOccurrenceListKind || lane.Extract.Module != occurrenceextract.Key || lane.Normalize.Module != occurrencenormalize.Key {
t.Fatalf("interaction lane = %#v", lane) t.Fatalf("occurrence lane = %#v", lane)
} }
for _, bindings := range [][]pipeline.ReferenceBinding{lane.ExtractReferences.Bindings, lane.NormalizeReferences.Bindings} { 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" { 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" {
@@ -39,15 +39,15 @@ func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) {
catalog := catalogFromRegistries(components.registries) catalog := catalogFromRegistries(components.registries)
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCOccurrenceListKind) codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCOccurrenceListKind)
if !ok || codecSpec.Schema.ID != interactioncodec.SchemaID || codecSpec.Schema.Version != interactioncodec.SchemaVersion { if !ok || codecSpec.Schema.ID != occurrencecodec.SchemaID || codecSpec.Schema.Version != occurrencecodec.SchemaVersion {
t.Fatalf("NPC interaction codec spec = %#v", codecSpec) t.Fatalf("NPC occurrence codec spec = %#v", codecSpec)
} }
} }
func TestProductionNPCInteractionReferencesRequireEarlierCompatibleProducer(t *testing.T) { func TestProductionNPCOccurrenceReferencesRequireEarlierCompatibleProducer(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries) catalog := catalogFromRegistries(components.registries)
laterProfile := npcInteractionProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")) laterProfile := npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry"))
laterProfile.Steps[0].ID = "seed" laterProfile.Steps[0].ID = "seed"
laterProfile.Steps[0].Artifacts["seed"] = laterProfile.Steps[0].Artifacts["npc_registry"] laterProfile.Steps[0].Artifacts["seed"] = laterProfile.Steps[0].Artifacts["npc_registry"]
delete(laterProfile.Steps[0].Artifacts, "npc_registry") delete(laterProfile.Steps[0].Artifacts, "npc_registry")
@@ -60,10 +60,10 @@ func TestProductionNPCInteractionReferencesRequireEarlierCompatibleProducer(t *t
profile pipeline.PipelineProfile profile pipeline.PipelineProfile
want string want string
}{ }{
{name: "missing", profile: npcInteractionProfile(pipeline.ReferenceSource{}), want: "source must not be empty"}, {name: "missing", profile: npcOccurrenceProfile(pipeline.ReferenceSource{}), want: "source must not be empty"},
{name: "same step", profile: npcInteractionProfile(pipeline.GeneratedReference("interactions", "interactions")), want: "earlier step"}, {name: "same step", profile: npcOccurrenceProfile(pipeline.GeneratedReference("occurrences", "occurrences")), want: "earlier step"},
{name: "later step", profile: laterProfile, want: "earlier step"}, {name: "later step", profile: laterProfile, want: "earlier step"},
{name: "wrong artifact kind", profile: npcInteractionProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), want: "does not accept artifact kind"}, {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")} tests[3].profile.Steps[0].Artifacts["npc_registry"] = pipeline.ArtifactLaneProfile{Extract: pipeline.Binding("dnd/spells")}
for _, test := range tests { for _, test := range tests {
@@ -76,7 +76,7 @@ func TestProductionNPCInteractionReferencesRequireEarlierCompatibleProducer(t *t
} }
} }
func TestProductionNPCInteractionReferencesRejectIncompatibleExternalRegistries(t *testing.T) { func TestProductionNPCOccurrenceReferencesRejectIncompatibleExternalRegistries(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries) catalog := catalogFromRegistries(components.registries)
root := t.TempDir() root := t.TempDir()
@@ -95,7 +95,7 @@ func TestProductionNPCInteractionReferencesRejectIncompatibleExternalRegistries(
if err := os.WriteFile(path, []byte(test.content), 0o600); err != nil { if err := os.WriteFile(path, []byte(test.content), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
resolved, err := pipeline.ResolvePipeline(npcInteractionProfile(pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog) resolved, err := pipeline.ResolvePipeline(npcOccurrenceProfile(pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog)
if err != nil { if err != nil {
t.Fatalf("ResolvePipeline() error = %v", err) t.Fatalf("ResolvePipeline() error = %v", err)
} }
@@ -116,9 +116,9 @@ func TestProductionNPCInteractionReferencesRejectIncompatibleExternalRegistries(
} }
} }
func npcInteractionProfile(reference pipeline.ReferenceSource) pipeline.PipelineProfile { func npcOccurrenceProfile(reference pipeline.ReferenceSource) pipeline.PipelineProfile {
profile := pipeline.PipelineProfile{ profile := pipeline.PipelineProfile{
ID: "dnd-npc-interactions", ID: "dnd-npc-occurrences",
Input: pipeline.Binding("seriatim"), Input: pipeline.Binding("seriatim"),
Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}}, Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
Output: pipeline.Binding("json"), Output: pipeline.Binding("json"),
@@ -126,8 +126,8 @@ func npcInteractionProfile(reference pipeline.ReferenceSource) pipeline.Pipeline
{ID: "npc-registry", Artifacts: map[string]pipeline.ArtifactLaneProfile{ {ID: "npc-registry", Artifacts: map[string]pipeline.ArtifactLaneProfile{
"npc_registry": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)}, "npc_registry": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
}}, }},
{ID: "interactions", References: map[string]pipeline.ReferenceSource{"npc_registry": reference}, Artifacts: map[string]pipeline.ArtifactLaneProfile{ {ID: "occurrences", References: map[string]pipeline.ReferenceSource{"npc_registry": reference}, Artifacts: map[string]pipeline.ArtifactLaneProfile{
"interactions": {Extract: pipeline.Binding(interactionextract.Key), Normalize: pipeline.Binding(interactionnormalize.Key)}, "occurrences": {Extract: pipeline.Binding(occurrenceextract.Key), Normalize: pipeline.Binding(occurrencenormalize.Key)},
}}, }},
}, },
} }

View File

@@ -58,7 +58,7 @@ 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,npc_registry,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-events,locations,npc_registry,scene-descriptions|extract-events:combat-turns,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, "locations")
@@ -102,7 +102,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
"npc_registry": {step: "describe-session", lane: "npc_registry"}, "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 {

View File

@@ -165,7 +165,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
{stage: pipeline.StageExtract, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked}, {stage: pipeline.StageExtract, key: "dnd/npc-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-events", 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/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},
@@ -174,7 +174,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
{stage: pipeline.StageNormalize, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked}, {stage: pipeline.StageNormalize, key: "dnd/npc-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-events", 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/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},

View File

@@ -1,5 +1,5 @@
// Package npcinteractions encodes durable D&D NPC occurrence artifacts. // Package npcoccurrences encodes durable D&D NPC occurrence artifacts.
package npcinteractions package npcoccurrences
import ( import (
"embed" "embed"

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"bytes" "bytes"
@@ -111,7 +111,7 @@ func TestCodecStrictlyRejectsMalformedUnknownAndTrailingJSON(t *testing.T) {
for _, test := range []struct{ name, raw, want string }{ for _, test := range []struct{ name, raw, want string }{
{"malformed", `{`, "decode dnd npc occurrence list"}, {"malformed", `{`, "decode dnd npc occurrence list"},
{"unknown top-level", `{"occurrences":[],"unexpected":true}`, "unknown field"}, {"unknown top-level", `{"occurrences":[],"unexpected":true}`, "unknown field"},
{"unknown interaction field", strings.Replace(validJSON, `"kind":"dialogue"`, `"kind":"dialogue","unexpected":true`, 1), "unknown field"}, {"unknown occurrence field", strings.Replace(validJSON, `"kind":"dialogue"`, `"kind":"dialogue","unexpected":true`, 1), "unknown field"},
{"unknown source reference field", strings.Replace(validJSON, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"}, {"unknown source reference field", strings.Replace(validJSON, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"},
{"trailing", `{"occurrences":[]} {}`, "multiple JSON values"}, {"trailing", `{"occurrences":[]} {}`, "multiple JSON values"},
} { } {
@@ -147,7 +147,7 @@ func TestCodecRejectsRequiredShapeEnumAndReferenceBoundaries(t *testing.T) {
} }
} }
func TestCodecAcceptsEveryInteractionKind(t *testing.T) { func TestCodecAcceptsEveryOccurrenceKind(t *testing.T) {
for _, kind := range []dnd.NPCOccurrenceKind{ for _, kind := range []dnd.NPCOccurrenceKind{
dnd.NPCOccurrenceKindMentioned, dnd.NPCOccurrenceKindMentioned,
dnd.NPCOccurrenceKindNoncombatPresence, dnd.NPCOccurrenceKindNoncombatPresence,

View File

@@ -49,8 +49,8 @@ func TestExtractMapsEnemyEventsInSourceOrder(t *testing.T) {
if got := string(request.Inputs[CombatTurnReferenceSlot].Content); !strings.Contains(got, `"actor":"Ashfang"`) || strings.Contains(got, "source_ref") { if got := string(request.Inputs[CombatTurnReferenceSlot].Content); !strings.Contains(got, `"actor":"Ashfang"`) || strings.Contains(got, "source_ref") {
t.Fatalf("combat grounding = %s", got) t.Fatalf("combat grounding = %s", got)
} }
if got := string(request.Inputs[NPCInteractionReferenceSlot].Content); !strings.Contains(got, `"kind":"combat_opponent"`) || strings.Contains(got, "Aria") { if got := string(request.Inputs[NPCOccurrenceReferenceSlot].Content); !strings.Contains(got, `"kind":"combat_opponent"`) || strings.Contains(got, "Aria") {
t.Fatalf("interaction grounding = %s", got) t.Fatalf("occurrence grounding = %s", got)
} }
} }

View File

@@ -15,7 +15,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns" combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
@@ -27,7 +27,7 @@ const (
NPCRegistryReferenceSlot = npcregistry.ReferenceSlot NPCRegistryReferenceSlot = npcregistry.ReferenceSlot
SceneDescriptionReferenceSlot = sceneregistry.ReferenceSlot SceneDescriptionReferenceSlot = sceneregistry.ReferenceSlot
CombatTurnReferenceSlot = "combat_turns" CombatTurnReferenceSlot = "combat_turns"
NPCInteractionReferenceSlot = "npc_interactions" NPCOccurrenceReferenceSlot = "npc_occurrences"
ReferenceMaxBytes = 1048576 ReferenceMaxBytes = 1048576
promptProjectionMediaType = "application/json" promptProjectionMediaType = "application/json"
) )
@@ -67,10 +67,10 @@ func referenceSlots() []contracts.ReferenceSlot {
MaxBytes: ReferenceMaxBytes, MaxBytes: ReferenceMaxBytes,
}, },
contracts.ReferenceSlot{ contracts.ReferenceSlot{
Name: NPCInteractionReferenceSlot, Name: NPCOccurrenceReferenceSlot,
Description: "Required NPC-interaction artifact used only as source-free enemy-event grounding.", Description: "Required NPC-occurrence artifact used only as source-free enemy-event grounding.",
Required: true, Required: true,
AcceptedMediaTypes: []string{interactioncodec.MediaType}, AcceptedMediaTypes: []string{occurrencecodec.MediaType},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCOccurrenceListKind}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCOccurrenceListKind},
MaxBytes: ReferenceMaxBytes, MaxBytes: ReferenceMaxBytes,
}, },
@@ -86,14 +86,14 @@ type groundingResolver struct {
npcs *npcregistry.Resolver npcs *npcregistry.Resolver
scenes *sceneregistry.Resolver scenes *sceneregistry.Resolver
combatTurns *contracts.LLMInputMaterial combatTurns *contracts.LLMInputMaterial
npcInteractions *contracts.LLMInputMaterial npcOccurrences *contracts.LLMInputMaterial
} }
type grounding struct { type grounding struct {
npcInput contracts.LLMInputMaterial npcInput contracts.LLMInputMaterial
combatTurnInput contracts.LLMInputMaterial combatTurnInput contracts.LLMInputMaterial
npcInteractionInput contracts.LLMInputMaterial npcOccurrenceInput contracts.LLMInputMaterial
} }
func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver, error) { func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver, error) {
@@ -109,15 +109,15 @@ func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver
if err != nil { if err != nil {
return nil, err return nil, err
} }
npcInteractions, err := prepareNPCInteractionInput(references) npcOccurrences, err := prepareNPCOccurrenceInput(references)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &groundingResolver{ return &groundingResolver{
npcs: npcs, npcs: npcs,
scenes: scenes, scenes: scenes,
combatTurns: combatTurns, combatTurns: combatTurns,
npcInteractions: npcInteractions, npcOccurrences: npcOccurrences,
}, nil }, nil
} }
@@ -136,14 +136,14 @@ func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (groundin
if err != nil { if err != nil {
return grounding{}, err return grounding{}, err
} }
npcInteractions, err := resolveInput(references, NPCInteractionReferenceSlot, r.npcInteractions, prepareNPCInteractionInput) npcOccurrences, err := resolveInput(references, NPCOccurrenceReferenceSlot, r.npcOccurrences, prepareNPCOccurrenceInput)
if err != nil { if err != nil {
return grounding{}, err return grounding{}, err
} }
return grounding{ return grounding{
npcInput: npcs.PromptInput(), npcInput: npcs.PromptInput(),
combatTurnInput: combatTurns, combatTurnInput: combatTurns,
npcInteractionInput: npcInteractions, npcOccurrenceInput: npcOccurrences,
}, nil }, nil
} }
@@ -188,9 +188,9 @@ func resolveInput(references contracts.ReferenceSet, slot string, seeded *contra
func (g grounding) PromptInputs() contracts.LLMInputSet { func (g grounding) PromptInputs() contracts.LLMInputSet {
return contracts.LLMInputSet{ return contracts.LLMInputSet{
NPCRegistryReferenceSlot: g.npcInput.Clone(), NPCRegistryReferenceSlot: g.npcInput.Clone(),
CombatTurnReferenceSlot: g.combatTurnInput.Clone(), CombatTurnReferenceSlot: g.combatTurnInput.Clone(),
NPCInteractionReferenceSlot: g.npcInteractionInput.Clone(), NPCOccurrenceReferenceSlot: g.npcOccurrenceInput.Clone(),
} }
} }
@@ -212,22 +212,22 @@ func prepareCombatTurnInput(references contracts.ReferenceSet) (*contracts.LLMIn
return newPromptInput(CombatTurnReferenceSlot, content), nil return newPromptInput(CombatTurnReferenceSlot, content), nil
} }
func prepareNPCInteractionInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) { func prepareNPCOccurrenceInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) {
item, ok, err := referenceItem(references, NPCInteractionReferenceSlot, interactioncodec.MediaType) item, ok, err := referenceItem(references, NPCOccurrenceReferenceSlot, occurrencecodec.MediaType)
if err != nil || !ok { if err != nil || !ok {
return nil, err return nil, err
} }
value, err := interactioncodec.New().Decode(item.Content) value, err := occurrencecodec.New().Decode(item.Content)
if err != nil { if err != nil {
return nil, fmt.Errorf("decode NPC-interaction grounding: invalid approved NPC-interaction JSON") return nil, fmt.Errorf("decode NPC-occurrence grounding: invalid approved NPC-occurrence JSON")
} }
content, err := json.Marshal(struct { content, err := json.Marshal(struct {
Interactions []npcInteractionProjection `json:"npc_interactions"` Occurrences []npcOccurrenceProjection `json:"npc_occurrences"`
}{Interactions: projectNPCInteractions(value.Occurrences)}) }{Occurrences: projectNPCOccurrences(value.Occurrences)})
if err != nil { if err != nil {
return nil, fmt.Errorf("encode NPC-interaction grounding: %w", err) return nil, fmt.Errorf("encode NPC-occurrence grounding: %w", err)
} }
return newPromptInput(NPCInteractionReferenceSlot, content), nil return newPromptInput(NPCOccurrenceReferenceSlot, content), nil
} }
func referenceItem(references contracts.ReferenceSet, slotName, expectedMediaType string) (contracts.ReferenceItem, bool, error) { func referenceItem(references contracts.ReferenceSet, slotName, expectedMediaType string) (contracts.ReferenceItem, bool, error) {
@@ -271,16 +271,16 @@ func projectCombatTurns(turns []dnd.CombatTurn) []combatTurnProjection {
return projection return projection
} }
type npcInteractionProjection struct { type npcOccurrenceProjection struct {
Name string `json:"name"` Name string `json:"name"`
Kind dnd.NPCOccurrenceKind `json:"kind"` Kind dnd.NPCOccurrenceKind `json:"kind"`
} }
func projectNPCInteractions(occurrences []dnd.NPCOccurrence) []npcInteractionProjection { func projectNPCOccurrences(occurrences []dnd.NPCOccurrence) []npcOccurrenceProjection {
projection := make([]npcInteractionProjection, 0, len(occurrences)) projection := make([]npcOccurrenceProjection, 0, len(occurrences))
for _, occurrence := range occurrences { for _, occurrence := range occurrences {
if occurrence.Kind == dnd.NPCOccurrenceKindCombatOpponent { if occurrence.Kind == dnd.NPCOccurrenceKindCombatOpponent {
projection = append(projection, npcInteractionProjection{Name: occurrence.Name, Kind: occurrence.Kind}) projection = append(projection, npcOccurrenceProjection{Name: occurrence.Name, Kind: occurrence.Kind})
} }
} }
return projection return projection

View File

@@ -10,7 +10,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns" combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
@@ -36,7 +36,7 @@ func TestReferenceSlotsDescribeRequiredTypedArtifacts(t *testing.T) {
{NPCRegistryReferenceSlot, dnd.NPCRegistryKind}, {NPCRegistryReferenceSlot, dnd.NPCRegistryKind},
{SceneDescriptionReferenceSlot, dnd.SceneDescriptionListKind}, {SceneDescriptionReferenceSlot, dnd.SceneDescriptionListKind},
{CombatTurnReferenceSlot, dnd.CombatTurnListKind}, {CombatTurnReferenceSlot, dnd.CombatTurnListKind},
{NPCInteractionReferenceSlot, dnd.NPCOccurrenceListKind}, {NPCOccurrenceReferenceSlot, dnd.NPCOccurrenceListKind},
} { } {
slot, ok := byName[want.name] slot, ok := byName[want.name]
if !ok || !slot.Required || slot.MaxBytes != ReferenceMaxBytes || len(slot.AcceptedMediaTypes) != 1 || slot.AcceptedMediaTypes[0] != "application/json" || len(slot.AcceptedArtifactKinds) != 1 || slot.AcceptedArtifactKinds[0] != want.kind { if !ok || !slot.Required || slot.MaxBytes != ReferenceMaxBytes || len(slot.AcceptedMediaTypes) != 1 || slot.AcceptedMediaTypes[0] != "application/json" || len(slot.AcceptedArtifactKinds) != 1 || slot.AcceptedArtifactKinds[0] != want.kind {
@@ -61,9 +61,9 @@ func TestGroundingProducesExactSourceFreePromptInputs(t *testing.T) {
} }
inputs := resolved.PromptInputs() inputs := resolved.PromptInputs()
want := map[string]string{ want := map[string]string{
NPCRegistryReferenceSlot: `{"npcs":[{"name":"Ashfang"}]}`, NPCRegistryReferenceSlot: `{"npcs":[{"name":"Ashfang"}]}`,
CombatTurnReferenceSlot: `{"combat_turns":[{"actor":"Ashfang","turn_kind":"turn"},{"actor":"Aria","turn_kind":"reaction"}]}`, CombatTurnReferenceSlot: `{"combat_turns":[{"actor":"Ashfang","turn_kind":"turn"},{"actor":"Aria","turn_kind":"reaction"}]}`,
NPCInteractionReferenceSlot: `{"npc_interactions":[{"name":"Ashfang","kind":"combat_opponent"}]}`, NPCOccurrenceReferenceSlot: `{"npc_occurrences":[{"name":"Ashfang","kind":"combat_opponent"}]}`,
} }
if len(inputs) != len(want) { if len(inputs) != len(want) {
t.Fatalf("PromptInputs() = %#v", inputs) t.Fatalf("PromptInputs() = %#v", inputs)
@@ -153,7 +153,7 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) {
{NPCRegistryReferenceSlot, "NPC registry"}, {NPCRegistryReferenceSlot, "NPC registry"},
{SceneDescriptionReferenceSlot, "scene descriptions"}, {SceneDescriptionReferenceSlot, "scene descriptions"},
{CombatTurnReferenceSlot, CombatTurnReferenceSlot}, {CombatTurnReferenceSlot, CombatTurnReferenceSlot},
{NPCInteractionReferenceSlot, NPCInteractionReferenceSlot}, {NPCOccurrenceReferenceSlot, NPCOccurrenceReferenceSlot},
} { } {
t.Run("missing "+test.slot, func(t *testing.T) { t.Run("missing "+test.slot, func(t *testing.T) {
resolver, err := newGroundingResolver(withoutSlot(valid, test.slot)) resolver, err := newGroundingResolver(withoutSlot(valid, test.slot))
@@ -176,7 +176,7 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) {
set contracts.ReferenceSet set contracts.ReferenceSet
}{ }{
{"multiple items", replaceSlot(valid, CombatTurnReferenceSlot, contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{{MediaType: "application/json"}, {MediaType: "application/json"}}})}, {"multiple items", replaceSlot(valid, CombatTurnReferenceSlot, contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{{MediaType: "application/json"}, {MediaType: "application/json"}}})},
{"malformed durable content", replaceItem(valid, NPCInteractionReferenceSlot, contracts.ReferenceItem{MediaType: "application/json", Content: []byte(`{}`)})}, {"malformed durable content", replaceItem(valid, NPCOccurrenceReferenceSlot, contracts.ReferenceItem{MediaType: "application/json", Content: []byte(`{}`)})},
{"wrong media type", replaceItem(valid, CombatTurnReferenceSlot, contracts.ReferenceItem{MediaType: "text/plain", Content: valid.Slots[CombatTurnReferenceSlot].Items[0].Content})}, {"wrong media type", replaceItem(valid, CombatTurnReferenceSlot, contracts.ReferenceItem{MediaType: "text/plain", Content: valid.Slots[CombatTurnReferenceSlot].Items[0].Content})},
{"oversize", replaceItem(valid, CombatTurnReferenceSlot, contracts.ReferenceItem{MediaType: "application/json", Content: make([]byte, ReferenceMaxBytes+1)})}, {"oversize", replaceItem(valid, CombatTurnReferenceSlot, contracts.ReferenceItem{MediaType: "application/json", Content: make([]byte, ReferenceMaxBytes+1)})},
} }
@@ -220,7 +220,7 @@ func groundingReferences(t *testing.T, enemy string, sceneKind dnd.SceneKind) co
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
interactionContent, err := interactioncodec.New().Encode(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{ interactionContent, err := occurrencecodec.New().Encode(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{
{NPCID: identity.DeriveID(enemy), Name: enemy, Kind: dnd.NPCOccurrenceKindCombatOpponent, SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 1, EndUnitID: 1}}}, {NPCID: identity.DeriveID(enemy), Name: enemy, Kind: dnd.NPCOccurrenceKindCombatOpponent, SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 1, EndUnitID: 1}}},
{NPCID: identity.DeriveID("Aria"), Name: "Aria", Kind: dnd.NPCOccurrenceKindCombatAlly, SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 2, EndUnitID: 2}}}, {NPCID: identity.DeriveID("Aria"), Name: "Aria", Kind: dnd.NPCOccurrenceKindCombatAlly, SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 2, EndUnitID: 2}}},
}}) }})
@@ -231,7 +231,7 @@ func groundingReferences(t *testing.T, enemy string, sceneKind dnd.SceneKind) co
NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCRegistryReferenceSlot, npcContent)}}, NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCRegistryReferenceSlot, npcContent)}},
SceneDescriptionReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(SceneDescriptionReferenceSlot, sceneContent)}}, SceneDescriptionReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(SceneDescriptionReferenceSlot, sceneContent)}},
CombatTurnReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(CombatTurnReferenceSlot, turnContent)}}, CombatTurnReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(CombatTurnReferenceSlot, turnContent)}},
NPCInteractionReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCInteractionReferenceSlot, interactionContent)}}, NPCOccurrenceReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCOccurrenceReferenceSlot, interactionContent)}},
}} }}
} }

View File

@@ -32,7 +32,7 @@ func TestRegisterPromptAssetsAndPrepareEnemyEventPrompt(t *testing.T) {
func TestEnemyEventPromptRequiresGroundingInputs(t *testing.T) { func TestEnemyEventPromptRequiresGroundingInputs(t *testing.T) {
engine := newEnemyEventPromptEngine(t) engine := newEnemyEventPromptEngine(t)
for _, inputName := range []string{"npc_registry", "combat_turns", "npc_interactions"} { for _, inputName := range []string{"npc_registry", "combat_turns", "npc_occurrences"} {
t.Run(inputName, func(t *testing.T) { t.Run(inputName, func(t *testing.T) {
inputs := enemyEventPromptInputs() inputs := enemyEventPromptInputs()
delete(inputs, inputName) delete(inputs, inputName)
@@ -79,12 +79,12 @@ func newEnemyEventPromptEngine(t *testing.T) *promptkit.Engine {
func enemyEventPromptInputs() map[string]promptkit.ArtifactRef { func enemyEventPromptInputs() map[string]promptkit.ArtifactRef {
return map[string]promptkit.ArtifactRef{ return map[string]promptkit.ArtifactRef{
"transcript": promptkit.Inline(`{"value":"enemy-transcript"}`), "transcript": promptkit.Inline(`{"value":"enemy-transcript"}`),
"players": promptkit.Inline("enemy-player"), "players": promptkit.Inline("enemy-player"),
"party": promptkit.Inline("enemy-party"), "party": promptkit.Inline("enemy-party"),
"glossary": promptkit.Inline("enemy-glossary"), "glossary": promptkit.Inline("enemy-glossary"),
"npc_registry": promptkit.Inline(`{"npcs":[{"name":"enemy-npc"}]}`), "npc_registry": promptkit.Inline(`{"npcs":[{"name":"enemy-npc"}]}`),
"combat_turns": promptkit.Inline(`{"combat_turns":[{"actor":"enemy-turn","turn_kind":"turn"}]}`), "combat_turns": promptkit.Inline(`{"combat_turns":[{"actor":"enemy-turn","turn_kind":"turn"}]}`),
"npc_interactions": promptkit.Inline(`{"npc_interactions":[{"name":"enemy-opponent","kind":"combat_opponent"}]}`), "npc_occurrences": promptkit.Inline(`{"npc_occurrences":[{"name":"enemy-opponent","kind":"combat_opponent"}]}`),
} }
} }

View File

@@ -1,17 +0,0 @@
package npcinteractions
type extractionResponse struct {
Occurrences []occurrenceResponse `json:"occurrences"`
}
type occurrenceResponse struct {
NPCID string `json:"npc_id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceRefs []interactionSourceRefResponse `json:"source_refs"`
}
type interactionSourceRefResponse struct {
StartUnitID int `json:"start_unit_id"`
EndUnitID int `json:"end_unit_id"`
}

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"sort" "sort"
@@ -66,7 +66,7 @@ func canonicalOccurrenceList(response extractionResponse, sourceID string) dnd.N
return dnd.NPCOccurrenceList{Occurrences: occurrences} return dnd.NPCOccurrenceList{Occurrences: occurrences}
} }
func canonicalSourceRefs(refs []interactionSourceRefResponse, sourceID string) []source.SourceRef { func canonicalSourceRefs(refs []occurrenceSourceRefResponse, sourceID string) []source.SourceRef {
if refs == nil { if refs == nil {
return nil return nil
} }
@@ -77,13 +77,13 @@ func canonicalSourceRefs(refs []interactionSourceRefResponse, sourceID string) [
return out return out
} }
func occurrenceResponseRefs(refs []source.SourceRef) []interactionSourceRefResponse { func occurrenceResponseRefs(refs []source.SourceRef) []occurrenceSourceRefResponse {
if refs == nil { if refs == nil {
return nil return nil
} }
values := make([]interactionSourceRefResponse, len(refs)) values := make([]occurrenceSourceRefResponse, len(refs))
for index, ref := range refs { for index, ref := range refs {
values[index] = interactionSourceRefResponse{StartUnitID: ref.StartUnitID, EndUnitID: ref.EndUnitID} values[index] = occurrenceSourceRefResponse{StartUnitID: ref.StartUnitID, EndUnitID: ref.EndUnitID}
} }
return values return values
} }

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"context" "context"
@@ -13,8 +13,8 @@ import (
) )
const ( const (
Key = "dnd/npc-interactions" Key = "dnd/npc-occurrences"
mappingPolicy = "dnd.npc_interactions.extract_mapping.v2" mappingPolicy = "dnd.npc_occurrences.extract_mapping.v2"
) )
const ( const (
@@ -28,21 +28,21 @@ var requiredCapabilities = []string{
} }
var providedCapabilities = []string{ var providedCapabilities = []string{
"dnd.npc_interactions", "dnd.npc_occurrences",
} }
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
Glossary: "Optional campaign glossary reference material used only for interaction disambiguation.", Glossary: "Optional campaign glossary reference material used only for occurrence disambiguation.",
Party: "Optional party roster reference material used only for interaction disambiguation.", Party: "Optional party roster reference material used only for occurrence disambiguation.",
Players: "Optional player list reference material used only for interaction disambiguation.", Players: "Optional player list reference material used only for occurrence disambiguation.",
Roster: "Deprecated alias for party roster reference material used only for interaction disambiguation.", Roster: "Deprecated alias for party roster reference material used only for occurrence disambiguation.",
} }
func referenceSlots() []contracts.ReferenceSlot { func referenceSlots() []contracts.ReferenceSlot {
slots := shared.ReferenceSlots(referenceSlotDescriptions) slots := shared.ReferenceSlots(referenceSlotDescriptions)
slots = append(slots, contracts.ReferenceSlot{ slots = append(slots, contracts.ReferenceSlot{
Name: NPCRegistryReferenceSlot, Name: NPCRegistryReferenceSlot,
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.", Description: "Required normalized NPC registry used only for occurrence identity grounding, never as occurrence evidence.",
Required: true, Required: true,
AcceptedMediaTypes: []string{"application/json"}, AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
@@ -225,5 +225,5 @@ func DecodeOptions(options map[string]any) (Options, error) {
} }
func extractorErrorf(format string, args ...any) error { func extractorErrorf(format string, args ...any) error {
return fmt.Errorf("dnd NPC interactions extractor: "+format, args...) return fmt.Errorf("dnd NPC occurrences extractor: "+format, args...)
} }

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"context" "context"
@@ -17,14 +17,14 @@ import (
) )
func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) { func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) {
client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{ client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{
{NPCID: identity.DeriveID("Other"), Name: "Other", Kind: "other", SourceRefs: interactionRefs(30, 30)}, {NPCID: identity.DeriveID("Other"), Name: "Other", Kind: "other", SourceRefs: occurrenceRefs(30, 30)},
{NPCID: identity.DeriveID("Opponent"), Name: "Opponent", Kind: "combat_opponent", SourceRefs: interactionRefs(20, 20)}, {NPCID: identity.DeriveID("Opponent"), Name: "Opponent", Kind: "combat_opponent", SourceRefs: occurrenceRefs(20, 20)},
{NPCID: identity.DeriveID("Ally"), Name: "Ally", Kind: "combat_ally", SourceRefs: interactionRefs(5, 5)}, {NPCID: identity.DeriveID("Ally"), Name: "Ally", Kind: "combat_ally", SourceRefs: occurrenceRefs(5, 5)},
{NPCID: identity.DeriveID("Speaker"), Name: "Speaker", Kind: "dialogue", SourceRefs: append(interactionRefs(2, 2), interactionRefs(2, 2)...)}, {NPCID: identity.DeriveID("Speaker"), Name: "Speaker", Kind: "dialogue", SourceRefs: append(occurrenceRefs(2, 2), occurrenceRefs(2, 2)...)},
{NPCID: identity.DeriveID("Present"), Name: "Present", Kind: "noncombat_presence", SourceRefs: interactionRefs(7, 7)}, {NPCID: identity.DeriveID("Present"), Name: "Present", Kind: "noncombat_presence", SourceRefs: occurrenceRefs(7, 7)},
{NPCID: identity.DeriveID("Mentioned"), Name: "Mentioned", Kind: "mentioned", SourceRefs: interactionRefs(10, 10)}, {NPCID: identity.DeriveID("Mentioned"), Name: "Mentioned", Kind: "mentioned", SourceRefs: occurrenceRefs(10, 10)},
{NPCID: identity.DeriveID("Invalid"), Name: "Invalid", Kind: "unsupported", SourceRefs: interactionRefs(0, 0)}, {NPCID: identity.DeriveID("Invalid"), Name: "Invalid", Kind: "unsupported", SourceRefs: occurrenceRefs(0, 0)},
}}} }}}
references := requiredRegistryReferences(t, "Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid") references := requiredRegistryReferences(t, "Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid")
req := extractionRequest() req := extractionRequest()
@@ -34,10 +34,10 @@ func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Extract() error = %v", err) t.Fatalf("Extract() error = %v", err)
} }
if got := interactionNames(result.Value); !reflect.DeepEqual(got, []string{"Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid"}) { if got := occurrenceNames(result.Value); !reflect.DeepEqual(got, []string{"Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid"}) {
t.Fatalf("interaction order = %#v", got) t.Fatalf("occurrence order = %#v", got)
} }
if got := interactionKinds(result.Value); !reflect.DeepEqual(got, []dnd.NPCOccurrenceKind{ if got := occurrenceKinds(result.Value); !reflect.DeepEqual(got, []dnd.NPCOccurrenceKind{
dnd.NPCOccurrenceKindMentioned, dnd.NPCOccurrenceKindMentioned,
dnd.NPCOccurrenceKindDialogue, dnd.NPCOccurrenceKindDialogue,
dnd.NPCOccurrenceKindNoncombatPresence, dnd.NPCOccurrenceKindNoncombatPresence,
@@ -46,7 +46,7 @@ func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) {
dnd.NPCOccurrenceKindOther, dnd.NPCOccurrenceKindOther,
"unsupported", "unsupported",
}) { }) {
t.Fatalf("interaction kinds = %#v", got) t.Fatalf("occurrence kinds = %#v", got)
} }
if refs := result.Value.Occurrences[1].SourceRefs; !reflect.DeepEqual(refs, []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}}) { if refs := result.Value.Occurrences[1].SourceRefs; !reflect.DeepEqual(refs, []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}}) {
t.Fatalf("canonical source refs = %#v", refs) t.Fatalf("canonical source refs = %#v", refs)
@@ -59,16 +59,16 @@ func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) {
} }
} }
func TestExtractUsesDocumentOrderForReferencesAndInteractions(t *testing.T) { func TestExtractUsesDocumentOrderForReferencesAndOccurrences(t *testing.T) {
client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{ client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{
{NPCID: identity.DeriveID("Later"), Name: "Later", Kind: "dialogue", SourceRefs: interactionRefs(10, 10)}, {NPCID: identity.DeriveID("Later"), Name: "Later", Kind: "dialogue", SourceRefs: occurrenceRefs(10, 10)},
{NPCID: identity.DeriveID("First"), Name: "First", Kind: "mentioned", SourceRefs: []interactionSourceRefResponse{ {NPCID: identity.DeriveID("First"), Name: "First", Kind: "mentioned", SourceRefs: []occurrenceSourceRefResponse{
{StartUnitID: 10, EndUnitID: 10}, {StartUnitID: 10, EndUnitID: 10},
{StartUnitID: 30, EndUnitID: 30}, {StartUnitID: 30, EndUnitID: 30},
{StartUnitID: 30, EndUnitID: 30}, {StartUnitID: 30, EndUnitID: 30},
{StartUnitID: 999, EndUnitID: 0}, {StartUnitID: 999, EndUnitID: 0},
}}, }},
{NPCID: identity.DeriveID("Second"), Name: "Second", Kind: "other", SourceRefs: interactionRefs(30, 30)}, {NPCID: identity.DeriveID("Second"), Name: "Second", Kind: "other", SourceRefs: occurrenceRefs(30, 30)},
}}} }}}
references := requiredRegistryReferences(t, "Later", "First", "Second") references := requiredRegistryReferences(t, "Later", "First", "Second")
req := extractionRequest() req := extractionRequest()
@@ -81,16 +81,16 @@ func TestExtractUsesDocumentOrderForReferencesAndInteractions(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Extract() error = %v", err) t.Fatalf("Extract() error = %v", err)
} }
if got := interactionNames(result.Value); !reflect.DeepEqual(got, []string{"First", "Second", "Later"}) { if got := occurrenceNames(result.Value); !reflect.DeepEqual(got, []string{"First", "Second", "Later"}) {
t.Fatalf("interaction order = %#v, want document chronology with stable equal-evidence ties", got) t.Fatalf("occurrence order = %#v, want document chronology with stable equal-evidence ties", got)
} }
refs := result.Value.Occurrences[0].SourceRefs refs := result.Value.Occurrences[0].SourceRefs
if got := []int{refs[0].StartUnitID, refs[1].StartUnitID, refs[2].StartUnitID}; !reflect.DeepEqual(got, []int{30, 10, 999}) { if got := []int{refs[0].StartUnitID, refs[1].StartUnitID, refs[2].StartUnitID}; !reflect.DeepEqual(got, []int{30, 10, 999}) {
t.Fatalf("source refs = %#v, want document order with exact duplicate removed", refs) t.Fatalf("source refs = %#v, want document order with exact duplicate removed", refs)
} }
refs[0].StartUnitID = 777 refs[0].StartUnitID = 777
for _, interaction := range client.response.Occurrences { for _, occurrence := range client.response.Occurrences {
for _, ref := range interaction.SourceRefs { for _, ref := range occurrence.SourceRefs {
if ref.StartUnitID == 777 { if ref.StartUnitID == 777 {
t.Fatal("result source references alias the model response") t.Fatal("result source references alias the model response")
} }
@@ -102,14 +102,14 @@ func TestNewRequiresLLMAndRejectsAmbiguousReferenceSets(t *testing.T) {
if _, err := New(nil, Options{}); err == nil || !strings.Contains(err.Error(), "LLM client") { if _, err := New(nil, Options{}); err == nil || !strings.Contains(err.Error(), "LLM client") {
t.Fatalf("New(nil) error = %v", err) t.Fatalf("New(nil) error = %v", err)
} }
if _, err := New(&fakeInteractionsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one reference set") { if _, err := New(&fakeOccurrencesLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one reference set") {
t.Fatalf("New() error = %v", err) t.Fatalf("New() error = %v", err)
} }
} }
func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(t *testing.T) { func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(t *testing.T) {
client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{{ client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{{
NPCID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", Kind: "dialogue", SourceRefs: interactionRefs(10, 10), NPCID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", Kind: "dialogue", SourceRefs: occurrenceRefs(10, 10),
}}}} }}}}
references := requiredRegistryReferences(t, "Mira Thorn", "Hooded Guard") references := requiredRegistryReferences(t, "Mira Thorn", "Hooded Guard")
req := extractionRequest() req := extractionRequest()
@@ -130,7 +130,7 @@ func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(t *testing.T) {
if strings.Contains(string(request.Inputs["transcript"].Content), "other-session") { if strings.Contains(string(request.Inputs["transcript"].Content), "other-session") {
t.Fatal("transcript input contains registry evidence") t.Fatal("transcript input contains registry evidence")
} }
metadata, err := json.Marshal(newExtractor(t, &fakeInteractionsLLMClient{}, references).ManifestMetadata()) metadata, err := json.Marshal(newExtractor(t, &fakeOccurrencesLLMClient{}, references).ManifestMetadata())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -140,7 +140,7 @@ func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(t *testing.T) {
} }
func TestExtractRequiresBoundRegistryBeforeLLMCall(t *testing.T) { func TestExtractRequiresBoundRegistryBeforeLLMCall(t *testing.T) {
client := &fakeInteractionsLLMClient{} client := &fakeOccurrencesLLMClient{}
if _, err := newExtractor(t, client).Extract(context.Background(), extractionRequest()); err == nil || !strings.Contains(err.Error(), "NPC registry reference is required") { if _, err := newExtractor(t, client).Extract(context.Background(), extractionRequest()); err == nil || !strings.Contains(err.Error(), "NPC registry reference is required") {
t.Fatalf("Extract() error = %v, want required registry failure", err) t.Fatalf("Extract() error = %v, want required registry failure", err)
} }
@@ -169,11 +169,11 @@ func TestExtractRejectsUnknownIDsAndMismatchedNames(t *testing.T) {
occurrence occurrenceResponse occurrence occurrenceResponse
want string want string
}{ }{
{"unknown ID", occurrenceResponse{NPCID: "npc:unknown", Name: "Mira Thorn", Kind: "dialogue", SourceRefs: interactionRefs(10, 10)}, "npc_id is not in the NPC registry"}, {"unknown ID", occurrenceResponse{NPCID: "npc:unknown", Name: "Mira Thorn", Kind: "dialogue", SourceRefs: occurrenceRefs(10, 10)}, "npc_id is not in the NPC registry"},
{"mismatched name", occurrenceResponse{NPCID: identity.DeriveID("Mira Thorn"), Name: "Hooded Guard", Kind: "dialogue", SourceRefs: interactionRefs(10, 10)}, "name does not match npc_id"}, {"mismatched name", occurrenceResponse{NPCID: identity.DeriveID("Mira Thorn"), Name: "Hooded Guard", Kind: "dialogue", SourceRefs: occurrenceRefs(10, 10)}, "name does not match npc_id"},
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{test.occurrence}}} client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{test.occurrence}}}
req := extractionRequest() req := extractionRequest()
req.References = references req.References = references
if _, err := newExtractor(t, client, references).Extract(context.Background(), req); err == nil || !strings.Contains(err.Error(), test.want) { if _, err := newExtractor(t, client, references).Extract(context.Background(), req); err == nil || !strings.Contains(err.Error(), test.want) {
@@ -184,7 +184,7 @@ func TestExtractRejectsUnknownIDsAndMismatchedNames(t *testing.T) {
} }
func TestExtractResolvesGeneratedRegistryAtOperationTime(t *testing.T) { func TestExtractResolvesGeneratedRegistryAtOperationTime(t *testing.T) {
client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{}}} client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{}}}
references := requiredRegistryReferences(t, "Mira Thorn") references := requiredRegistryReferences(t, "Mira Thorn")
req := extractionRequest() req := extractionRequest()
req.References = references req.References = references
@@ -212,12 +212,12 @@ func TestExtractAcceptsEmptyBoundRegistryAndEmptyResponse(t *testing.T) {
Items: []contracts.ReferenceItem{{SlotName: NPCRegistryReferenceSlot, MediaType: npccodec.MediaType, Content: content}}, Items: []contracts.ReferenceItem{{SlotName: NPCRegistryReferenceSlot, MediaType: npccodec.MediaType, Content: content}},
}, },
}} }}
client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{}}} client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{}}}
req := extractionRequest() req := extractionRequest()
req.References = references req.References = references
result, err := newExtractor(t, client, references).Extract(context.Background(), req) result, err := newExtractor(t, client, references).Extract(context.Background(), req)
if err != nil || result.Value.Occurrences == nil || len(result.Value.Occurrences) != 0 { if err != nil || result.Value.Occurrences == nil || len(result.Value.Occurrences) != 0 {
t.Fatalf("Extract() = %#v, %v; want present empty interactions", result, err) t.Fatalf("Extract() = %#v, %v; want present empty occurrences", result, err)
} }
} }
@@ -225,7 +225,7 @@ func TestExtractRejectsInvalidRequestsAndProviderFailures(t *testing.T) {
references := requiredRegistryReferences(t, "Mira Thorn") references := requiredRegistryReferences(t, "Mira Thorn")
valid := extractionRequest() valid := extractionRequest()
valid.References = references valid.References = references
extractor := newExtractor(t, &fakeInteractionsLLMClient{}, references) extractor := newExtractor(t, &fakeOccurrencesLLMClient{}, references)
var nilExtractor *Extractor var nilExtractor *Extractor
for _, test := range []struct { for _, test := range []struct {
name string name string
@@ -244,14 +244,14 @@ func TestExtractRejectsInvalidRequestsAndProviderFailures(t *testing.T) {
} }
}) })
} }
if _, err := newExtractor(t, &fakeInteractionsLLMClient{err: errors.New("provider unavailable")}, references).Extract(context.Background(), valid); err == nil || !strings.Contains(err.Error(), "provider unavailable") { if _, err := newExtractor(t, &fakeOccurrencesLLMClient{err: errors.New("provider unavailable")}, references).Extract(context.Background(), valid); err == nil || !strings.Contains(err.Error(), "provider unavailable") {
t.Fatalf("provider error = %v", err) t.Fatalf("provider error = %v", err)
} }
} }
func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) { func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) {
got := ModuleSpec() got := ModuleSpec()
if got.Key != Key || got.Stage != pipeline.StageExtract || got.ArtifactKind != dnd.NPCOccurrenceListKind || !reflect.DeepEqual(got.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(got.Provides, []string{"dnd.npc_interactions"}) { if got.Key != Key || got.Stage != pipeline.StageExtract || got.ArtifactKind != dnd.NPCOccurrenceListKind || !reflect.DeepEqual(got.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(got.Provides, []string{"dnd.npc_occurrences"}) {
t.Fatalf("ModuleSpec() = %#v", got) t.Fatalf("ModuleSpec() = %#v", got)
} }
var registrySlot contracts.ReferenceSlot var registrySlot contracts.ReferenceSlot
@@ -267,9 +267,9 @@ func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) {
if ModuleSpec().ReferenceSlots[0].AcceptedMediaTypes[0] == "changed" { if ModuleSpec().ReferenceSlots[0].AcceptedMediaTypes[0] == "changed" {
t.Fatal("ModuleSpec() returned mutable reference slots") t.Fatal("ModuleSpec() returned mutable reference slots")
} }
extractorSlots := newExtractor(t, &fakeInteractionsLLMClient{}).ReferenceSlots() extractorSlots := newExtractor(t, &fakeOccurrencesLLMClient{}).ReferenceSlots()
extractorSlots[0].AcceptedMediaTypes[0] = "changed" extractorSlots[0].AcceptedMediaTypes[0] = "changed"
if newExtractor(t, &fakeInteractionsLLMClient{}).ReferenceSlots()[0].AcceptedMediaTypes[0] == "changed" { if newExtractor(t, &fakeOccurrencesLLMClient{}).ReferenceSlots()[0].AcceptedMediaTypes[0] == "changed" {
t.Fatal("ReferenceSlots() returned mutable reference slots") t.Fatal("ReferenceSlots() returned mutable reference slots")
} }
registry := pipeline.NewExtractorRegistry() registry := pipeline.NewExtractorRegistry()
@@ -284,7 +284,7 @@ func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) {
} }
references := requiredRegistryReferences(t, "Mira Thorn") references := requiredRegistryReferences(t, "Mira Thorn")
extractor := newExtractor(t, &fakeInteractionsLLMClient{}, references) extractor := newExtractor(t, &fakeOccurrencesLLMClient{}, references)
metadata := extractor.ManifestMetadata() metadata := extractor.ManifestMetadata()
for key, want := range map[string]string{ for key, want := range map[string]string{
"prompt_id": PromptID, "prompt_version": SchemaVersion, "mapping_policy": mappingPolicy, "prompt_id": PromptID, "prompt_version": SchemaVersion, "mapping_policy": mappingPolicy,
@@ -320,7 +320,7 @@ func extractionRequest() contracts.TypedExtractionRequest {
return contracts.TypedExtractionRequest{ return contracts.TypedExtractionRequest{
Source: doc, Chunk: chunk, Source: doc, Chunk: chunk,
SourceInput: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "sha256:chunk", "file:///session-alpha.json"), SourceInput: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "sha256:chunk", "file:///session-alpha.json"),
SessionID: "session-123", LLMProfile: "profile-npc-interactions", SessionID: "session-123", LLMProfile: "profile-npc-occurrences",
} }
} }
@@ -359,22 +359,22 @@ func requiredRegistryReferences(t *testing.T, names ...string) contracts.Referen
}} }}
} }
func interactionRefs(start, end int) []interactionSourceRefResponse { func occurrenceRefs(start, end int) []occurrenceSourceRefResponse {
return []interactionSourceRefResponse{{StartUnitID: start, EndUnitID: end}} return []occurrenceSourceRefResponse{{StartUnitID: start, EndUnitID: end}}
} }
func interactionNames(value dnd.NPCOccurrenceList) []string { func occurrenceNames(value dnd.NPCOccurrenceList) []string {
names := make([]string, len(value.Occurrences)) names := make([]string, len(value.Occurrences))
for index, interaction := range value.Occurrences { for index, occurrence := range value.Occurrences {
names[index] = interaction.Name names[index] = occurrence.Name
} }
return names return names
} }
func interactionKinds(value dnd.NPCOccurrenceList) []dnd.NPCOccurrenceKind { func occurrenceKinds(value dnd.NPCOccurrenceList) []dnd.NPCOccurrenceKind {
kinds := make([]dnd.NPCOccurrenceKind, len(value.Occurrences)) kinds := make([]dnd.NPCOccurrenceKind, len(value.Occurrences))
for index, interaction := range value.Occurrences { for index, occurrence := range value.Occurrences {
kinds[index] = interaction.Kind kinds[index] = occurrence.Kind
} }
return kinds return kinds
} }
@@ -393,13 +393,13 @@ func newExtractor(t *testing.T, client contracts.StructuredLLMClient, references
return extractor return extractor
} }
type fakeInteractionsLLMClient struct { type fakeOccurrencesLLMClient struct {
response extractionResponse response extractionResponse
err error err error
requests []contracts.StructuredCompletionRequest requests []contracts.StructuredCompletionRequest
} }
func (client *fakeInteractionsLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) { func (client *fakeOccurrencesLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
client.requests = append(client.requests, cloneStructuredCompletionRequest(req)) client.requests = append(client.requests, cloneStructuredCompletionRequest(req))
if client.err != nil { if client.err != nil {
return contracts.StructuredCompletionResponse{}, client.err return contracts.StructuredCompletionResponse{}, client.err

View File

@@ -0,0 +1,17 @@
package npcoccurrences
type extractionResponse struct {
Occurrences []occurrenceResponse `json:"occurrences"`
}
type occurrenceResponse struct {
NPCID string `json:"npc_id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceRefs []occurrenceSourceRefResponse `json:"source_refs"`
}
type occurrenceSourceRefResponse struct {
StartUnitID int `json:"start_unit_id"`
EndUnitID int `json:"end_unit_id"`
}

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"encoding/json" "encoding/json"
@@ -12,7 +12,7 @@ func TestExtractionResponsePreservesValidatorOwnedSemantics(t *testing.T) {
t.Fatalf("json.Unmarshal() error = %v", err) t.Fatalf("json.Unmarshal() error = %v", err)
} }
occurrence := response.Occurrences[0] occurrence := response.Occurrences[0]
if occurrence.NPCID != "" || occurrence.Name != "" || occurrence.Kind != "unsupported" || occurrence.SourceRefs[0] != (interactionSourceRefResponse{StartUnitID: 0, EndUnitID: -1}) { if occurrence.NPCID != "" || occurrence.Name != "" || occurrence.Kind != "unsupported" || occurrence.SourceRefs[0] != (occurrenceSourceRefResponse{StartUnitID: 0, EndUnitID: -1}) {
t.Fatalf("decoded response = %#v", occurrence) t.Fatalf("decoded response = %#v", occurrence)
} }
} }

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"fmt" "fmt"
@@ -14,7 +14,7 @@ import (
const promptAssetRoot = "assets/prompts" const promptAssetRoot = "assets/prompts"
var promptAssetManifest = shared.PromptAssetManifest{ var promptAssetManifest = shared.PromptAssetManifest{
ModuleDir: "dnd.npc_interactions", ModuleDir: "dnd.npc_occurrences",
ModuleFiles: []promptfs.ModulePromptFile{ ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"}, {Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "instructions.md", Path: "prompts/instructions.md"}, {Name: "instructions.md", Path: "prompts/instructions.md"},
@@ -30,9 +30,9 @@ var promptAssetManifest = shared.PromptAssetManifest{
} }
func moduleAssetFS() (fs.FS, error) { func moduleAssetFS() (fs.FS, error) {
assets, err := fs.Sub(rootassets.FS(), "dnd/npc-interactions") assets, err := fs.Sub(rootassets.FS(), "dnd/npc-occurrences")
if err != nil { if err != nil {
return nil, fmt.Errorf("scope NPC-interaction assets: %w", err) return nil, fmt.Errorf("scope NPC-occurrence assets: %w", err)
} }
return assets, nil return assets, nil
} }
@@ -44,14 +44,14 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
} }
promptFS, err := promptAssetManifest.PromptFS(assets) promptFS, err := promptAssetManifest.PromptFS(assets)
if err != nil { if err != nil {
return fmt.Errorf("prepare NPC-interaction prompt assets: %w", err) return fmt.Errorf("prepare NPC-occurrence prompt assets: %w", err)
} }
if err := registry.RegisterPromptFS(promptFS, promptAssetRoot); err != nil { if err := registry.RegisterPromptFS(promptFS, promptAssetRoot); err != nil {
return err return err
} }
schemas, err := fs.Sub(assets, "schemas") schemas, err := fs.Sub(assets, "schemas")
if err != nil { if err != nil {
return fmt.Errorf("scope NPC-interaction schemas: %w", err) return fmt.Errorf("scope NPC-occurrence schemas: %w", err)
} }
return registry.RegisterSchemaFS(schemas, ".") return registry.RegisterSchemaFS(schemas, ".")
} }

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"context" "context"
@@ -11,7 +11,7 @@ import (
"gitea.maximumdirect.net/eric/promptkit" "gitea.maximumdirect.net/eric/promptkit"
) )
func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) { func TestRegisterPromptAssetsAndPrepareOccurrencePrompt(t *testing.T) {
registry := llm.NewAssetRegistry() registry := llm.NewAssetRegistry()
if err := RegisterPromptAssets(registry); err != nil { if err := RegisterPromptAssets(registry); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -28,21 +28,21 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
options = append(options, promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{ options = append(options, promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
ID: "npc-interactions-test-profile", Endpoint: "http://127.0.0.1:1/v1", Model: "npc-interactions-test-model", ID: "npc-occurrences-test-profile", Endpoint: "http://127.0.0.1:1/v1", Model: "npc-occurrences-test-model",
}))) })))
engine, err := promptkit.NewEngine(promptkit.Config{Timeout: time.Second}, options...) engine, err := promptkit.NewEngine(promptkit.Config{Timeout: time.Second}, options...)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
transcript := `{"units":[{"sentinel":"interaction-transcript"}]}` transcript := `{"units":[{"sentinel":"occurrence-transcript"}]}`
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "npc-interactions-test-profile", PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "npc-occurrences-test-profile",
Inputs: map[string]promptkit.ArtifactRef{ Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.InlineWithURI("file:///session.json", transcript), "transcript": promptkit.InlineWithURI("file:///session.json", transcript),
"players": promptkit.Inline("interaction-player"), "players": promptkit.Inline("occurrence-player"),
"party": promptkit.Inline("Mira: ranger"), "party": promptkit.Inline("Mira: ranger"),
"glossary": promptkit.Inline("Greencloak: title"), "glossary": promptkit.Inline("Greencloak: title"),
"npc_registry": promptkit.Inline(`{"npcs":[{"id":"npc:sha256:test","name":"interaction-npc"}]}`), "npc_registry": promptkit.Inline(`{"npcs":[{"id":"npc:sha256:test","name":"occurrence-npc"}]}`),
}, },
}) })
if err != nil { if err != nil {
@@ -58,7 +58,7 @@ func TestPromptMetadataDoesNotExposeAssetContent(t *testing.T) {
if err != nil || !strings.HasPrefix(hash, "sha256:") { if err != nil || !strings.HasPrefix(hash, "sha256:") {
t.Fatalf("promptAssetMetadata() = %q, %v", hash, err) t.Fatalf("promptAssetMetadata() = %q, %v", hash, err)
} }
metadata := newExtractor(t, &fakeInteractionsLLMClient{}).ManifestMetadata() metadata := newExtractor(t, &fakeOccurrencesLLMClient{}).ManifestMetadata()
for _, forbidden := range []string{"common-dnd-system", "dnd_npc_occurrences_llm.v1.json"} { for _, forbidden := range []string{"common-dnd-system", "dnd_npc_occurrences_llm.v1.json"} {
if strings.Contains(strings.Join(mapValues(metadata), " "), forbidden) { if strings.Contains(strings.Join(mapValues(metadata), " "), forbidden) {
t.Fatalf("metadata leaked prompt or schema content %q: %#v", forbidden, metadata) t.Fatalf("metadata leaked prompt or schema content %q: %#v", forbidden, metadata)

View File

@@ -1,9 +1,9 @@
package npcinteractions package npcoccurrences
import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
const ( const (
PromptID = "dnd.npc_interactions" PromptID = "dnd.npc_occurrences"
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_npc_occurrences_llm") ResponseSchemaKey = llm.ResponseSchemaKey("dnd_npc_occurrences_llm")
ResponseSchemaID = "notarius.dnd.npc_occurrences.llm" ResponseSchemaID = "notarius.dnd.npc_occurrences.llm"
ResponseSchemaName = "notarius_dnd_npc_occurrences_llm_v1" ResponseSchemaName = "notarius_dnd_npc_occurrences_llm_v1"

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"bytes" "bytes"
@@ -17,7 +17,7 @@ func TestResponseSchemaOwnsOnlyPrivateStructuralContract(t *testing.T) {
if schema.Key != ResponseSchemaKey || schema.ID != ResponseSchemaID || schema.Name != ResponseSchemaName || schema.Version != SchemaVersion || !strings.HasPrefix(schema.SHA256, "sha256:") || !json.Valid(schema.JSONSchema) { if schema.Key != ResponseSchemaKey || schema.ID != ResponseSchemaID || schema.Name != ResponseSchemaName || schema.Version != SchemaVersion || !strings.HasPrefix(schema.SHA256, "sha256:") || !json.Valid(schema.JSONSchema) {
t.Fatalf("schema = %#v", schema) t.Fatalf("schema = %#v", schema)
} }
valid := validInteractionResponse() valid := validOccurrenceResponse()
content, err := json.Marshal(valid) content, err := json.Marshal(valid)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -26,11 +26,11 @@ func TestResponseSchemaOwnsOnlyPrivateStructuralContract(t *testing.T) {
t.Fatalf("valid response rejected: %v", err) t.Fatalf("valid response rejected: %v", err)
} }
semanticCandidate := validInteractionResponse() semanticCandidate := validOccurrenceResponse()
interaction := semanticCandidate["occurrences"].([]any)[0].(map[string]any) occurrence := semanticCandidate["occurrences"].([]any)[0].(map[string]any)
interaction["name"] = "" occurrence["name"] = ""
interaction["kind"] = "unsupported" occurrence["kind"] = "unsupported"
ref := interaction["source_refs"].([]any)[0].(map[string]any) ref := occurrence["source_refs"].([]any)[0].(map[string]any)
ref["start_unit_id"] = 0 ref["start_unit_id"] = 0
ref["end_unit_id"] = -1 ref["end_unit_id"] = -1
content, err = json.Marshal(semanticCandidate) content, err = json.Marshal(semanticCandidate)
@@ -50,7 +50,7 @@ func TestResponseSchemaOwnsOnlyPrivateStructuralContract(t *testing.T) {
record["source_refs"].([]any)[0].(map[string]any)["source_id"] = "assigned later" record["source_refs"].([]any)[0].(map[string]any)["source_id"] = "assigned later"
}, },
} { } {
candidate := validInteractionResponse() candidate := validOccurrenceResponse()
mutate(candidate["occurrences"].([]any)[0].(map[string]any)) mutate(candidate["occurrences"].([]any)[0].(map[string]any))
content, err := json.Marshal(candidate) content, err := json.Marshal(candidate)
if err != nil { if err != nil {
@@ -79,7 +79,7 @@ func TestResponseSchemaIsDefensiveAndContentSafe(t *testing.T) {
} }
} }
func validInteractionResponse() map[string]any { func validOccurrenceResponse() map[string]any {
return map[string]any{"occurrences": []any{map[string]any{ return map[string]any{"occurrences": []any{map[string]any{
"npc_id": "npc:sha256:test", "name": "Mira Thorn", "kind": "dialogue", "npc_id": "npc:sha256:test", "name": "Mira Thorn", "kind": "dialogue",
"source_refs": []any{map[string]any{"start_unit_id": 1, "end_unit_id": 2}}, "source_refs": []any{map[string]any{"start_unit_id": 1, "end_unit_id": 2}},

View File

@@ -1,5 +1,5 @@
// Package npcinteractions normalizes merged D&D NPC interaction candidates. // Package npcoccurrences normalizes merged D&D NPC occurrence candidates.
package npcinteractions package npcoccurrences
import ( import (
"context" "context"
@@ -10,21 +10,21 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"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"
interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcinteractions" occurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcoccurrences"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
) )
const ( const (
Key = "dnd/npc-interactions" Key = "dnd/npc-occurrences"
normalizationPolicy = "dnd.npc_interactions.normalize.v2" normalizationPolicy = "dnd.npc_occurrences.normalize.v2"
NormalizationPolicy = normalizationPolicy NormalizationPolicy = normalizationPolicy
ReasonCodeSourceRefsNormalized = "source_references_normalized" ReasonCodeSourceRefsNormalized = "source_references_normalized"
ReasonCodeInteractionsReordered = "npc_interactions_reordered" ReasonCodeOccurrencesReordered = "npc_occurrences_reordered"
ReasonCodeDuplicateCollapsed = "duplicate_npc_interaction_collapsed" ReasonCodeDuplicateCollapsed = "duplicate_npc_occurrence_collapsed"
ReasonCodeWarningsOmitted = "npc_interaction_normalization_warnings_omitted" ReasonCodeWarningsOmitted = "npc_occurrence_normalization_warnings_omitted"
) )
const ( const (
@@ -36,10 +36,10 @@ var requiredCapabilities = []string{"merged"}
var providedCapabilities = []string{"normalized"} var providedCapabilities = []string{"normalized"}
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
Glossary: "Optional campaign glossary reference material used only for interaction disambiguation.", Glossary: "Optional campaign glossary reference material used only for occurrence disambiguation.",
Party: "Optional party roster reference material used only for interaction disambiguation.", Party: "Optional party roster reference material used only for occurrence disambiguation.",
Players: "Optional player list reference material used only for interaction disambiguation.", Players: "Optional player list reference material used only for occurrence disambiguation.",
Roster: "Deprecated alias for party roster reference material used only for interaction disambiguation.", Roster: "Deprecated alias for party roster reference material used only for occurrence disambiguation.",
} }
var _ contracts.Normalizer[dnd.NPCOccurrenceList] = (*Normalizer)(nil) var _ contracts.Normalizer[dnd.NPCOccurrenceList] = (*Normalizer)(nil)
@@ -152,7 +152,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
} }
sort.SliceStable(records, func(left, right int) bool { sort.SliceStable(records, func(left, right int) bool {
return interactionmodel.Less(order, records[left].occurrence, records[right].occurrence) return occurrencemodel.Less(order, records[left].occurrence, records[right].occurrence)
}) })
for position, record := range records { for position, record := range records {
if position == record.inputIndex { if position == record.inputIndex {
@@ -160,7 +160,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
} }
warnings = append(warnings, contracts.Warning{ warnings = append(warnings, contracts.Warning{
Scope: occurrenceScope(record.inputIndex), Scope: occurrenceScope(record.inputIndex),
ReasonCode: ReasonCodeInteractionsReordered, ReasonCode: ReasonCodeOccurrencesReordered,
Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology", record.inputIndex, position), Message: fmt.Sprintf("input index %d moved to normalized position %d by source chronology", record.inputIndex, position),
}) })
} }
@@ -168,7 +168,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd
output, duplicateWarnings := collapseDuplicates(records, documentIndex) output, duplicateWarnings := collapseDuplicates(records, documentIndex)
warnings = append(warnings, duplicateWarnings...) warnings = append(warnings, duplicateWarnings...)
return dnd.NPCOccurrenceList{Occurrences: output}, return dnd.NPCOccurrenceList{Occurrences: output},
diagnostics.LimitWarnings(warnings, "npc_interactions", ReasonCodeWarningsOmitted), nil diagnostics.LimitWarnings(warnings, "npc_occurrences", ReasonCodeWarningsOmitted), nil
} }
func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrence, bool, error) { func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, registry *npcregistry.Registry) (dnd.NPCOccurrence, bool, error) {
@@ -181,7 +181,7 @@ func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, r
} }
output := cloneOccurrence(input) output := cloneOccurrence(input)
output.SourceRefs = order.Canonicalize(input.SourceRefs) output.SourceRefs = order.Canonicalize(input.SourceRefs)
return output, !interactionmodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs), nil return output, !occurrencemodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs), nil
} }
func cloneOccurrence(input dnd.NPCOccurrence) dnd.NPCOccurrence { func cloneOccurrence(input dnd.NPCOccurrence) dnd.NPCOccurrence {
@@ -205,11 +205,11 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen
groups := make([]duplicateGroup, 0) groups := make([]duplicateGroup, 0)
groupByKey := make(map[string]int) groupByKey := make(map[string]int)
for index, record := range records { for index, record := range records {
if !interactionmodel.ValidSourceRefs(documentIndex, record.occurrence.SourceRefs) { if !occurrencemodel.ValidSourceRefs(documentIndex, record.occurrence.SourceRefs) {
keep[index] = true keep[index] = true
continue continue
} }
key := interactionmodel.ExactIdentity(record.occurrence) key := occurrencemodel.ExactIdentity(record.occurrence)
groupIndex, exists := groupByKey[key] groupIndex, exists := groupByKey[key]
if !exists { if !exists {
groupByKey[key] = len(groups) groupByKey[key] = len(groups)
@@ -243,7 +243,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
Scope: occurrenceScope(retainedIndex), Scope: occurrenceScope(retainedIndex),
ReasonCode: ReasonCodeDuplicateCollapsed, ReasonCode: ReasonCodeDuplicateCollapsed,
Message: diagnostics.Aggregate( Message: diagnostics.Aggregate(
fmt.Sprintf("duplicate NPC interaction collapsed; retained input index %d", retainedIndex), issues), fmt.Sprintf("duplicate NPC occurrence collapsed; retained input index %d", retainedIndex), issues),
} }
} }
@@ -253,7 +253,7 @@ func referenceSlots() []contracts.ReferenceSlot {
slots := shared.ReferenceSlots(referenceSlotDescriptions) slots := shared.ReferenceSlots(referenceSlotDescriptions)
slots = append(slots, contracts.ReferenceSlot{ slots = append(slots, contracts.ReferenceSlot{
Name: NPCRegistryReferenceSlot, Name: NPCRegistryReferenceSlot,
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.", Description: "Required normalized NPC registry used only for occurrence identity grounding, never as occurrence evidence.",
Required: true, Required: true,
AcceptedMediaTypes: []string{"application/json"}, AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
@@ -295,5 +295,5 @@ func DecodeOptions(options map[string]any) (Options, error) {
func validateOptions(options map[string]any) error { _, err := DecodeOptions(options); return err } func validateOptions(options map[string]any) error { _, err := DecodeOptions(options); return err }
func normalizerErrorf(format string, args ...any) error { func normalizerErrorf(format string, args ...any) error {
return fmt.Errorf("dnd NPC interactions normalizer: "+format, args...) return fmt.Errorf("dnd NPC occurrences normalizer: "+format, args...)
} }

View File

@@ -1,4 +1,4 @@
package npcinteractions package npcoccurrences
import ( import (
"context" "context"
@@ -31,7 +31,7 @@ func TestNormalizeValidatesPairsAndClones(t *testing.T) {
} }
got := result.Value.Occurrences[0] got := result.Value.Occurrences[0]
if got.NPCID != identity.DeriveID("Ária") || got.Name != "Ária" || !reflect.DeepEqual(got.SourceRefs, []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}) { if got.NPCID != identity.DeriveID("Ária") || got.Name != "Ária" || !reflect.DeepEqual(got.SourceRefs, []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}) {
t.Fatalf("normalized interaction = %#v", got) t.Fatalf("normalized occurrence = %#v", got)
} }
if !hasWarning(result.Warnings, ReasonCodeSourceRefsNormalized) { if !hasWarning(result.Warnings, ReasonCodeSourceRefsNormalized) {
t.Fatalf("warnings = %#v", result.Warnings) t.Fatalf("warnings = %#v", result.Warnings)
@@ -77,7 +77,7 @@ func TestNormalizeRejectsUnknownIDsAndMismatchedNames(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for _, occurrence := range []dnd.NPCOccurrence{ for _, occurrence := range []dnd.NPCOccurrence{
interaction("Unknown NPC", dnd.NPCOccurrenceKindOther, source.SourceRef{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}), occurrence("Unknown NPC", dnd.NPCOccurrenceKindOther, source.SourceRef{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}),
{NPCID: identity.DeriveID("Ária"), Name: "Borin", Kind: dnd.NPCOccurrenceKindOther, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {NPCID: identity.DeriveID("Ária"), Name: "Borin", Kind: dnd.NPCOccurrenceKindOther, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
} { } {
input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{occurrence}} input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{occurrence}}
@@ -92,14 +92,14 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) {
ref := func(unit int) source.SourceRef { ref := func(unit int) source.SourceRef {
return source.SourceRef{SourceID: doc.ID, StartUnitID: unit, EndUnitID: unit} return source.SourceRef{SourceID: doc.ID, StartUnitID: unit, EndUnitID: unit}
} }
first := interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(50)) first := occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(50))
input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{ input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{
interaction("Borin", dnd.NPCOccurrenceKindMentioned, ref(90)), occurrence("Borin", dnd.NPCOccurrenceKindMentioned, ref(90)),
first, first,
first, first,
interaction("Ária", dnd.NPCOccurrenceKindCombatAlly, ref(50)), occurrence("Ária", dnd.NPCOccurrenceKindCombatAlly, ref(50)),
interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(10)), occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(10)),
interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(999)), occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(999)),
}} }}
normalizer, err := New(Options{}, npcReferences(t)) normalizer, err := New(Options{}, npcReferences(t))
if err != nil { if err != nil {
@@ -111,12 +111,12 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) {
} }
got := result.Value.Occurrences got := result.Value.Occurrences
if len(got) != 5 { if len(got) != 5 {
t.Fatalf("interaction count = %d, want 5: %#v", len(got), got) t.Fatalf("occurrence count = %d, want 5: %#v", len(got), got)
} }
if got[0].Kind != dnd.NPCOccurrenceKindCombatAlly || got[0].SourceRefs[0].StartUnitID != 50 || got[1].SourceRefs[0].StartUnitID != 50 || got[2].SourceRefs[0].StartUnitID != 10 || got[3].SourceRefs[0].StartUnitID != 90 || got[4].SourceRefs[0].StartUnitID != 999 { if got[0].Kind != dnd.NPCOccurrenceKindCombatAlly || got[0].SourceRefs[0].StartUnitID != 50 || got[1].SourceRefs[0].StartUnitID != 50 || got[2].SourceRefs[0].StartUnitID != 10 || got[3].SourceRefs[0].StartUnitID != 90 || got[4].SourceRefs[0].StartUnitID != 999 {
t.Fatalf("canonical order = %#v", got) t.Fatalf("canonical order = %#v", got)
} }
if !hasWarning(result.Warnings, ReasonCodeInteractionsReordered) || !hasWarning(result.Warnings, ReasonCodeDuplicateCollapsed) { if !hasWarning(result.Warnings, ReasonCodeOccurrencesReordered) || !hasWarning(result.Warnings, ReasonCodeDuplicateCollapsed) {
t.Fatalf("warnings = %#v", result.Warnings) t.Fatalf("warnings = %#v", result.Warnings)
} }
} }
@@ -147,7 +147,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) {
for index := range doc.Units { for index := range doc.Units {
doc.Units[index].ID = index + 1 doc.Units[index].ID = index + 1
unitID := count - index unitID := count - index
input.Occurrences[index] = interaction( input.Occurrences[index] = occurrence(
"Ária", "Ária",
dnd.NPCOccurrenceKindDialogue, dnd.NPCOccurrenceKindDialogue,
source.SourceRef{SourceID: doc.ID, StartUnitID: unitID, EndUnitID: unitID}, source.SourceRef{SourceID: doc.ID, StartUnitID: unitID, EndUnitID: unitID},
@@ -169,7 +169,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) {
} }
} }
func interaction(name string, kind dnd.NPCOccurrenceKind, ref source.SourceRef) dnd.NPCOccurrence { func occurrence(name string, kind dnd.NPCOccurrenceKind, ref source.SourceRef) dnd.NPCOccurrence {
return dnd.NPCOccurrence{NPCID: identity.DeriveID(name), Name: name, Kind: kind, SourceRefs: []source.SourceRef{ref}} return dnd.NPCOccurrence{NPCID: identity.DeriveID(name), Name: name, Kind: kind, SourceRefs: []source.SourceRef{ref}}
} }

View File

@@ -1,6 +1,6 @@
// Package npcinteractions owns canonical ordering and exact-identity rules for // Package npcoccurrences owns canonical ordering and exact-identity rules for
// D&D NPC interaction artifacts. // D&D NPC occurrence artifacts.
package npcinteractions package npcoccurrences
import ( import (
"strconv" "strconv"

View File

@@ -7,7 +7,7 @@ import (
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
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/locations"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
@@ -16,7 +16,7 @@ import (
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
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/locations"
interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions" scenedescriptionnormalize "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"
@@ -33,20 +33,20 @@ import (
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs" itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs"
itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness" itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness"
occurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants" locationoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants"
occurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry" locationoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry"
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape" locationoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape"
occurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs" locationoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs"
occurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_relatedness" locationoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_relatedness"
locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/identity" locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/identity"
locationshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/shape" locationshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/shape"
locationrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_refs" locationrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_refs"
locationrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_relatedness" locationrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_relatedness"
interactioninvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/invariants" npcoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/invariants"
interactionregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/registry" npcoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/registry"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" npcoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape"
interactionrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_refs" npcoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/source_refs"
interactionrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_relatedness" npcoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/source_relatedness"
npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/identity" npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/identity"
npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/shape" npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/shape"
npcsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/source_refs" npcsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/source_refs"
@@ -202,32 +202,32 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
}, },
}) })
}}, }},
{name: "npc interactions validator chain", register: func() error { {name: "npc occurrences validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{ return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
Module: interactionextract.Key, Module: occurrenceextract.Key,
Validators: []pipeline.ModuleBinding{ Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(validjson.Key),
pipeline.Binding(interactionshape.Key), pipeline.Binding(npcoccurrenceshape.Key),
pipeline.Binding(interactionregistry.Key), pipeline.Binding(npcoccurrenceregistry.Key),
pipeline.Binding(interactionrefs.Key), pipeline.Binding(npcoccurrencerefs.Key),
pipeline.Binding(validjsonschema.Key), pipeline.Binding(validjsonschema.Key),
pipeline.Binding(interactionrelatedness.Key), pipeline.Binding(npcoccurrencerelatedness.Key),
}, },
}) })
}}, }},
{name: "npc interactions normalize validator chain", register: func() error { {name: "npc occurrences normalize validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{ return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
Module: interactionnormalize.Key, Module: occurrencenormalize.Key,
Validators: []pipeline.ModuleBinding{ Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(validjson.Key),
pipeline.Binding(interactionshape.Key), pipeline.Binding(npcoccurrenceshape.Key),
pipeline.Binding(interactionregistry.Key), pipeline.Binding(npcoccurrenceregistry.Key),
pipeline.Binding(interactioninvariants.Key), pipeline.Binding(npcoccurrenceinvariants.Key),
pipeline.Binding(interactionrefs.Key), pipeline.Binding(npcoccurrencerefs.Key),
pipeline.Binding(validjsonschema.Key), pipeline.Binding(validjsonschema.Key),
pipeline.Binding(interactionrelatedness.Key), pipeline.Binding(npcoccurrencerelatedness.Key),
}, },
}) })
}}, }},
@@ -280,8 +280,8 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
return registry.Register(pipeline.ValidatorChainMapping{ return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageExtract, Module: locationoccurrenceextract.Key, Stage: pipeline.StageExtract, Module: locationoccurrenceextract.Key,
Validators: []pipeline.ModuleBinding{ Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(occurrenceshape.Key), pipeline.Binding(occurrenceregistry.Key), pipeline.Binding(validjson.Key), pipeline.Binding(locationoccurrenceshape.Key), pipeline.Binding(locationoccurrenceregistry.Key),
pipeline.Binding(occurrencerefs.Key), pipeline.Binding(validjsonschema.Key), pipeline.Binding(occurrencerelatedness.Key), pipeline.Binding(locationoccurrencerefs.Key), pipeline.Binding(validjsonschema.Key), pipeline.Binding(locationoccurrencerelatedness.Key),
}, },
}) })
}}, }},
@@ -289,9 +289,9 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
return registry.Register(pipeline.ValidatorChainMapping{ return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageNormalize, Module: locationoccurrencenormalize.Key, Stage: pipeline.StageNormalize, Module: locationoccurrencenormalize.Key,
Validators: []pipeline.ModuleBinding{ Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key), pipeline.Binding(occurrenceshape.Key), pipeline.Binding(occurrenceregistry.Key), pipeline.Binding(validjson.Key), pipeline.Binding(locationoccurrenceshape.Key), pipeline.Binding(locationoccurrenceregistry.Key),
pipeline.Binding(occurrenceinvariants.Key), pipeline.Binding(occurrencerefs.Key), pipeline.Binding(validjsonschema.Key), pipeline.Binding(locationoccurrenceinvariants.Key), pipeline.Binding(locationoccurrencerefs.Key), pipeline.Binding(validjsonschema.Key),
pipeline.Binding(occurrencerelatedness.Key), pipeline.Binding(locationoccurrencerelatedness.Key),
}, },
}) })
}}, }},

View File

@@ -19,8 +19,8 @@ func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error {
{name: "item events evidence", register: func() error { {name: "item events evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.ItemEventListKind, itemEventEvidence) return pipeline.RegisterArtifactEvidence(registry, dnd.ItemEventListKind, itemEventEvidence)
}}, }},
{name: "npc interactions evidence", register: func() error { {name: "npc occurrences evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.NPCOccurrenceListKind, npcInteractionEvidence) return pipeline.RegisterArtifactEvidence(registry, dnd.NPCOccurrenceListKind, npcOccurrenceEvidence)
}}, }},
{name: "scene descriptions evidence", register: func() error { {name: "scene descriptions evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence) return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence)
@@ -74,7 +74,7 @@ func itemEventEvidence(value dnd.ItemEventList) []source.SourceRef {
return append([]source.SourceRef(nil), refs...) return append([]source.SourceRef(nil), refs...)
} }
func npcInteractionEvidence(value dnd.NPCOccurrenceList) []source.SourceRef { func npcOccurrenceEvidence(value dnd.NPCOccurrenceList) []source.SourceRef {
var refs []source.SourceRef var refs []source.SourceRef
for _, record := range value.Occurrences { for _, record := range value.Occurrences {
refs = append(refs, record.SourceRefs...) refs = append(refs, record.SourceRefs...)

View File

@@ -112,7 +112,7 @@ func appendItemEventLists(values []dnd.ItemEventList) (dnd.ItemEventList, error)
return combined, nil return combined, nil
} }
func appendNPCInteractionLists(values []dnd.NPCOccurrenceList) (dnd.NPCOccurrenceList, error) { func appendNPCOccurrenceLists(values []dnd.NPCOccurrenceList) (dnd.NPCOccurrenceList, error) {
count := 0 count := 0
present := false present := false
for _, value := range values { for _, value := range values {

View File

@@ -10,7 +10,7 @@ import (
itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents" itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents"
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/locations"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
scenedescriptioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" scenedescriptioncodec "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"
@@ -19,7 +19,7 @@ import (
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
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/locations"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
@@ -28,7 +28,7 @@ import (
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
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/locations"
interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions" scenedescriptionnormalize "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"
@@ -45,7 +45,7 @@ func registerModules(registries pipeline.Registries) error {
{name: "combat turns codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, combatcodec.New()) }}, {name: "combat turns codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, combatcodec.New()) }},
{name: "enemy events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, enemyeventcodec.New()) }}, {name: "enemy events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, enemyeventcodec.New()) }},
{name: "item events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemeventcodec.New()) }}, {name: "item events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemeventcodec.New()) }},
{name: "npc interactions codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, interactioncodec.New()) }}, {name: "npc occurrences codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, occurrencecodec.New()) }},
{name: "scene descriptions codec", register: func() error { {name: "scene descriptions codec", register: func() error {
return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, scenedescriptioncodec.New()) return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, scenedescriptioncodec.New())
}}, }},
@@ -59,7 +59,7 @@ func registerModules(registries pipeline.Registries) error {
{name: "combat turns extractor", register: func() error { return combatextract.Register(registries.Extractors) }}, {name: "combat turns extractor", register: func() error { return combatextract.Register(registries.Extractors) }},
{name: "enemy events extractor", register: func() error { return enemyeventextract.Register(registries.Extractors) }}, {name: "enemy events extractor", register: func() error { return enemyeventextract.Register(registries.Extractors) }},
{name: "item events extractor", register: func() error { return itemeventextract.Register(registries.Extractors) }}, {name: "item events extractor", register: func() error { return itemeventextract.Register(registries.Extractors) }},
{name: "npc interactions extractor", register: func() error { return interactionextract.Register(registries.Extractors) }}, {name: "npc occurrences extractor", register: func() error { return occurrenceextract.Register(registries.Extractors) }},
{name: "scene descriptions extractor", register: func() error { return scenedescriptionextract.Register(registries.Extractors) }}, {name: "scene descriptions extractor", register: func() error { return scenedescriptionextract.Register(registries.Extractors) }},
{name: "locations extractor", register: func() error { return locationextract.Register(registries.Extractors) }}, {name: "locations extractor", register: func() error { return locationextract.Register(registries.Extractors) }},
{name: "location occurrences extractor", register: func() error { return locationoccurrenceextract.Register(registries.Extractors) }}, {name: "location occurrences extractor", register: func() error { return locationoccurrenceextract.Register(registries.Extractors) }},
@@ -78,8 +78,8 @@ func registerModules(registries pipeline.Registries) error {
{name: "item-event-list appendorder merger", register: func() error { {name: "item-event-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.ItemEventListKind, appendItemEventLists) return appendorder.RegisterTyped(registries.Mergers, dnd.ItemEventListKind, appendItemEventLists)
}}, }},
{name: "npc-interaction-list appendorder merger", register: func() error { {name: "npc-occurrence-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.NPCOccurrenceListKind, appendNPCInteractionLists) return appendorder.RegisterTyped(registries.Mergers, dnd.NPCOccurrenceListKind, appendNPCOccurrenceLists)
}}, }},
{name: "scene-description-list appendorder merger", register: func() error { {name: "scene-description-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.SceneDescriptionListKind, appendSceneDescriptionLists) return appendorder.RegisterTyped(registries.Mergers, dnd.SceneDescriptionListKind, appendSceneDescriptionLists)
@@ -95,7 +95,7 @@ func registerModules(registries pipeline.Registries) error {
{name: "combat turns normalizer", register: func() error { return combatnormalize.Register(registries.Normalizers) }}, {name: "combat turns normalizer", register: func() error { return combatnormalize.Register(registries.Normalizers) }},
{name: "enemy events normalizer", register: func() error { return enemyeventnormalize.Register(registries.Normalizers) }}, {name: "enemy events normalizer", register: func() error { return enemyeventnormalize.Register(registries.Normalizers) }},
{name: "item events normalizer", register: func() error { return itemeventnormalize.Register(registries.Normalizers) }}, {name: "item events normalizer", register: func() error { return itemeventnormalize.Register(registries.Normalizers) }},
{name: "npc interactions normalizer", register: func() error { return interactionnormalize.Register(registries.Normalizers) }}, {name: "npc occurrences normalizer", register: func() error { return occurrencenormalize.Register(registries.Normalizers) }},
{name: "scene descriptions normalizer", register: func() error { return scenedescriptionnormalize.Register(registries.Normalizers) }}, {name: "scene descriptions normalizer", register: func() error { return scenedescriptionnormalize.Register(registries.Normalizers) }},
{name: "locations normalizer", register: func() error { return locationnormalize.Register(registries.Normalizers) }}, {name: "locations normalizer", register: func() error { return locationnormalize.Register(registries.Normalizers) }},
{name: "location occurrences normalizer", register: func() error { return locationoccurrencenormalize.Register(registries.Normalizers) }}, {name: "location occurrences normalizer", register: func() error { return locationoccurrencenormalize.Register(registries.Normalizers) }},
@@ -114,7 +114,7 @@ func registerModules(registries pipeline.Registries) error {
{name: "item-event-list noop normalizer", register: func() error { {name: "item-event-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.ItemEventList](registries.Normalizers, dnd.ItemEventListKind) return noop.RegisterTyped[dnd.ItemEventList](registries.Normalizers, dnd.ItemEventListKind)
}}, }},
{name: "npc-interaction-list noop normalizer", register: func() error { {name: "npc-occurrence-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.NPCOccurrenceList](registries.Normalizers, dnd.NPCOccurrenceListKind) return noop.RegisterTyped[dnd.NPCOccurrenceList](registries.Normalizers, dnd.NPCOccurrenceListKind)
}}, }},
{name: "scene-description-list noop normalizer", register: func() error { {name: "scene-description-list noop normalizer", register: func() error {
@@ -139,7 +139,7 @@ func registerPromptAssets(assets *llm.AssetRegistry) error {
{name: "combat turns prompt assets", register: func() error { return combatextract.RegisterPromptAssets(assets) }}, {name: "combat turns prompt assets", register: func() error { return combatextract.RegisterPromptAssets(assets) }},
{name: "enemy events prompt assets", register: func() error { return enemyeventextract.RegisterPromptAssets(assets) }}, {name: "enemy events prompt assets", register: func() error { return enemyeventextract.RegisterPromptAssets(assets) }},
{name: "item events prompt assets", register: func() error { return itemeventextract.RegisterPromptAssets(assets) }}, {name: "item events prompt assets", register: func() error { return itemeventextract.RegisterPromptAssets(assets) }},
{name: "npc interactions prompt assets", register: func() error { return interactionextract.RegisterPromptAssets(assets) }}, {name: "npc occurrences prompt assets", register: func() error { return occurrenceextract.RegisterPromptAssets(assets) }},
{name: "scene descriptions prompt assets", register: func() error { return scenedescriptionextract.RegisterPromptAssets(assets) }}, {name: "scene descriptions prompt assets", register: func() error { return scenedescriptionextract.RegisterPromptAssets(assets) }},
{name: "locations prompt assets", register: func() error { return locationextract.RegisterPromptAssets(assets) }}, {name: "locations prompt assets", register: func() error { return locationextract.RegisterPromptAssets(assets) }},
{name: "location normalization prompt assets", register: func() error { return locationnormalize.RegisterPromptAssets(assets) }}, {name: "location normalization prompt assets", register: func() error { return locationnormalize.RegisterPromptAssets(assets) }},

View File

@@ -13,7 +13,7 @@ import (
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
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/locations"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
@@ -66,20 +66,20 @@ func TestExtractionPromptComposition(t *testing.T) {
promptID: enemyeventextract.PromptID, promptID: enemyeventextract.PromptID,
promptVersion: enemyeventextract.SchemaVersion, promptVersion: enemyeventextract.SchemaVersion,
inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{
"npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), "npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`),
"combat_turns": promptkit.Inline(`{"sentinel":"combat-turns-sentinel"}`), "combat_turns": promptkit.Inline(`{"sentinel":"combat-turns-sentinel"}`),
"npc_interactions": promptkit.Inline(`{"sentinel":"npc-interactions-sentinel"}`), "npc_occurrences": promptkit.Inline(`{"sentinel":"npc-occurrences-sentinel"}`),
}), }),
suffixGroups: [][]string{ suffixGroups: [][]string{
{evidenceSentinel}, {evidenceSentinel},
{npcSentinel}, {npcSentinel},
{"combat-turns-sentinel", "npc-interactions-sentinel"}, {"combat-turns-sentinel", "npc-occurrences-sentinel"},
}, },
}, },
{ {
name: "npc interactions", name: "npc occurrences",
promptID: interactionextract.PromptID, promptID: occurrenceextract.PromptID,
promptVersion: interactionextract.SchemaVersion, promptVersion: occurrenceextract.SchemaVersion,
inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{
"npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), "npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`),
}), }),

View File

@@ -17,7 +17,7 @@ import (
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
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/locations"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" scenedescriptionextract "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"
@@ -26,7 +26,7 @@ import (
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
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/locations"
interactionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcinteractions" occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions" scenedescriptionnormalize "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"
@@ -52,7 +52,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"dnd.combat_turns/prompt.yaml", "dnd.combat_turns/prompt.yaml",
"dnd.enemy_events/prompt.yaml", "dnd.enemy_events/prompt.yaml",
"dnd.item_events/prompt.yaml", "dnd.item_events/prompt.yaml",
"dnd.npc_interactions/prompt.yaml", "dnd.npc_occurrences/prompt.yaml",
"dnd.scene_descriptions/prompt.yaml", "dnd.scene_descriptions/prompt.yaml",
"dnd.npc_registry.normalize/prompt.yaml", "dnd.npc_registry.normalize/prompt.yaml",
"dnd.locations/prompt.yaml", "dnd.locations/prompt.yaml",
@@ -82,8 +82,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
t.Fatalf("entity reconciliation schema asset = %v, want registered shared schema", err) t.Fatalf("entity reconciliation schema asset = %v, want registered shared schema", err)
} }
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"}) assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, interactionextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key}) assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, interactionnormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule}) assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
@@ -92,7 +92,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(interactionnormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(occurrencenormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(scenedescriptionnormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(scenedescriptionnormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind})
assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{ assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
"extract/dnd/locations/shape", "normalize/dnd/locations/identity", "extract/dnd/locations/source_refs", "extract/dnd/locations/source_relatedness", "extract/dnd/locations/shape", "normalize/dnd/locations/identity", "extract/dnd/locations/source_refs", "extract/dnd/locations/source_relatedness",
@@ -118,11 +118,11 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"extract/dnd/item-events/source_refs", "extract/dnd/item-events/source_refs",
"extract/dnd/item-events/source_relatedness", "extract/dnd/item-events/source_relatedness",
"normalize/dnd/item-events/invariants", "normalize/dnd/item-events/invariants",
"extract/dnd/npc-interactions/shape", "extract/dnd/npc-occurrences/shape",
"extract/dnd/npc-interactions/registry", "extract/dnd/npc-occurrences/registry",
"extract/dnd/npc-interactions/source_refs", "extract/dnd/npc-occurrences/source_refs",
"extract/dnd/npc-interactions/source_relatedness", "extract/dnd/npc-occurrences/source_relatedness",
"normalize/dnd/npc-interactions/invariants", "normalize/dnd/npc-occurrences/invariants",
"extract/dnd/scene-descriptions/shape", "extract/dnd/scene-descriptions/shape",
"extract/dnd/scene-descriptions/source_refs", "extract/dnd/scene-descriptions/source_refs",
"extract/dnd/scene-descriptions/source_relatedness", "extract/dnd/scene-descriptions/source_relatedness",
@@ -245,28 +245,28 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemeventnormalize.Key); !reflect.DeepEqual(got, itemEventNormalizeChain) { 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) t.Fatalf("item event normalize validator chain = %#v, want %#v", got, itemEventNormalizeChain)
} }
interactionExtractChain := []pipeline.ModuleBinding{ npcOccurrenceExtractChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"), pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/npc-interactions/shape"), pipeline.Binding("extract/dnd/npc-occurrences/shape"),
pipeline.Binding("extract/dnd/npc-interactions/registry"), pipeline.Binding("extract/dnd/npc-occurrences/registry"),
pipeline.Binding("extract/dnd/npc-interactions/source_refs"), pipeline.Binding("extract/dnd/npc-occurrences/source_refs"),
pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npc-interactions/source_relatedness"), pipeline.Binding("extract/dnd/npc-occurrences/source_relatedness"),
} }
interactionNormalizeChain := []pipeline.ModuleBinding{ npcOccurrenceNormalizeChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"), pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/npc-interactions/shape"), pipeline.Binding("extract/dnd/npc-occurrences/shape"),
pipeline.Binding("extract/dnd/npc-interactions/registry"), pipeline.Binding("extract/dnd/npc-occurrences/registry"),
pipeline.Binding("normalize/dnd/npc-interactions/invariants"), pipeline.Binding("normalize/dnd/npc-occurrences/invariants"),
pipeline.Binding("extract/dnd/npc-interactions/source_refs"), pipeline.Binding("extract/dnd/npc-occurrences/source_refs"),
pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npc-interactions/source_relatedness"), pipeline.Binding("extract/dnd/npc-occurrences/source_relatedness"),
} }
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, interactionextract.Key); !reflect.DeepEqual(got, interactionExtractChain) { if got := registries.ValidatorChains.Validators(pipeline.StageExtract, occurrenceextract.Key); !reflect.DeepEqual(got, npcOccurrenceExtractChain) {
t.Fatalf("NPC interaction extract validator chain = %#v, want %#v", got, interactionExtractChain) t.Fatalf("NPC occurrence extract validator chain = %#v, want %#v", got, npcOccurrenceExtractChain)
} }
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, interactionnormalize.Key); !reflect.DeepEqual(got, interactionNormalizeChain) { if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, occurrencenormalize.Key); !reflect.DeepEqual(got, npcOccurrenceNormalizeChain) {
t.Fatalf("NPC interaction normalize validator chain = %#v, want %#v", got, interactionNormalizeChain) t.Fatalf("NPC occurrence normalize validator chain = %#v, want %#v", got, npcOccurrenceNormalizeChain)
} }
sceneExtractChain := []pipeline.ModuleBinding{ sceneExtractChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"), pipeline.Binding("generic/valid_json"),
@@ -292,8 +292,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, npcextract.Key); got != nil { if got := registries.ValidatorChains.Validators(pipeline.StageMerge, npcextract.Key); got != nil {
t.Fatalf("NPC merge validator chain = %#v, want absent", got) t.Fatalf("NPC merge validator chain = %#v, want absent", got)
} }
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, interactionextract.Key); got != nil { if got := registries.ValidatorChains.Validators(pipeline.StageMerge, occurrenceextract.Key); got != nil {
t.Fatalf("NPC interaction merge validator chain = %#v, want absent", got) t.Fatalf("NPC occurrence merge validator chain = %#v, want absent", got)
} }
assertAssetNamesContain(t, assets.SchemaFS, []string{ assertAssetNamesContain(t, assets.SchemaFS, []string{
"dnd_scenes_llm.v1.json", "dnd_scenes_llm.v1.json",
@@ -351,31 +351,31 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
t.Fatalf("item event extractor has a generated-reference dependency: %#v", slot) t.Fatalf("item event extractor has a generated-reference dependency: %#v", slot)
} }
} }
interactionExtractSpec, extractOK := registries.Extractors.Spec(interactionextract.Key) occurrenceExtractSpec, extractOK := registries.Extractors.Spec(occurrenceextract.Key)
interactionNormalizeSpec, normalizeOK := registries.Normalizers.Spec(interactionnormalize.Key) occurrenceNormalizeSpec, normalizeOK := registries.Normalizers.Spec(occurrencenormalize.Key)
if !extractOK || interactionExtractSpec.ArtifactKind != dnd.NPCOccurrenceListKind || !normalizeOK || interactionNormalizeSpec.ArtifactKind != dnd.NPCOccurrenceListKind || interactionNormalizeSpec.Stage != pipeline.StageNormalize { if !extractOK || occurrenceExtractSpec.ArtifactKind != dnd.NPCOccurrenceListKind || !normalizeOK || occurrenceNormalizeSpec.ArtifactKind != dnd.NPCOccurrenceListKind || occurrenceNormalizeSpec.Stage != pipeline.StageNormalize {
t.Fatalf("NPC interaction specs = %#v / %#v, present = %t / %t", interactionExtractSpec, interactionNormalizeSpec, extractOK, normalizeOK) t.Fatalf("NPC occurrence specs = %#v / %#v, present = %t / %t", occurrenceExtractSpec, occurrenceNormalizeSpec, extractOK, normalizeOK)
} }
locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.Key) locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.Key)
locationNormalizeSpec, locationNormalizeOK := registries.Normalizers.Spec(locationnormalize.Key) locationNormalizeSpec, locationNormalizeOK := registries.Normalizers.Spec(locationnormalize.Key)
if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationListKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationListKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked { if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationListKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationListKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked {
t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec) t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec)
} }
occurrenceExtractSpec, occurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key) locationOccurrenceExtractSpec, locationOccurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key)
occurrenceNormalizeSpec, occurrenceNormalizeOK := registries.Normalizers.Spec(locationoccurrencenormalize.Key) locationOccurrenceNormalizeSpec, locationOccurrenceNormalizeOK := registries.Normalizers.Spec(locationoccurrencenormalize.Key)
if !occurrenceExtractOK || occurrenceExtractSpec.ArtifactKind != dnd.LocationOccurrenceListKind || occurrenceExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !occurrenceNormalizeOK || occurrenceNormalizeSpec.ArtifactKind != dnd.LocationOccurrenceListKind || occurrenceNormalizeSpec.ExecutionClass != contracts.ExecutionClassDeterministic { if !locationOccurrenceExtractOK || locationOccurrenceExtractSpec.ArtifactKind != dnd.LocationOccurrenceListKind || locationOccurrenceExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationOccurrenceNormalizeOK || locationOccurrenceNormalizeSpec.ArtifactKind != dnd.LocationOccurrenceListKind || locationOccurrenceNormalizeSpec.ExecutionClass != contracts.ExecutionClassDeterministic {
t.Fatalf("location occurrence specs = %#v / %#v", occurrenceExtractSpec, occurrenceNormalizeSpec) t.Fatalf("location occurrence specs = %#v / %#v", locationOccurrenceExtractSpec, locationOccurrenceNormalizeSpec)
} }
locationRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "locations") locationRegistrySlot := referenceSlot(locationOccurrenceExtractSpec.ReferenceSlots, "locations")
occurrenceNormalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "locations") occurrenceNormalizeRegistrySlot := referenceSlot(locationOccurrenceNormalizeSpec.ReferenceSlots, "locations")
if len(occurrenceExtractSpec.ReferenceSlots) != 5 || len(occurrenceNormalizeSpec.ReferenceSlots) != 1 { if len(locationOccurrenceExtractSpec.ReferenceSlots) != 5 || len(locationOccurrenceNormalizeSpec.ReferenceSlots) != 1 {
t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots) t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", locationOccurrenceExtractSpec.ReferenceSlots, locationOccurrenceNormalizeSpec.ReferenceSlots)
} }
if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) { if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) {
t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots) t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
} }
for _, name := range []string{"party", "roster", "players", "glossary"} { for _, name := range []string{"party", "roster", "players", "glossary"} {
slot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, name) slot := referenceSlot(locationOccurrenceExtractSpec.ReferenceSlots, name)
if slot.Name != name || slot.Required || len(slot.AcceptedArtifactKinds) != 0 { if slot.Name != name || slot.Required || len(slot.AcceptedArtifactKinds) != 0 {
t.Fatalf("location occurrence extractor campaign slot %q = %#v, want optional text context", name, slot) t.Fatalf("location occurrence extractor campaign slot %q = %#v, want optional text context", name, slot)
} }
@@ -388,10 +388,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if len(sceneExtractSpec.ReferenceSlots) != 3 || len(sceneNormalizeSpec.ReferenceSlots) != 0 { if len(sceneExtractSpec.ReferenceSlots) != 3 || len(sceneNormalizeSpec.ReferenceSlots) != 0 {
t.Fatalf("scene description reference slots = %#v / %#v, want extractor campaign slots only", sceneExtractSpec.ReferenceSlots, sceneNormalizeSpec.ReferenceSlots) t.Fatalf("scene description reference slots = %#v / %#v, want extractor campaign slots only", sceneExtractSpec.ReferenceSlots, sceneNormalizeSpec.ReferenceSlots)
} }
extractRegistrySlot := referenceSlot(interactionExtractSpec.ReferenceSlots, "npc_registry") extractRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "npc_registry")
normalizeRegistrySlot := referenceSlot(interactionNormalizeSpec.ReferenceSlots, "npc_registry") normalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "npc_registry")
if !extractRegistrySlot.Required || !reflect.DeepEqual(extractRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCRegistryKind}) || !sameReferenceSlotContract(extractRegistrySlot, normalizeRegistrySlot) { if !extractRegistrySlot.Required || !reflect.DeepEqual(extractRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCRegistryKind}) || !sameReferenceSlotContract(extractRegistrySlot, normalizeRegistrySlot) {
t.Fatalf("NPC interaction registry slots disagree: %#v / %#v", interactionExtractSpec.ReferenceSlots, interactionNormalizeSpec.ReferenceSlots) t.Fatalf("NPC occurrence registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
} }
} }
@@ -418,8 +418,8 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes
{name: "item events", project: func() []source.SourceRef { {name: "item events", project: func() []source.SourceRef {
return itemEventEvidence(dnd.ItemEventList{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{first, second}}}}) return itemEventEvidence(dnd.ItemEventList{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}}, }, want: []source.SourceRef{first, second}},
{name: "npc interactions", project: func() []source.SourceRef { {name: "npc occurrences", project: func() []source.SourceRef {
return npcInteractionEvidence(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{first, second}}}}) return npcOccurrenceEvidence(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}}, }, want: []source.SourceRef{first, second}},
{name: "scene descriptions", project: func() []source.SourceRef { {name: "scene descriptions", project: func() []source.SourceRef {
return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}}) return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}})
@@ -559,7 +559,7 @@ func TestAppendSpellListsPreservesOrderPresenceAndOwnership(t *testing.T) {
} }
} }
func TestAppendNPCInteractionListsPreservesOrderPresenceAndOwnership(t *testing.T) { func TestAppendNPCOccurrenceListsPreservesOrderPresenceAndOwnership(t *testing.T) {
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}} refs := []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}
input := []dnd.NPCOccurrenceList{ input := []dnd.NPCOccurrenceList{
{}, {},
@@ -567,21 +567,21 @@ func TestAppendNPCInteractionListsPreservesOrderPresenceAndOwnership(t *testing.
{Occurrences: []dnd.NPCOccurrence{{Name: "Aria", Kind: dnd.NPCOccurrenceKindDialogue, SourceRefs: refs}}}, {Occurrences: []dnd.NPCOccurrence{{Name: "Aria", Kind: dnd.NPCOccurrenceKindDialogue, SourceRefs: refs}}},
{Occurrences: []dnd.NPCOccurrence{{Name: "Borin", Kind: dnd.NPCOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 20, EndUnitID: 20}}}}}, {Occurrences: []dnd.NPCOccurrence{{Name: "Borin", Kind: dnd.NPCOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 20, EndUnitID: 20}}}}},
} }
got, err := appendNPCInteractionLists(input) got, err := appendNPCOccurrenceLists(input)
if err != nil { if err != nil {
t.Fatalf("appendNPCInteractionLists() error = %v", err) t.Fatalf("appendNPCOccurrenceLists() error = %v", err)
} }
if got.Occurrences == nil || !reflect.DeepEqual([]string{got.Occurrences[0].Name, got.Occurrences[1].Name}, []string{"Aria", "Borin"}) { if got.Occurrences == nil || !reflect.DeepEqual([]string{got.Occurrences[0].Name, got.Occurrences[1].Name}, []string{"Aria", "Borin"}) {
t.Fatalf("combined interactions = %#v", got) t.Fatalf("combined occurrences = %#v", got)
} }
got.Occurrences[0].SourceRefs[0].StartUnitID = 999 got.Occurrences[0].SourceRefs[0].StartUnitID = 999
if input[2].Occurrences[0].SourceRefs[0].StartUnitID == 999 { if input[2].Occurrences[0].SourceRefs[0].StartUnitID == 999 {
t.Fatal("merged interactions share source reference storage") t.Fatal("merged occurrences share source reference storage")
} }
for _, values := range [][]dnd.NPCOccurrenceList{nil, []dnd.NPCOccurrenceList{{}, {}}} { for _, values := range [][]dnd.NPCOccurrenceList{nil, []dnd.NPCOccurrenceList{{}, {}}} {
result, err := appendNPCInteractionLists(values) result, err := appendNPCOccurrenceLists(values)
if err != nil || result.Occurrences != nil { if err != nil || result.Occurrences != nil {
t.Fatalf("nil-only merge = %#v, %v; want nil interactions", result, err) t.Fatalf("nil-only merge = %#v, %v; want nil occurrences", result, err)
} }
} }
} }
@@ -711,9 +711,9 @@ func TestAppendListsPreserveNestedSourceReferencePresence(t *testing.T) {
t.Fatalf("appendEnemyEventLists() = %#v, %v; want present-empty source refs", enemyEvents, err) t.Fatalf("appendEnemyEventLists() = %#v, %v; want present-empty source refs", enemyEvents, err)
} }
interactions, err := appendNPCInteractionLists([]dnd.NPCOccurrenceList{{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{}}}}}) occurrences, err := appendNPCOccurrenceLists([]dnd.NPCOccurrenceList{{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{}}}}})
if err != nil || interactions.Occurrences[0].SourceRefs == nil { if err != nil || occurrences.Occurrences[0].SourceRefs == nil {
t.Fatalf("appendNPCInteractionLists() = %#v, %v; want present-empty source refs", interactions, err) t.Fatalf("appendNPCOccurrenceLists() = %#v, %v; want present-empty source refs", occurrences, err)
} }
events, err := appendItemEventLists([]dnd.ItemEventList{{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{}}}}}) events, err := appendItemEventLists([]dnd.ItemEventList{{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{}}}}})

View File

@@ -16,20 +16,20 @@ import (
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs" itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs"
itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness" itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness"
occurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants" locationoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants"
occurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry" locationoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry"
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape" locationoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape"
occurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs" locationoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs"
occurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_relatedness" locationoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_relatedness"
locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/identity" locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/identity"
locationshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/shape" locationshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/shape"
locationrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_refs" locationrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_refs"
locationrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_relatedness" locationrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locations/source_relatedness"
interactioninvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/invariants" npcoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/invariants"
interactionregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/registry" npcoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/registry"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" npcoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape"
interactionrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_refs" npcoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/source_refs"
interactionrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_relatedness" npcoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/source_relatedness"
npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/identity" npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/identity"
npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/shape" npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/shape"
npcsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/source_refs" npcsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/source_refs"
@@ -69,11 +69,11 @@ func registerValidators(registries pipeline.Registries) error {
{name: "item event source references validator", register: func() error { return itemeventrefs.Register(registries.Validators) }}, {name: "item event source references validator", register: func() error { return itemeventrefs.Register(registries.Validators) }},
{name: "item event source relatedness validator", register: func() error { return itemeventrelatedness.Register(registries.Validators) }}, {name: "item event source relatedness validator", register: func() error { return itemeventrelatedness.Register(registries.Validators) }},
{name: "item event normalized invariants validator", register: func() error { return itemeventinvariants.Register(registries.Validators) }}, {name: "item event normalized invariants validator", register: func() error { return itemeventinvariants.Register(registries.Validators) }},
{name: "npc interaction shape validator", register: func() error { return interactionshape.Register(registries.Validators) }}, {name: "npc occurrence shape validator", register: func() error { return npcoccurrenceshape.Register(registries.Validators) }},
{name: "npc interaction registry validator", register: func() error { return interactionregistry.Register(registries.Validators) }}, {name: "npc occurrence registry validator", register: func() error { return npcoccurrenceregistry.Register(registries.Validators) }},
{name: "npc interaction source references validator", register: func() error { return interactionrefs.Register(registries.Validators) }}, {name: "npc occurrence source references validator", register: func() error { return npcoccurrencerefs.Register(registries.Validators) }},
{name: "npc interaction source relatedness validator", register: func() error { return interactionrelatedness.Register(registries.Validators) }}, {name: "npc occurrence source relatedness validator", register: func() error { return npcoccurrencerelatedness.Register(registries.Validators) }},
{name: "npc interaction normalized invariants validator", register: func() error { return interactioninvariants.Register(registries.Validators) }}, {name: "npc occurrence normalized invariants validator", register: func() error { return npcoccurrenceinvariants.Register(registries.Validators) }},
{name: "scene description shape validator", register: func() error { return sceneshape.Register(registries.Validators) }}, {name: "scene description shape validator", register: func() error { return sceneshape.Register(registries.Validators) }},
{name: "scene description source references validator", register: func() error { return scenerefs.Register(registries.Validators) }}, {name: "scene description source references validator", register: func() error { return scenerefs.Register(registries.Validators) }},
{name: "scene description source relatedness validator", register: func() error { return scenerelatedness.Register(registries.Validators) }}, {name: "scene description source relatedness validator", register: func() error { return scenerelatedness.Register(registries.Validators) }},
@@ -82,11 +82,11 @@ func registerValidators(registries pipeline.Registries) error {
{name: "location identity validator", register: func() error { return locationidentity.Register(registries.Validators) }}, {name: "location identity validator", register: func() error { return locationidentity.Register(registries.Validators) }},
{name: "location source references validator", register: func() error { return locationrefs.Register(registries.Validators) }}, {name: "location source references validator", register: func() error { return locationrefs.Register(registries.Validators) }},
{name: "location source relatedness validator", register: func() error { return locationrelatedness.Register(registries.Validators) }}, {name: "location source relatedness validator", register: func() error { return locationrelatedness.Register(registries.Validators) }},
{name: "location occurrence shape validator", register: func() error { return occurrenceshape.Register(registries.Validators) }}, {name: "location occurrence shape validator", register: func() error { return locationoccurrenceshape.Register(registries.Validators) }},
{name: "location occurrence registry validator", register: func() error { return occurrenceregistry.Register(registries.Validators) }}, {name: "location occurrence registry validator", register: func() error { return locationoccurrenceregistry.Register(registries.Validators) }},
{name: "location occurrence normalized invariants validator", register: func() error { return occurrenceinvariants.Register(registries.Validators) }}, {name: "location occurrence normalized invariants validator", register: func() error { return locationoccurrenceinvariants.Register(registries.Validators) }},
{name: "location occurrence source references validator", register: func() error { return occurrencerefs.Register(registries.Validators) }}, {name: "location occurrence source references validator", register: func() error { return locationoccurrencerefs.Register(registries.Validators) }},
{name: "location occurrence source relatedness validator", register: func() error { return occurrencerelatedness.Register(registries.Validators) }}, {name: "location occurrence source relatedness validator", register: func() error { return locationoccurrencerelatedness.Register(registries.Validators) }},
{name: "spell-list always accept validator", register: func() error { {name: "spell-list always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.SpellList](registries.Validators, dnd.SpellListKind) return alwaysaccept.RegisterTyped[dnd.SpellList](registries.Validators, dnd.SpellListKind)
}}, }},
@@ -117,10 +117,10 @@ func registerValidators(registries pipeline.Registries) error {
{name: "item-event-list always reject validator", register: func() error { {name: "item-event-list always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.ItemEventList](registries.Validators, dnd.ItemEventListKind) return alwaysreject.RegisterTyped[dnd.ItemEventList](registries.Validators, dnd.ItemEventListKind)
}}, }},
{name: "npc-interaction-list always accept validator", register: func() error { {name: "npc-occurrence-list always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.NPCOccurrenceList](registries.Validators, dnd.NPCOccurrenceListKind) return alwaysaccept.RegisterTyped[dnd.NPCOccurrenceList](registries.Validators, dnd.NPCOccurrenceListKind)
}}, }},
{name: "npc-interaction-list always reject validator", register: func() error { {name: "npc-occurrence-list always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.NPCOccurrenceList](registries.Validators, dnd.NPCOccurrenceListKind) return alwaysreject.RegisterTyped[dnd.NPCOccurrenceList](registries.Validators, dnd.NPCOccurrenceListKind)
}}, }},
{name: "scene-description-list always accept validator", register: func() error { {name: "scene-description-list always accept validator", register: func() error {

View File

@@ -1,4 +1,4 @@
// Package invariants validates normalized D&D NPC interaction artifacts. // Package invariants validates normalized D&D NPC occurrence artifacts.
package invariants package invariants
import ( import (
@@ -10,17 +10,17 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"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"
interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcinteractions" interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcoccurrences"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape"
) )
const ( const (
Key = "normalize/dnd/npc-interactions/invariants" Key = "normalize/dnd/npc-occurrences/invariants"
ReasonCode = "invalid_npc_interaction_normalization" ReasonCode = "invalid_npc_interaction_normalization"
policy = "dnd.npc_interactions.validator.normalized.v1" policy = "dnd.npc_occurrences.validator.normalized.v1"
) )
type Options struct{} type Options struct{}
@@ -35,7 +35,7 @@ var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) { func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) {
if len(references) > 1 { if len(references) > 1 {
return nil, fmt.Errorf("NPC interaction invariants validator accepts at most one reference set") return nil, fmt.Errorf("NPC occurrence invariants validator accepts at most one reference set")
} }
var referenceSet contracts.ReferenceSet var referenceSet contracts.ReferenceSet
if len(references) == 1 { if len(references) == 1 {
@@ -77,7 +77,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
} }
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) { func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) {
if interactionshape.Validate(req.Value) != nil { if occurrenceshape.Validate(req.Value) != nil {
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
index := source.NewDocumentIndex(req.Source) index := source.NewDocumentIndex(req.Source)
@@ -85,14 +85,14 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
if v == nil || v.npcResolver == nil { if v == nil || v.npcResolver == nil {
return contracts.ValidationResult{}, fmt.Errorf("NPC interaction invariants validator must not be nil") return contracts.ValidationResult{}, fmt.Errorf("NPC occurrence invariants validator must not be nil")
} }
npcRegistry, err := v.npcResolver.Resolve(req.References) npcRegistry, err := v.npcResolver.Resolve(req.References)
if err != nil { if err != nil {
return contracts.ValidationResult{}, fmt.Errorf("resolve NPC registry: %w", err) return contracts.ValidationResult{}, fmt.Errorf("resolve NPC registry: %w", err)
} }
if !npcRegistry.Bound() { if !npcRegistry.Bound() {
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: "invalid NPC interaction normalization: NPC registry reference is required"}, nil return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: "invalid NPC occurrence normalization: NPC registry reference is required"}, nil
} }
issues := issuesFor(shared.NewSourceRefOrderFromIndex(index), req.Value, npcRegistry) issues := issuesFor(shared.NewSourceRefOrderFromIndex(index), req.Value, npcRegistry)
if len(issues) == 0 { if len(issues) == 0 {
@@ -101,7 +101,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
return contracts.ValidationResult{ return contracts.ValidationResult{
Approved: false, Approved: false,
ReasonCode: ReasonCode, ReasonCode: ReasonCode,
Message: diagnostics.Aggregate("invalid NPC interaction normalization", issues), Message: diagnostics.Aggregate("invalid NPC occurrence normalization", issues),
}, nil }, nil
} }

View File

@@ -16,7 +16,7 @@ import (
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
) )
func TestValidatorApprovesCanonicalNormalizedInteractions(t *testing.T) { func TestValidatorApprovesCanonicalNormalizedOccurrences(t *testing.T) {
references := registryReferences(t, "Aria", "Borin") references := registryReferences(t, "Aria", "Borin")
result, err := newValidator(t, references).Validate(context.Background(), request(references, normalizedList())) result, err := newValidator(t, references).Validate(context.Background(), request(references, normalizedList()))
if err != nil || !result.Approved { if err != nil || !result.Approved {
@@ -143,9 +143,9 @@ func normalizedList() dnd.NPCOccurrenceList {
func cloneList(value dnd.NPCOccurrenceList) dnd.NPCOccurrenceList { func cloneList(value dnd.NPCOccurrenceList) dnd.NPCOccurrenceList {
copyValue := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, len(value.Occurrences))} copyValue := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, len(value.Occurrences))}
for index, interaction := range value.Occurrences { for index, occurrence := range value.Occurrences {
copyValue.Occurrences[index] = interaction copyValue.Occurrences[index] = occurrence
copyValue.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), interaction.SourceRefs...) copyValue.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), occurrence.SourceRefs...)
} }
return copyValue return copyValue
} }

View File

@@ -1,4 +1,4 @@
// Package registry validates D&D NPC interaction names against NPC grounding. // Package registry validates D&D NPC occurrence names against NPC grounding.
package registry package registry
import ( import (
@@ -10,13 +10,13 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape"
) )
const ( const (
Key = "extract/dnd/npc-interactions/registry" Key = "extract/dnd/npc-occurrences/registry"
ReasonCode = "invalid_npc_interaction_registry" ReasonCode = "invalid_npc_interaction_registry"
policy = "dnd.npc_interactions.validator.registry.v1" policy = "dnd.npc_occurrences.validator.registry.v1"
) )
type Options struct{} type Options struct{}
@@ -31,7 +31,7 @@ var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) { func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) {
if len(references) > 1 { if len(references) > 1 {
return nil, fmt.Errorf("NPC interaction registry validator accepts at most one reference set") return nil, fmt.Errorf("NPC occurrence registry validator accepts at most one reference set")
} }
var referenceSet contracts.ReferenceSet var referenceSet contracts.ReferenceSet
if len(references) == 1 { if len(references) == 1 {
@@ -73,11 +73,11 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
} }
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) { func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) {
if interactionshape.Validate(req.Value) != nil { if occurrenceshape.Validate(req.Value) != nil {
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
if v == nil || v.npcResolver == nil { if v == nil || v.npcResolver == nil {
return contracts.ValidationResult{}, fmt.Errorf("NPC interaction registry validator must not be nil") return contracts.ValidationResult{}, fmt.Errorf("NPC occurrence registry validator must not be nil")
} }
npcRegistry, err := v.npcResolver.Resolve(req.References) npcRegistry, err := v.npcResolver.Resolve(req.References)
if err != nil { if err != nil {
@@ -107,7 +107,7 @@ func rejection(issues []string) contracts.ValidationResult {
return contracts.ValidationResult{ return contracts.ValidationResult{
Approved: false, Approved: false,
ReasonCode: ReasonCode, ReasonCode: ReasonCode,
Message: diagnostics.Aggregate("invalid NPC interaction registry", issues), Message: diagnostics.Aggregate("invalid NPC occurrence registry", issues),
} }
} }

View File

@@ -1,4 +1,4 @@
// Package shape validates required D&D NPC interaction candidate fields. // Package shape validates required D&D NPC occurrence candidate fields.
package shape package shape
import ( import (
@@ -13,9 +13,9 @@ import (
) )
const ( const (
Key = "extract/dnd/npc-interactions/shape" Key = "extract/dnd/npc-occurrences/shape"
ReasonCode = "invalid_npc_interaction_shape" ReasonCode = "invalid_npc_interaction_shape"
policy = "dnd.npc_interactions.validator.shape.v1" policy = "dnd.npc_occurrences.validator.shape.v1"
) )
type Options struct{} type Options struct{}
@@ -45,7 +45,7 @@ func Validate(value dnd.NPCOccurrenceList) error {
if len(issues) == 0 { if len(issues) == 0 {
return nil return nil
} }
return fmt.Errorf("%s", diagnostics.Aggregate("invalid NPC interaction shape", issues)) return fmt.Errorf("%s", diagnostics.Aggregate("invalid NPC occurrence shape", issues))
} }
func issuesFor(value dnd.NPCOccurrenceList) []string { func issuesFor(value dnd.NPCOccurrenceList) []string {

View File

@@ -1,4 +1,4 @@
// Package sourcerefs validates D&D NPC interaction transcript evidence. // Package sourcerefs validates D&D NPC occurrence transcript evidence.
package sourcerefs package sourcerefs
import ( import (
@@ -10,13 +10,13 @@ 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/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape"
) )
const ( const (
Key = "extract/dnd/npc-interactions/source_refs" Key = "extract/dnd/npc-occurrences/source_refs"
ReasonCode = "invalid_npc_interaction_source_refs" ReasonCode = "invalid_npc_interaction_source_refs"
policy = "dnd.npc_interactions.validator.source_refs.v2" policy = "dnd.npc_occurrences.validator.source_refs.v2"
) )
type Options struct{} type Options struct{}
@@ -36,9 +36,9 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) { func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) {
if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil { if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil {
return contracts.ValidationResult{}, fmt.Errorf("NPC interaction source-reference validator requires the current extraction chunk") return contracts.ValidationResult{}, fmt.Errorf("NPC occurrence source-reference validator requires the current extraction chunk")
} }
if interactionshape.Validate(req.Value) != nil { if occurrenceshape.Validate(req.Value) != nil {
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
index := source.NewDocumentIndex(req.Source) index := source.NewDocumentIndex(req.Source)
@@ -63,7 +63,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
return contracts.ValidationResult{ return contracts.ValidationResult{
Approved: false, Approved: false,
ReasonCode: ReasonCode, ReasonCode: ReasonCode,
Message: diagnostics.Aggregate("invalid NPC interaction source references", issues), Message: diagnostics.Aggregate("invalid NPC occurrence source references", issues),
}, nil }, nil
} }

View File

@@ -1,4 +1,4 @@
// Package sourcerelatedness warns about NPC interaction evidence unrelated to its NPC. // Package sourcerelatedness warns about NPC occurrence evidence unrelated to its NPC.
package sourcerelatedness package sourcerelatedness
import ( import (
@@ -10,14 +10,14 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape"
) )
const ( const (
Key = "extract/dnd/npc-interactions/source_relatedness" Key = "extract/dnd/npc-occurrences/source_relatedness"
WarningReasonCode = "npc_interaction_not_near_source" WarningReasonCode = "npc_interaction_not_near_source"
OmittedReasonCode = "npc_interaction_relatedness_warnings_omitted" OmittedReasonCode = "npc_interaction_relatedness_warnings_omitted"
policy = "dnd.npc_interactions.validator.source_relatedness.v2" policy = "dnd.npc_occurrences.validator.source_relatedness.v2"
) )
type Options struct{} type Options struct{}
@@ -36,7 +36,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
} }
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) { func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) {
if interactionshape.Validate(req.Value) != nil { if occurrenceshape.Validate(req.Value) != nil {
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
resolver, err := shared.NewCitationResolver(req.Source) resolver, err := shared.NewCitationResolver(req.Source)
@@ -59,12 +59,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
warnings = append(warnings, contracts.Warning{ warnings = append(warnings, contracts.Warning{
Scope: fmt.Sprintf("occurrences[%d]", index), Scope: fmt.Sprintf("occurrences[%d]", index),
ReasonCode: WarningReasonCode, ReasonCode: WarningReasonCode,
Message: fmt.Sprintf("NPC interaction name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)), Message: fmt.Sprintf("NPC occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
}) })
} }
return contracts.ValidationResult{ return contracts.ValidationResult{
Approved: true, Approved: true,
Warnings: diagnostics.LimitWarnings(warnings, "npc_interactions", OmittedReasonCode), Warnings: diagnostics.LimitWarnings(warnings, "npc_occurrences", OmittedReasonCode),
}, nil }, nil
} }

View File

@@ -13,7 +13,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
) )
func TestValidatorUsesOnlyCurrentTranscriptAndWarnsOncePerInteraction(t *testing.T) { func TestValidatorUsesOnlyCurrentTranscriptAndWarnsOncePerOccurrence(t *testing.T) {
value := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{ value := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{
{NPCID: "npc:test", Name: "O'Rin Thorn", Kind: dnd.NPCOccurrenceKindDialogue, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, {NPCID: "npc:test", Name: "O'Rin Thorn", Kind: dnd.NPCOccurrenceKindDialogue, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
{NPCID: "npc:test", Name: "Missing\nNPC", Kind: dnd.NPCOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}, {SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}, {NPCID: "npc:test", Name: "Missing\nNPC", Kind: dnd.NPCOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}, {SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
@@ -53,9 +53,9 @@ func TestValidatorDefersMalformedShapeAndInvalidRanges(t *testing.T) {
func TestValidatorBoundsWarnings(t *testing.T) { func TestValidatorBoundsWarnings(t *testing.T) {
count := diagnostics.MaxWarnings + 5 count := diagnostics.MaxWarnings + 5
interactions := make([]dnd.NPCOccurrence, count) occurrences := make([]dnd.NPCOccurrence, count)
for index := range interactions { for index := range occurrences {
interactions[index] = dnd.NPCOccurrence{ occurrences[index] = dnd.NPCOccurrence{
NPCID: "npc:test", NPCID: "npc:test",
Name: "Missing NPC", Name: "Missing NPC",
Kind: dnd.NPCOccurrenceKindMentioned, Kind: dnd.NPCOccurrenceKindMentioned,
@@ -64,7 +64,7 @@ func TestValidatorBoundsWarnings(t *testing.T) {
} }
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{ result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "The party waits."}}}, Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "The party waits."}}},
Value: dnd.NPCOccurrenceList{Occurrences: interactions}, Value: dnd.NPCOccurrenceList{Occurrences: occurrences},
}) })
if err != nil || !result.Approved { if err != nil || !result.Approved {
t.Fatalf("Validate() = %#v, %v", result, err) t.Fatalf("Validate() = %#v, %v", result, err)

View File

@@ -14,73 +14,73 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry" npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
) )
func TestNPCInteractionPipelineUsesAcceptedRegistryAndCurrentEvidence(t *testing.T) { func TestNPCOccurrencePipelineUsesAcceptedRegistryAndCurrentEvidence(t *testing.T) {
registries := productionNPCRegistries(t) registries := productionNPCRegistries(t)
resolved := resolveNPCInteractionPipeline(t, registries) resolved := resolveNPCOccurrencePipeline(t, registries)
client := &npcInteractionLLMClient{} client := &npcOccurrenceLLMClient{}
output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)}) output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)})
if err != nil { if err != nil {
t.Fatalf("Run() error = %v", err) t.Fatalf("Run() error = %v", err)
} }
if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 2 { if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 2 {
t.Fatalf("run outputs = %#v rejected = %#v, want NPC and interaction artifacts", output.NormalizeOutputs, output.Rejected) t.Fatalf("run outputs = %#v rejected = %#v, want NPC and occurrence artifacts", output.NormalizeOutputs, output.Rejected)
} }
request := client.requestFor(t, interactionextract.PromptID) request := client.requestFor(t, occurrenceextract.PromptID)
wantRegistry := `{"npcs":[{"id":"` + identity.DeriveID("Mira Thorn") + `","name":"Mira Thorn"},{"id":"` + identity.DeriveID("Hooded Guard") + `","name":"Hooded Guard"}]}` wantRegistry := `{"npcs":[{"id":"` + identity.DeriveID("Mira Thorn") + `","name":"Mira Thorn"},{"id":"` + identity.DeriveID("Hooded Guard") + `","name":"Hooded Guard"}]}`
if got := string(request.Inputs["npc_registry"].Content); got != wantRegistry { if got := string(request.Inputs["npc_registry"].Content); got != wantRegistry {
t.Fatalf("interaction registry input = %s, want names-only projection %s", got, wantRegistry) t.Fatalf("occurrence registry input = %s, want names-only projection %s", got, wantRegistry)
} }
if request.Inputs["npc_registry"].MediaType != npccodec.MediaType { if request.Inputs["npc_registry"].MediaType != npccodec.MediaType {
t.Fatalf("interaction registry media type = %q, want %q", request.Inputs["npc_registry"].MediaType, npccodec.MediaType) t.Fatalf("occurrence registry media type = %q, want %q", request.Inputs["npc_registry"].MediaType, npccodec.MediaType)
} }
serialized := normalizedLane(t, output, "interactions") serialized := normalizedLane(t, output, "occurrences")
if serialized.Artifact.Schema.ID != interactioncodec.SchemaID || serialized.Artifact.Schema.Version != interactioncodec.SchemaVersion { if serialized.Artifact.Schema.ID != occurrencecodec.SchemaID || serialized.Artifact.Schema.Version != occurrencecodec.SchemaVersion {
t.Fatalf("interaction artifact schema = %#v", serialized.Artifact.Schema) t.Fatalf("occurrence artifact schema = %#v", serialized.Artifact.Schema)
} }
interactions, err := interactioncodec.New().Decode(serialized.Artifact.Content) occurrences, err := occurrencecodec.New().Decode(serialized.Artifact.Content)
if err != nil { if err != nil {
t.Fatalf("Decode(interaction output) error = %v", err) t.Fatalf("Decode(occurrence output) error = %v", err)
} }
if len(interactions.Occurrences) != 2 { if len(occurrences.Occurrences) != 2 {
t.Fatalf("interactions = %#v, want two occurrences", interactions) t.Fatalf("occurrences = %#v, want two occurrences", occurrences)
} }
first, second := interactions.Occurrences[0], interactions.Occurrences[1] first, second := occurrences.Occurrences[0], occurrences.Occurrences[1]
if first.Name != "Mira Thorn" || string(first.Kind) != "dialogue" || second.Name != "Hooded Guard" || string(second.Kind) != "noncombat_presence" { if first.Name != "Mira Thorn" || string(first.Kind) != "dialogue" || second.Name != "Hooded Guard" || string(second.Kind) != "noncombat_presence" {
t.Fatalf("interactions = %#v, want canonical names, kinds, and source chronology", interactions) t.Fatalf("occurrences = %#v, want canonical names, kinds, and source chronology", occurrences)
} }
assertInteractionEvidence(t, first.SourceRefs) assertOccurrenceEvidence(t, first.SourceRefs)
assertInteractionEvidence(t, second.SourceRefs) assertOccurrenceEvidence(t, second.SourceRefs)
if first.SourceRefs[0].StartUnitID >= second.SourceRefs[0].StartUnitID { if first.SourceRefs[0].StartUnitID >= second.SourceRefs[0].StartUnitID {
t.Fatalf("interaction chronology = %#v, want source order", interactions.Occurrences) t.Fatalf("occurrence chronology = %#v, want source order", occurrences.Occurrences)
} }
var durable map[string]json.RawMessage var durable map[string]json.RawMessage
if err := json.Unmarshal(serialized.Artifact.Content, &durable); err != nil { if err := json.Unmarshal(serialized.Artifact.Content, &durable); err != nil {
t.Fatalf("unmarshal durable interaction payload: %v", err) t.Fatalf("unmarshal durable occurrence payload: %v", err)
} }
if len(durable) != 1 || durable["occurrences"] == nil { if len(durable) != 1 || durable["occurrences"] == nil {
t.Fatalf("durable interaction payload = %#v, want only occurrences", durable) t.Fatalf("durable occurrence payload = %#v, want only occurrences", durable)
} }
} }
func TestSemanticNPCNormalizationCrossesOrderedRegistryHandoff(t *testing.T) { func TestSemanticNPCNormalizationCrossesOrderedRegistryHandoff(t *testing.T) {
registries := productionNPCRegistries(t) registries := productionNPCRegistries(t)
cfg := loadNPCInteractionPipelineConfig(t) cfg := loadNPCOccurrencePipelineConfig(t)
profile := cfg.Pipelines["dnd-npc-interactions-fixture"] profile := cfg.Pipelines["dnd-npc-occurrences-fixture"]
profile.Chunk = pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}} profile.Chunk = pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}}
cfg.Pipelines["dnd-npc-interactions-fixture"] = profile cfg.Pipelines["dnd-npc-occurrences-fixture"] = profile
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-npc-interactions-fixture", Catalog: moduleCatalog(registries)}) effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-npc-occurrences-fixture", Catalog: moduleCatalog(registries)})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -88,7 +88,7 @@ func TestSemanticNPCNormalizationCrossesOrderedRegistryHandoff(t *testing.T) {
if err != nil || len(warnings) != 0 { if err != nil || len(warnings) != 0 {
t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings) t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings)
} }
client := &semanticNPCInteractionClient{} client := &semanticNPCOccurrenceClient{}
raw := []byte(`{"metadata":{"id":"semantic-session","title":"Semantic NPC session"},"segments":[{"id":1,"start":0,"end":1,"speaker":"DM","text":"Mira Thorn enters."},{"id":2,"start":1,"end":2,"speaker":"DM","text":"Mira Thorn, the Greencloak, waves."}]}`) raw := []byte(`{"metadata":{"id":"semantic-session","title":"Semantic NPC session"},"segments":[{"id":1,"start":0,"end":1,"speaker":"DM","text":"Mira Thorn enters."},{"id":2,"start":1,"end":2,"speaker":"DM","text":"Mira Thorn, the Greencloak, waves."}]}`)
output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: raw}) output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: raw})
if err != nil { if err != nil {
@@ -108,11 +108,11 @@ func TestSemanticNPCNormalizationCrossesOrderedRegistryHandoff(t *testing.T) {
if refs := npcsValue.NPCs[0].SourceRefs; !reflect.DeepEqual(refs, []source.SourceRef{{SourceID: "semantic-session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "semantic-session", StartUnitID: 2, EndUnitID: 2}}) { if refs := npcsValue.NPCs[0].SourceRefs; !reflect.DeepEqual(refs, []source.SourceRef{{SourceID: "semantic-session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "semantic-session", StartUnitID: 2, EndUnitID: 2}}) {
t.Fatalf("NPC evidence = %#v, want original extraction evidence union", refs) t.Fatalf("NPC evidence = %#v, want original extraction evidence union", refs)
} }
interactionOutput := normalizedLane(t, output, "interactions") occurrenceOutput := normalizedLane(t, output, "occurrences")
if interactionOutput.StepID != "extract-interactions" { if occurrenceOutput.StepID != "extract-occurrences" {
t.Fatalf("interaction output step = %q, want ordered downstream step", interactionOutput.StepID) t.Fatalf("occurrence output step = %q, want ordered downstream step", occurrenceOutput.StepID)
} }
registryRequest := client.requestFor(t, interactionextract.PromptID) registryRequest := client.requestFor(t, occurrenceextract.PromptID)
if got := string(registryRequest.Inputs["npc_registry"].Content); got != `{"npcs":[{"id":"`+identity.DeriveID("Mira Thorn")+`","name":"Mira Thorn"}]}` { if got := string(registryRequest.Inputs["npc_registry"].Content); got != `{"npcs":[{"id":"`+identity.DeriveID("Mira Thorn")+`","name":"Mira Thorn"}]}` {
t.Fatalf("downstream registry = %s, want one canonical NPC identity", got) t.Fatalf("downstream registry = %s, want one canonical NPC identity", got)
} }
@@ -127,10 +127,10 @@ func TestSemanticNPCNormalizationCrossesOrderedRegistryHandoff(t *testing.T) {
} }
} }
func TestNPCInteractionPipelineSkipsConsumerWhenNPCProducerIsRejected(t *testing.T) { func TestNPCOccurrencePipelineSkipsConsumerWhenNPCProducerIsRejected(t *testing.T) {
registries := productionNPCRegistries(t) registries := productionNPCRegistries(t)
resolved := resolveNPCInteractionPipeline(t, registries) resolved := resolveNPCOccurrencePipeline(t, registries)
client := &npcInteractionLLMClient{rejectNPCs: true} client := &npcOccurrenceLLMClient{rejectNPCs: true}
output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)}) output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)})
if err == nil { if err == nil {
@@ -139,15 +139,15 @@ func TestNPCInteractionPipelineSkipsConsumerWhenNPCProducerIsRejected(t *testing
if client.requestCount(npcregistry.PromptID) != 1 { if client.requestCount(npcregistry.PromptID) != 1 {
t.Fatalf("NPC requests = %d, want rejected producer", client.requestCount(npcregistry.PromptID)) t.Fatalf("NPC requests = %d, want rejected producer", client.requestCount(npcregistry.PromptID))
} }
if client.requestCount(interactionextract.PromptID) != 0 { if client.requestCount(occurrenceextract.PromptID) != 0 {
t.Fatalf("interaction requests = %d, want none after rejected producer", client.requestCount(interactionextract.PromptID)) t.Fatalf("occurrence requests = %d, want none after rejected producer", client.requestCount(occurrenceextract.PromptID))
} }
} }
func resolveNPCInteractionPipeline(t *testing.T, registries pipeline.Registries) pipeline.ResolvedPipeline { func resolveNPCOccurrencePipeline(t *testing.T, registries pipeline.Registries) pipeline.ResolvedPipeline {
t.Helper() t.Helper()
configValue := loadNPCInteractionPipelineConfig(t) configValue := loadNPCOccurrencePipelineConfig(t)
effective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-npc-interactions-fixture", Catalog: moduleCatalog(registries)}) effective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-npc-occurrences-fixture", Catalog: moduleCatalog(registries)})
if err != nil { if err != nil {
t.Fatalf("Resolve() error = %v", err) t.Fatalf("Resolve() error = %v", err)
} }
@@ -158,11 +158,11 @@ func resolveNPCInteractionPipeline(t *testing.T, registries pipeline.Registries)
return resolved return resolved
} }
func loadNPCInteractionPipelineConfig(t *testing.T) config.Config { func loadNPCOccurrencePipelineConfig(t *testing.T) config.Config {
t.Helper() t.Helper()
data, err := os.ReadFile("testdata/dnd_npc_interactions_pipeline.yml") data, err := os.ReadFile("testdata/dnd_npc_occurrences_pipeline.yml")
if err != nil { if err != nil {
t.Fatalf("ReadFile(dnd_npc_interactions_pipeline.yml) error = %v", err) t.Fatalf("ReadFile(dnd_npc_occurrences_pipeline.yml) error = %v", err)
} }
fileConfig, err := config.ParseFileConfigYAML(data) fileConfig, err := config.ParseFileConfigYAML(data)
if err != nil { if err != nil {
@@ -186,25 +186,25 @@ func normalizedLane(t *testing.T, output pipeline.RunOutput, laneID string) cont
return contracts.SerializedOutput{} return contracts.SerializedOutput{}
} }
func assertInteractionEvidence(t *testing.T, references []source.SourceRef) { func assertOccurrenceEvidence(t *testing.T, references []source.SourceRef) {
t.Helper() t.Helper()
if len(references) == 0 { if len(references) == 0 {
t.Fatal("interaction has no current-source evidence") t.Fatal("occurrence has no current-source evidence")
} }
for _, reference := range references { for _, reference := range references {
if reference.SourceID != "npc-session" { if reference.SourceID != "npc-session" {
t.Fatalf("interaction evidence = %#v, want current source only", reference) t.Fatalf("occurrence evidence = %#v, want current source only", reference)
} }
} }
} }
type npcInteractionLLMClient struct { type npcOccurrenceLLMClient struct {
mu sync.Mutex mu sync.Mutex
requests []contracts.StructuredCompletionRequest requests []contracts.StructuredCompletionRequest
rejectNPCs bool rejectNPCs bool
} }
func (client *npcInteractionLLMClient) CompleteStructured(ctx context.Context, request contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) { func (client *npcOccurrenceLLMClient) CompleteStructured(ctx context.Context, request contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return contracts.StructuredCompletionResponse{}, err return contracts.StructuredCompletionResponse{}, err
} }
@@ -227,25 +227,25 @@ func (client *npcInteractionLLMClient) CompleteStructured(ctx context.Context, r
} }
case npcnormalize.PromptID: case npcnormalize.PromptID:
payload = map[string]any{"duplicate_groups": []any{}} payload = map[string]any{"duplicate_groups": []any{}}
case interactionextract.PromptID: case occurrenceextract.PromptID:
payload = map[string]any{"occurrences": []any{ payload = map[string]any{"occurrences": []any{
map[string]any{"npc_id": identity.DeriveID("Hooded Guard"), "name": "Hooded Guard", "kind": "noncombat_presence", "source_refs": []any{map[string]int{"start_unit_id": 3, "end_unit_id": 3}}}, map[string]any{"npc_id": identity.DeriveID("Hooded Guard"), "name": "Hooded Guard", "kind": "noncombat_presence", "source_refs": []any{map[string]int{"start_unit_id": 3, "end_unit_id": 3}}},
map[string]any{"npc_id": identity.DeriveID("Mira Thorn"), "name": "Mira Thorn", "kind": "dialogue", "source_refs": []any{map[string]int{"start_unit_id": 2, "end_unit_id": 2}}}, map[string]any{"npc_id": identity.DeriveID("Mira Thorn"), "name": "Mira Thorn", "kind": "dialogue", "source_refs": []any{map[string]int{"start_unit_id": 2, "end_unit_id": 2}}},
}} }}
default: default:
return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected interaction prompt %q", request.PromptID) return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected occurrence prompt %q", request.PromptID)
} }
content, err := json.Marshal(payload) content, err := json.Marshal(payload)
if err != nil { if err != nil {
return contracts.StructuredCompletionResponse{}, err return contracts.StructuredCompletionResponse{}, err
} }
if err := json.Unmarshal(content, out); err != nil { if err := json.Unmarshal(content, out); err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("populate interaction response: %w", err) return contracts.StructuredCompletionResponse{}, fmt.Errorf("populate occurrence response: %w", err)
} }
return contracts.StructuredCompletionResponse{Content: content, Provider: "test", Model: "interaction-fake"}, nil return contracts.StructuredCompletionResponse{Content: content, Provider: "test", Model: "occurrence-fake"}, nil
} }
func (client *npcInteractionLLMClient) requestFor(t *testing.T, promptID string) contracts.StructuredCompletionRequest { func (client *npcOccurrenceLLMClient) requestFor(t *testing.T, promptID string) contracts.StructuredCompletionRequest {
t.Helper() t.Helper()
client.mu.Lock() client.mu.Lock()
defer client.mu.Unlock() defer client.mu.Unlock()
@@ -258,7 +258,7 @@ func (client *npcInteractionLLMClient) requestFor(t *testing.T, promptID string)
return contracts.StructuredCompletionRequest{} return contracts.StructuredCompletionRequest{}
} }
func (client *npcInteractionLLMClient) requestCount(promptID string) int { func (client *npcOccurrenceLLMClient) requestCount(promptID string) int {
client.mu.Lock() client.mu.Lock()
defer client.mu.Unlock() defer client.mu.Unlock()
count := 0 count := 0
@@ -270,14 +270,14 @@ func (client *npcInteractionLLMClient) requestCount(promptID string) int {
return count return count
} }
var _ contracts.StructuredLLMClient = (*npcInteractionLLMClient)(nil) var _ contracts.StructuredLLMClient = (*npcOccurrenceLLMClient)(nil)
type semanticNPCInteractionClient struct { type semanticNPCOccurrenceClient struct {
requests []contracts.StructuredCompletionRequest requests []contracts.StructuredCompletionRequest
npcCalls int npcCalls int
} }
func (client *semanticNPCInteractionClient) CompleteStructured(_ context.Context, request contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) { func (client *semanticNPCOccurrenceClient) CompleteStructured(_ context.Context, request contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
client.requests = append(client.requests, cloneStructuredCompletionRequest(request)) client.requests = append(client.requests, cloneStructuredCompletionRequest(request))
var payload any var payload any
switch request.PromptID { switch request.PromptID {
@@ -290,7 +290,7 @@ func (client *semanticNPCInteractionClient) CompleteStructured(_ context.Context
payload = map[string]any{"npcs": []any{map[string]any{"name": name, "source_refs": []any{map[string]int{"start_unit_id": client.npcCalls, "end_unit_id": client.npcCalls}}}}} payload = map[string]any{"npcs": []any{map[string]any{"name": name, "source_refs": []any{map[string]int{"start_unit_id": client.npcCalls, "end_unit_id": client.npcCalls}}}}}
case npcnormalize.PromptID: case npcnormalize.PromptID:
payload = map[string]any{"duplicate_groups": []any{map[string]any{"members": []string{"candidate-000001", "candidate-000002"}, "canonical": "candidate-000001"}}} payload = map[string]any{"duplicate_groups": []any{map[string]any{"members": []string{"candidate-000001", "candidate-000002"}, "canonical": "candidate-000001"}}}
case interactionextract.PromptID: case occurrenceextract.PromptID:
payload = map[string]any{"occurrences": []any{map[string]any{"npc_id": identity.DeriveID("Mira Thorn"), "name": "Mira Thorn", "kind": "dialogue", "source_refs": []any{map[string]int{"start_unit_id": 1, "end_unit_id": 1}}}}} payload = map[string]any{"occurrences": []any{map[string]any{"npc_id": identity.DeriveID("Mira Thorn"), "name": "Mira Thorn", "kind": "dialogue", "source_refs": []any{map[string]int{"start_unit_id": 1, "end_unit_id": 1}}}}}
default: default:
return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected semantic pipeline prompt %q", request.PromptID) return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected semantic pipeline prompt %q", request.PromptID)
@@ -305,7 +305,7 @@ func (client *semanticNPCInteractionClient) CompleteStructured(_ context.Context
return contracts.StructuredCompletionResponse{Content: content}, nil return contracts.StructuredCompletionResponse{Content: content}, nil
} }
func (client *semanticNPCInteractionClient) requestCount(promptID string) int { func (client *semanticNPCOccurrenceClient) requestCount(promptID string) int {
count := 0 count := 0
for _, request := range client.requests { for _, request := range client.requests {
if request.PromptID == promptID { if request.PromptID == promptID {
@@ -315,7 +315,7 @@ func (client *semanticNPCInteractionClient) requestCount(promptID string) int {
return count return count
} }
func (client *semanticNPCInteractionClient) requestFor(t *testing.T, promptID string) contracts.StructuredCompletionRequest { func (client *semanticNPCOccurrenceClient) requestFor(t *testing.T, promptID string) contracts.StructuredCompletionRequest {
t.Helper() t.Helper()
for _, request := range client.requests { for _, request := range client.requests {
if request.PromptID == promptID { if request.PromptID == promptID {
@@ -326,4 +326,4 @@ func (client *semanticNPCInteractionClient) requestFor(t *testing.T, promptID st
return contracts.StructuredCompletionRequest{} return contracts.StructuredCompletionRequest{}
} }
var _ contracts.StructuredLLMClient = (*semanticNPCInteractionClient)(nil) var _ contracts.StructuredLLMClient = (*semanticNPCOccurrenceClient)(nil)

View File

@@ -8,7 +8,7 @@ cache:
debug: debug:
directory: ./notarius-debug directory: ./notarius-debug
pipelines: pipelines:
dnd-npc-interactions-fixture: dnd-npc-occurrences-fixture:
input: seriatim input: seriatim
steps: steps:
- id: identify-npcs - id: identify-npcs
@@ -16,13 +16,13 @@ pipelines:
npc_registry: npc_registry:
extract: dnd/npc-registry extract: dnd/npc-registry
normalize: dnd/npc-registry normalize: dnd/npc-registry
- id: extract-interactions - id: extract-occurrences
references: references:
npc_registry: npc_registry:
artifact: artifact:
step: identify-npcs step: identify-npcs
lane: npc_registry lane: npc_registry
artifacts: artifacts:
interactions: occurrences:
extract: dnd/npc-interactions extract: dnd/npc-occurrences
normalize: dnd/npc-interactions normalize: dnd/npc-occurrences