diff --git a/README.md b/README.md index 51906af..f967d61 100644 --- a/README.md +++ b/README.md @@ -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 transcript JSON and can produce location registries and occurrences, scene descriptions, item and currency events, NPC identities, combat turns, NPC -interactions, enemy events, and spell casts. +occurrences, enemy events, and spell casts. ## Quickstart diff --git a/assets/dnd/enemy-events/prompts/combat-grounding.md b/assets/dnd/enemy-events/prompts/combat-grounding.md index a9ef48f..32f27d3 100644 --- a/assets/dnd/enemy-events/prompts/combat-grounding.md +++ b/assets/dnd/enemy-events/prompts/combat-grounding.md @@ -9,4 +9,4 @@ Combat-turn grounding: Named combat-opponent grounding: -{{ input "npc_interactions" }} +{{ input "npc_occurrences" }} diff --git a/assets/dnd/enemy-events/prompts/prompt.yaml b/assets/dnd/enemy-events/prompts/prompt.yaml index 398059c..7f73d24 100644 --- a/assets/dnd/enemy-events/prompts/prompt.yaml +++ b/assets/dnd/enemy-events/prompts/prompt.yaml @@ -20,7 +20,7 @@ inputs: - name: combat_turns required: true content_type: application/json - - name: npc_interactions + - name: npc_occurrences required: true content_type: application/json messages: diff --git a/assets/dnd/npc-interactions/prompts/instructions.md b/assets/dnd/npc-occurrences/prompts/instructions.md similarity index 86% rename from assets/dnd/npc-interactions/prompts/instructions.md rename to assets/dnd/npc-occurrences/prompts/instructions.md index 9750909..f12ada8 100644 --- a/assets/dnd/npc-interactions/prompts/instructions.md +++ b/assets/dnd/npc-occurrences/prompts/instructions.md @@ -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 -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; never invent an ID or substitute a similar name. Do not summarize, infer relationships, sentiment, factions, motives, aliases, or persistent state. Do not identify player characters, anonymous groups, or -invented NPCs. Split records when an NPC's 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 present. diff --git a/assets/dnd/npc-interactions/prompts/prompt.yaml b/assets/dnd/npc-occurrences/prompts/prompt.yaml similarity index 97% rename from assets/dnd/npc-interactions/prompts/prompt.yaml rename to assets/dnd/npc-occurrences/prompts/prompt.yaml index 3b0bc86..6454911 100644 --- a/assets/dnd/npc-interactions/prompts/prompt.yaml +++ b/assets/dnd/npc-occurrences/prompts/prompt.yaml @@ -1,4 +1,4 @@ -id: dnd.npc_interactions +id: dnd.npc_occurrences version: "v1" default_profile: dnd-extraction inputs: diff --git a/assets/dnd/npc-interactions/schemas/dnd_npc_occurrences_llm.v1.json b/assets/dnd/npc-occurrences/schemas/dnd_npc_occurrences_llm.v1.json similarity index 100% rename from assets/dnd/npc-interactions/schemas/dnd_npc_occurrences_llm.v1.json rename to assets/dnd/npc-occurrences/schemas/dnd_npc_occurrences_llm.v1.json diff --git a/docs/config.md b/docs/config.md index 04ea208..6b259b0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -371,10 +371,10 @@ selected target declares them: | **glossary** | Optional text campaign glossary. | | **spell_catalog** | Optional JSON spell-catalog overlay for spell extraction and normalization. See [spell-catalog overlays](integrations/dnd-spell-catalog-overlays.md). | | **locations** | Required normalized location registry for location-occurrence extraction and normalization. | -| **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. | | **combat_turns** | Required normalized combat-turn artifact for enemy-event extraction. | -| **npc_interactions** | Required normalized NPC-interaction artifact for enemy-event extraction. | +| **npc_occurrences** | Required normalized NPC-occurrence artifact for enemy-event extraction. | Location-occurrence and enemy-event artifact slots have the following exact binding contracts. Durable semantics and wire shapes remain in their @@ -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 | | `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only | | `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only | -| `npc_interactions` | `dnd/npc-interaction-list` | `application/json` | 1,048,576 bytes | extract only | +| `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 | 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 normalized combat-turn module may use optional **npc_registry**. Location occurrences require **locations** for extraction and normalization. Enemy-event extraction @@ -404,9 +404,9 @@ generated bindings. | --- | --- | | Input | **seriatim** | | 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** | -| 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** | `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: [spells](integrations/dnd-spell-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), [item events](integrations/dnd-item-event-artifacts.md), [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** | | Combat turns | **extract/dnd/combat-turns/shape**, **extract/dnd/combat-turns/source_refs**, **extract/dnd/combat-turns/source_relatedness**, **normalize/dnd/combat-turns/invariants** | | Item events | **extract/dnd/item-events/shape**, **extract/dnd/item-events/source_refs**, **extract/dnd/item-events/source_relatedness**, **normalize/dnd/item-events/invariants** | -| NPC interactions | **extract/dnd/npc-interactions/shape**, **extract/dnd/npc-interactions/registry**, **extract/dnd/npc-interactions/source_refs**, **extract/dnd/npc-interactions/source_relatedness**, **normalize/dnd/npc-interactions/invariants** | +| NPC occurrences | **extract/dnd/npc-occurrences/shape**, **extract/dnd/npc-occurrences/registry**, **extract/dnd/npc-occurrences/source_refs**, **extract/dnd/npc-occurrences/source_relatedness**, **normalize/dnd/npc-occurrences/invariants** | | Scene descriptions | **extract/dnd/scene-descriptions/shape**, **extract/dnd/scene-descriptions/source_refs**, **extract/dnd/scene-descriptions/source_relatedness**, **normalize/dnd/scene-descriptions/invariants** | | Enemy events | **extract/dnd/enemy-events/shape**, **extract/dnd/enemy-events/engagements**, **extract/dnd/enemy-events/source_refs**, **extract/dnd/enemy-events/source_relatedness**, **normalize/dnd/enemy-events/invariants** | | Locations | **extract/dnd/locations/shape**, **extract/dnd/locations/source_refs**, **extract/dnd/locations/source_relatedness**, **normalize/dnd/locations/identity** | @@ -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 | | Combat turns | generic/valid_json, extract/dnd/combat-turns/shape, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | generic/valid_json, extract/dnd/combat-turns/shape, normalize/dnd/combat-turns/invariants, extract/dnd/combat-turns/source_refs, generic/valid_json_schema, extract/dnd/combat-turns/source_relatedness | | Item events | generic/valid_json, extract/dnd/item-events/shape, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | generic/valid_json, extract/dnd/item-events/shape, normalize/dnd/item-events/invariants, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | -| NPC interactions | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | generic/valid_json, extract/dnd/npc-interactions/shape, extract/dnd/npc-interactions/registry, normalize/dnd/npc-interactions/invariants, extract/dnd/npc-interactions/source_refs, generic/valid_json_schema, extract/dnd/npc-interactions/source_relatedness | +| NPC occurrences | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, normalize/dnd/npc-occurrences/invariants, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness | | Scene descriptions | generic/valid_json, extract/dnd/scene-descriptions/shape, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | generic/valid_json, extract/dnd/scene-descriptions/shape, normalize/dnd/scene-descriptions/invariants, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | | Enemy events | generic/valid_json, extract/dnd/enemy-events/shape, extract/dnd/enemy-events/engagements, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness | generic/valid_json, extract/dnd/enemy-events/shape, normalize/dnd/enemy-events/invariants, extract/dnd/enemy-events/source_refs, generic/valid_json_schema, extract/dnd/enemy-events/source_relatedness | | Locations | generic/valid_json, extract/dnd/locations/shape, extract/dnd/locations/source_refs, generic/valid_json_schema, extract/dnd/locations/source_relatedness | generic/valid_json, extract/dnd/locations/shape, normalize/dnd/locations/identity, extract/dnd/locations/source_refs, generic/valid_json_schema, extract/dnd/locations/source_relatedness | diff --git a/docs/integrations/dnd-combat-turn-artifacts.md b/docs/integrations/dnd-combat-turn-artifacts.md index d0e645b..6c9f1f7 100644 --- a/docs/integrations/dnd-combat-turn-artifacts.md +++ b/docs/integrations/dnd-combat-turn-artifacts.md @@ -63,7 +63,7 @@ chronology; and collapses only duplicates with the same actor identity, turn kind, and complete valid evidence. It does not infer turns, initiative, or actions from registry or scene data. -The [NPC-interaction artifact](dnd-npc-interaction-artifacts.md) records +The [NPC-occurrence artifact](dnd-npc-occurrence-artifacts.md) records broader NPC occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) uses combat turns as grounding only; turns do not establish an enemy event or its outcome. The [JSON output contract](json-output.md) defines publication, diff --git a/docs/integrations/dnd-enemy-event-artifacts.md b/docs/integrations/dnd-enemy-event-artifacts.md index 969a894..4d702c4 100644 --- a/docs/integrations/dnd-enemy-event-artifacts.md +++ b/docs/integrations/dnd-enemy-event-artifacts.md @@ -66,7 +66,7 @@ are positive and the start may not follow the end. | `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. A missed turn is insufficient. | The current transcript is the only event evidence. Campaign context and -normalized NPC, scene-description, combat-turn, and NPC-interaction artifacts +normalized NPC, scene-description, combat-turn, and NPC-occurrence artifacts can ground names or control combat eligibility, but none may supply event evidence. An outcome may share evidence with an engagement, in which case both events are retained. diff --git a/docs/integrations/dnd-item-event-artifacts.md b/docs/integrations/dnd-item-event-artifacts.md index e4e3970..8f9c75f 100644 --- a/docs/integrations/dnd-item-event-artifacts.md +++ b/docs/integrations/dnd-item-event-artifacts.md @@ -74,5 +74,5 @@ valid evidence. The [JSON output contract](json-output.md) defines publication. See [D&D module internals](../internal/dnd.md) for implementation details and the -[NPC-interaction artifact](dnd-npc-interaction-artifacts.md) for a distinct +[NPC-occurrence artifact](dnd-npc-occurrence-artifacts.md) for a distinct kind of occurrence. diff --git a/docs/integrations/dnd-npc-interaction-artifacts.md b/docs/integrations/dnd-npc-occurrence-artifacts.md similarity index 86% rename from docs/integrations/dnd-npc-interaction-artifacts.md rename to docs/integrations/dnd-npc-occurrence-artifacts.md index 118089a..58d4ff3 100644 --- a/docs/integrations/dnd-npc-interaction-artifacts.md +++ b/docs/integrations/dnd-npc-occurrence-artifacts.md @@ -1,7 +1,7 @@ -# D&D NPC Interaction Artifact +# D&D NPC Occurrence Artifact This contract defines the durable occurrence list produced by -`dnd/npc-interactions`. It records discrete, source-grounded interactions with +`dnd/npc-occurrences`. It records discrete, source-grounded occurrences with NPCs already present in a normalized registry; it does not extend that registry or summarize the session. @@ -27,13 +27,13 @@ Each occurrence has these required fields: | --- | --- | | `npc_id` | Exact durable ID from the required NPC registry. | | `name` | Non-empty canonical display name from the required NPC registry. | -| `kind` | One of the interaction categories below. | +| `kind` | One of the occurrence categories below. | | `source_refs` | One or more transcript evidence ranges. | Each source reference has exactly `source_id`, `start_unit_id`, and `end_unit_id`. It identifies an inclusive range in the current transcript; unit IDs are positive and the start may not follow the end. Extraction evidence -for an interaction is confined to its accepted chunk. +for an occurrence is confined to its accepted chunk. ```json { @@ -50,7 +50,7 @@ for an interaction is confined to its accepted chunk. } ``` -## Interaction categories +## Occurrence categories | Kind | Meaning | | --- | --- | @@ -70,16 +70,16 @@ for uncertain classification. 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 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 -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. Only entries with the same NPC ID, canonical name, kind, and complete valid evidence sequence are collapsed; distinct categories or evidence remain separate. See the [combat-turn artifact](dnd-combat-turn-artifacts.md) for combat-action occurrences. The [enemy-event artifact](dnd-enemy-event-artifacts.md) consumes -only `combat_opponent` interactions as grounding; they never establish an enemy +only `combat_opponent` occurrences as grounding; they never establish an enemy event or outcome. The [JSON output contract](json-output.md) defines publication. Pipeline mechanics are described in [D&D module internals](../internal/dnd.md). diff --git a/docs/integrations/dnd-npc-registry-artifacts.md b/docs/integrations/dnd-npc-registry-artifacts.md index a06cc77..a923707 100644 --- a/docs/integrations/dnd-npc-registry-artifacts.md +++ b/docs/integrations/dnd-npc-registry-artifacts.md @@ -70,12 +70,12 @@ descriptor. Only individually identifiable NPC names with transcript evidence belong in this artifact. Groups, generic roles, invented labels, and descriptive enrichment are excluded. Its source references prove registry provenance; they -do not become evidence for a spell, interaction, combat, or enemy-event +do not become evidence for a spell, occurrence, combat, or enemy-event occurrence. This registry can ground actor or caster names in the [spell](dnd-spell-artifacts.md) and [combat-turn](dnd-combat-turn-artifacts.md) artifacts. It is required to -resolve the canonical `name` in an [NPC interaction](dnd-npc-interaction-artifacts.md). +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 subject grounding and canonical display names. The [JSON output contract](json-output.md) defines publication, and diff --git a/docs/integrations/json-output.md b/docs/integrations/json-output.md index 176d0c4..e108f83 100644 --- a/docs/integrations/json-output.md +++ b/docs/integrations/json-output.md @@ -72,7 +72,7 @@ Each `lanes/.json` file is the codec-owned normalized JSON for that lane. Consumers should use the index descriptor’s schema identity rather than infer a lane schema from its name. The current D&D payload contracts are [spells](dnd-spell-artifacts.md), [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), [item events](dnd-item-event-artifacts.md), [scene descriptions](dnd-scene-description-artifacts.md), and diff --git a/docs/internal/dnd.md b/docs/internal/dnd.md index 4c8c3a8..2a81dfe 100644 --- a/docs/internal/dnd.md +++ b/docs/internal/dnd.md @@ -16,7 +16,7 @@ does not repeat their JSON shapes or schemas. | NPC registry | [NPC registry artifacts](../integrations/dnd-npc-registry-artifacts.md) | | Combat turns | [combat-turn artifacts](../integrations/dnd-combat-turn-artifacts.md) | | Item events | [item-event artifacts](../integrations/dnd-item-event-artifacts.md) | -| NPC interactions | [NPC-interaction artifacts](../integrations/dnd-npc-interaction-artifacts.md) | +| NPC occurrences | [NPC-occurrence artifacts](../integrations/dnd-npc-occurrence-artifacts.md) | | Scene descriptions | [scene-description artifacts](../integrations/dnd-scene-description-artifacts.md) | | Enemy events | [enemy-event artifacts](../integrations/dnd-enemy-event-artifacts.md) | | Locations | [location artifacts](../integrations/dnd-location-artifacts.md) | @@ -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. 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 bounded warnings for changes or collapsed duplicates. The NPC and location 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. 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 projections: they retain the current chunk’s classification data, not scene prose or evidence, and exist to route combat extraction. Enemy-event extraction also projects combat turns to `actor` and `turn_kind` and filters NPC -interactions to `combat_opponent` names and kinds. Location registries project +occurrences to `combat_opponent` names and kinds. Location registries project ordered `{id, name}` pairs to location-occurrence extraction and normalization; exact ID/name matching keeps same-name locations distinguishable. These compact projections, like NPC grounding, are source-free guidance and never event @@ -163,9 +163,9 @@ shared helper changes. | 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. | | 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. | -| Enemy events | Requires NPC, scene-description, combat-turn, and NPC-interaction artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. | +| Enemy events | Requires NPC, scene-description, combat-turn, and NPC-occurrence artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. | | Locations | Produces a source-anchored, session-scoped registry. Its LLM-assisted reconciliation is proposal-only and never collapses same-name places without validated identity and evidence rules. | | Location 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. | diff --git a/examples/dnd-complete.config.yml b/examples/dnd-complete.config.yml index 3324d38..725654c 100644 --- a/examples/dnd-complete.config.yml +++ b/examples/dnd-complete.config.yml @@ -41,7 +41,7 @@ pipelines: - npc_registry - spells - combat-turns - - npc-interactions + - npc-occurrences - enemy-events steps: # Establish session-wide reference artifacts alongside independent item events. @@ -110,12 +110,12 @@ pipelines: retries: 2 merge: appendorder normalize: dnd/combat-turns - npc-interactions: + npc-occurrences: extract: - module: dnd/npc-interactions + module: dnd/npc-occurrences retries: 2 merge: appendorder - normalize: dnd/npc-interactions + normalize: dnd/npc-occurrences location-occurrences: extract: module: dnd/location-occurrences @@ -136,10 +136,10 @@ pipelines: artifact: step: extract-events lane: combat-turns - npc_interactions: + npc_occurrences: artifact: step: extract-events - lane: npc-interactions + lane: npc-occurrences artifacts: enemy-events: extract: diff --git a/internal/cli/dnd_enemy_events_contract_test.go b/internal/cli/dnd_enemy_events_contract_test.go index 640a184..cb07410 100644 --- a/internal/cli/dnd_enemy_events_contract_test.go +++ b/internal/cli/dnd_enemy_events_contract_test.go @@ -27,7 +27,7 @@ import ( itemevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locations" - npcinteractions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" + npcoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry" scenedescriptions "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" @@ -58,7 +58,7 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T "npc_registry": {step: "describe-session", lane: "npc_registry"}, "scene_descriptions": {step: "describe-session", lane: "scene-descriptions"}, "combat_turns": {step: "extract-events", lane: "combat-turns"}, - "npc_interactions": {step: "extract-events", lane: "npc-interactions"}, + "npc_occurrences": {step: "extract-events", lane: "npc-occurrences"}, } { binding, found := generatedReferenceBinding(lane.ExtractReferences.Bindings, slot) if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane { @@ -78,7 +78,7 @@ func TestProductionEnemyEventConfigurationResolvesGeneratedHandoffs(t *testing.T if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) { t.Fatalf("enemy event normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec) } - for _, slot := range []string{"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) { 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) } for slot, required := range map[string]string{ - "npc_registry": "Kesh", - "combat_turns": "Kesh", - "npc_interactions": "Kesh", + "npc_registry": "Kesh", + "combat_turns": "Kesh", + "npc_occurrences": "Kesh", } { input, ok := request.Inputs[slot] if !ok || !strings.Contains(string(input.Content), required) || strings.Contains(string(input.Content), "source_refs") || strings.Contains(string(input.Content), "start_unit_id") { @@ -257,7 +257,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque content = []byte(`{"events":[]}`) case combat.PromptID: content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`) - case npcinteractions.PromptID: + case npcoccurrences.PromptID: if combatScene { var registry struct { NPCs []struct { diff --git a/internal/cli/dnd_interactions_contract_test.go b/internal/cli/dnd_occurrences_contract_test.go similarity index 67% rename from internal/cli/dnd_interactions_contract_test.go rename to internal/cli/dnd_occurrences_contract_test.go index aa0a3c4..95ede60 100644 --- a/internal/cli/dnd_interactions_contract_test.go +++ b/internal/cli/dnd_occurrences_contract_test.go @@ -8,16 +8,16 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" - interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions" - interactionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcinteractions" + occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences" + occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences" 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" ) -func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) { +func TestProductionNPCOccurrencePipelineResolvesAndPrepares(t *testing.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 { t.Fatalf("ResolvePipeline() error = %v", err) } @@ -25,8 +25,8 @@ func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) { t.Fatalf("resolved pipeline = %#v", resolved) } lane := resolved.Steps[1].ArtifactLanes[0] - if lane.ArtifactKind != dnd.NPCOccurrenceListKind || lane.Extract.Module != interactionextract.Key || lane.Normalize.Module != interactionnormalize.Key { - t.Fatalf("interaction lane = %#v", lane) + if lane.ArtifactKind != dnd.NPCOccurrenceListKind || lane.Extract.Module != occurrenceextract.Key || lane.Normalize.Module != occurrencenormalize.Key { + t.Fatalf("occurrence lane = %#v", lane) } for _, bindings := range [][]pipeline.ReferenceBinding{lane.ExtractReferences.Bindings, lane.NormalizeReferences.Bindings} { if len(bindings) != 1 || bindings[0].SlotName != "npc_registry" || bindings[0].Artifact == nil || bindings[0].Artifact.Step != "npc-registry" || bindings[0].Artifact.Lane != "npc_registry" { @@ -39,15 +39,15 @@ func TestProductionNPCInteractionPipelineResolvesAndPrepares(t *testing.T) { catalog := catalogFromRegistries(components.registries) codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCOccurrenceListKind) - if !ok || codecSpec.Schema.ID != interactioncodec.SchemaID || codecSpec.Schema.Version != interactioncodec.SchemaVersion { - t.Fatalf("NPC interaction codec spec = %#v", codecSpec) + if !ok || codecSpec.Schema.ID != occurrencecodec.SchemaID || codecSpec.Schema.Version != occurrencecodec.SchemaVersion { + t.Fatalf("NPC occurrence codec spec = %#v", codecSpec) } } -func TestProductionNPCInteractionReferencesRequireEarlierCompatibleProducer(t *testing.T) { +func TestProductionNPCOccurrenceReferencesRequireEarlierCompatibleProducer(t *testing.T) { components := productionTestComponents(t) 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].Artifacts["seed"] = laterProfile.Steps[0].Artifacts["npc_registry"] delete(laterProfile.Steps[0].Artifacts, "npc_registry") @@ -60,10 +60,10 @@ func TestProductionNPCInteractionReferencesRequireEarlierCompatibleProducer(t *t profile pipeline.PipelineProfile want string }{ - {name: "missing", profile: npcInteractionProfile(pipeline.ReferenceSource{}), want: "source must not be empty"}, - {name: "same step", profile: npcInteractionProfile(pipeline.GeneratedReference("interactions", "interactions")), want: "earlier step"}, + {name: "missing", profile: npcOccurrenceProfile(pipeline.ReferenceSource{}), want: "source must not be empty"}, + {name: "same step", profile: npcOccurrenceProfile(pipeline.GeneratedReference("occurrences", "occurrences")), want: "earlier step"}, {name: "later step", profile: laterProfile, want: "earlier step"}, - {name: "wrong artifact kind", profile: 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")} 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) catalog := catalogFromRegistries(components.registries) root := t.TempDir() @@ -95,7 +95,7 @@ func TestProductionNPCInteractionReferencesRejectIncompatibleExternalRegistries( if err := os.WriteFile(path, []byte(test.content), 0o600); err != nil { t.Fatal(err) } - resolved, err := pipeline.ResolvePipeline(npcInteractionProfile(pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog) + resolved, err := pipeline.ResolvePipeline(npcOccurrenceProfile(pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog) if err != nil { 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{ - ID: "dnd-npc-interactions", + ID: "dnd-npc-occurrences", Input: pipeline.Binding("seriatim"), Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}}, Output: pipeline.Binding("json"), @@ -126,8 +126,8 @@ func npcInteractionProfile(reference pipeline.ReferenceSource) pipeline.Pipeline {ID: "npc-registry", Artifacts: map[string]pipeline.ArtifactLaneProfile{ "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{ - "interactions": {Extract: pipeline.Binding(interactionextract.Key), Normalize: pipeline.Binding(interactionnormalize.Key)}, + {ID: "occurrences", References: map[string]pipeline.ReferenceSource{"npc_registry": reference}, Artifacts: map[string]pipeline.ArtifactLaneProfile{ + "occurrences": {Extract: pipeline.Binding(occurrenceextract.Key), Normalize: pipeline.Binding(occurrencenormalize.Key)}, }}, }, } diff --git a/internal/cli/example_contract_test.go b/internal/cli/example_contract_test.go index f103886..1ca70e9 100644 --- a/internal/cli/example_contract_test.go +++ b/internal/cli/example_contract_test.go @@ -58,7 +58,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) { t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err) } if example.name == "complete" { - if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-events,locations,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) } locationLane := referenceContractLane(t, materialized, "locations") @@ -102,7 +102,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) { "npc_registry": {step: "describe-session", lane: "npc_registry"}, "scene_descriptions": {step: "describe-session", lane: "scene-descriptions"}, "combat_turns": {step: "extract-events", lane: "combat-turns"}, - "npc_interactions": {step: "extract-events", lane: "npc-interactions"}, + "npc_occurrences": {step: "extract-events", lane: "npc-occurrences"}, } { binding, found := generatedReferenceBinding(enemyEventLane.ExtractReferences.Bindings, slot) if !found || binding.Artifact.Step != want.step || binding.Artifact.Lane != want.lane { diff --git a/internal/cli/production_contract_test.go b/internal/cli/production_contract_test.go index 3306b24..7eaaaad 100644 --- a/internal/cli/production_contract_test.go +++ b/internal/cli/production_contract_test.go @@ -165,7 +165,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { {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/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: enemyeventextract.Key, want: contracts.ExecutionClassLLMBacked}, {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/combat-turns", 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: enemyeventnormalize.Key, want: contracts.ExecutionClassDeterministic}, {stage: pipeline.StageOutput, key: "json", want: contracts.ExecutionClassDeterministic}, diff --git a/internal/modules/dnd/codec/npcinteractions/assets/schemas/dnd_npc_occurrences.v1.json b/internal/modules/dnd/codec/npcoccurrences/assets/schemas/dnd_npc_occurrences.v1.json similarity index 100% rename from internal/modules/dnd/codec/npcinteractions/assets/schemas/dnd_npc_occurrences.v1.json rename to internal/modules/dnd/codec/npcoccurrences/assets/schemas/dnd_npc_occurrences.v1.json diff --git a/internal/modules/dnd/codec/npcinteractions/codec.go b/internal/modules/dnd/codec/npcoccurrences/codec.go similarity index 97% rename from internal/modules/dnd/codec/npcinteractions/codec.go rename to internal/modules/dnd/codec/npcoccurrences/codec.go index 0f3d301..39c0e9c 100644 --- a/internal/modules/dnd/codec/npcinteractions/codec.go +++ b/internal/modules/dnd/codec/npcoccurrences/codec.go @@ -1,5 +1,5 @@ -// Package npcinteractions encodes durable D&D NPC occurrence artifacts. -package npcinteractions +// Package npcoccurrences encodes durable D&D NPC occurrence artifacts. +package npcoccurrences import ( "embed" diff --git a/internal/modules/dnd/codec/npcinteractions/codec_test.go b/internal/modules/dnd/codec/npcoccurrences/codec_test.go similarity index 96% rename from internal/modules/dnd/codec/npcinteractions/codec_test.go rename to internal/modules/dnd/codec/npcoccurrences/codec_test.go index 85bcf26..af2180f 100644 --- a/internal/modules/dnd/codec/npcinteractions/codec_test.go +++ b/internal/modules/dnd/codec/npcoccurrences/codec_test.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "bytes" @@ -111,7 +111,7 @@ func TestCodecStrictlyRejectsMalformedUnknownAndTrailingJSON(t *testing.T) { for _, test := range []struct{ name, raw, want string }{ {"malformed", `{`, "decode dnd npc occurrence list"}, {"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"}, {"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{ dnd.NPCOccurrenceKindMentioned, dnd.NPCOccurrenceKindNoncombatPresence, diff --git a/internal/modules/dnd/codec/npcinteractions/testdata/dnd_npc_occurrences.v1.json b/internal/modules/dnd/codec/npcoccurrences/testdata/dnd_npc_occurrences.v1.json similarity index 100% rename from internal/modules/dnd/codec/npcinteractions/testdata/dnd_npc_occurrences.v1.json rename to internal/modules/dnd/codec/npcoccurrences/testdata/dnd_npc_occurrences.v1.json diff --git a/internal/modules/dnd/extract/enemyevents/extractor_test.go b/internal/modules/dnd/extract/enemyevents/extractor_test.go index b38dd0c..6da25d1 100644 --- a/internal/modules/dnd/extract/enemyevents/extractor_test.go +++ b/internal/modules/dnd/extract/enemyevents/extractor_test.go @@ -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") { t.Fatalf("combat grounding = %s", got) } - if got := string(request.Inputs[NPCInteractionReferenceSlot].Content); !strings.Contains(got, `"kind":"combat_opponent"`) || strings.Contains(got, "Aria") { - t.Fatalf("interaction grounding = %s", got) + if got := string(request.Inputs[NPCOccurrenceReferenceSlot].Content); !strings.Contains(got, `"kind":"combat_opponent"`) || strings.Contains(got, "Aria") { + t.Fatalf("occurrence grounding = %s", got) } } diff --git a/internal/modules/dnd/extract/enemyevents/grounding.go b/internal/modules/dnd/extract/enemyevents/grounding.go index ec202be..6663c4e 100644 --- a/internal/modules/dnd/extract/enemyevents/grounding.go +++ b/internal/modules/dnd/extract/enemyevents/grounding.go @@ -15,7 +15,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" 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" scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" @@ -27,7 +27,7 @@ const ( NPCRegistryReferenceSlot = npcregistry.ReferenceSlot SceneDescriptionReferenceSlot = sceneregistry.ReferenceSlot CombatTurnReferenceSlot = "combat_turns" - NPCInteractionReferenceSlot = "npc_interactions" + NPCOccurrenceReferenceSlot = "npc_occurrences" ReferenceMaxBytes = 1048576 promptProjectionMediaType = "application/json" ) @@ -67,10 +67,10 @@ func referenceSlots() []contracts.ReferenceSlot { MaxBytes: ReferenceMaxBytes, }, contracts.ReferenceSlot{ - Name: NPCInteractionReferenceSlot, - Description: "Required NPC-interaction artifact used only as source-free enemy-event grounding.", + Name: NPCOccurrenceReferenceSlot, + Description: "Required NPC-occurrence artifact used only as source-free enemy-event grounding.", Required: true, - AcceptedMediaTypes: []string{interactioncodec.MediaType}, + AcceptedMediaTypes: []string{occurrencecodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCOccurrenceListKind}, MaxBytes: ReferenceMaxBytes, }, @@ -86,14 +86,14 @@ type groundingResolver struct { npcs *npcregistry.Resolver scenes *sceneregistry.Resolver - combatTurns *contracts.LLMInputMaterial - npcInteractions *contracts.LLMInputMaterial + combatTurns *contracts.LLMInputMaterial + npcOccurrences *contracts.LLMInputMaterial } type grounding struct { - npcInput contracts.LLMInputMaterial - combatTurnInput contracts.LLMInputMaterial - npcInteractionInput contracts.LLMInputMaterial + npcInput contracts.LLMInputMaterial + combatTurnInput contracts.LLMInputMaterial + npcOccurrenceInput contracts.LLMInputMaterial } func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver, error) { @@ -109,15 +109,15 @@ func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver if err != nil { return nil, err } - npcInteractions, err := prepareNPCInteractionInput(references) + npcOccurrences, err := prepareNPCOccurrenceInput(references) if err != nil { return nil, err } return &groundingResolver{ - npcs: npcs, - scenes: scenes, - combatTurns: combatTurns, - npcInteractions: npcInteractions, + npcs: npcs, + scenes: scenes, + combatTurns: combatTurns, + npcOccurrences: npcOccurrences, }, nil } @@ -136,14 +136,14 @@ func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (groundin if err != nil { return grounding{}, err } - npcInteractions, err := resolveInput(references, NPCInteractionReferenceSlot, r.npcInteractions, prepareNPCInteractionInput) + npcOccurrences, err := resolveInput(references, NPCOccurrenceReferenceSlot, r.npcOccurrences, prepareNPCOccurrenceInput) if err != nil { return grounding{}, err } return grounding{ - npcInput: npcs.PromptInput(), - combatTurnInput: combatTurns, - npcInteractionInput: npcInteractions, + npcInput: npcs.PromptInput(), + combatTurnInput: combatTurns, + npcOccurrenceInput: npcOccurrences, }, nil } @@ -188,9 +188,9 @@ func resolveInput(references contracts.ReferenceSet, slot string, seeded *contra func (g grounding) PromptInputs() contracts.LLMInputSet { return contracts.LLMInputSet{ - NPCRegistryReferenceSlot: g.npcInput.Clone(), - CombatTurnReferenceSlot: g.combatTurnInput.Clone(), - NPCInteractionReferenceSlot: g.npcInteractionInput.Clone(), + NPCRegistryReferenceSlot: g.npcInput.Clone(), + CombatTurnReferenceSlot: g.combatTurnInput.Clone(), + NPCOccurrenceReferenceSlot: g.npcOccurrenceInput.Clone(), } } @@ -212,22 +212,22 @@ func prepareCombatTurnInput(references contracts.ReferenceSet) (*contracts.LLMIn return newPromptInput(CombatTurnReferenceSlot, content), nil } -func prepareNPCInteractionInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) { - item, ok, err := referenceItem(references, NPCInteractionReferenceSlot, interactioncodec.MediaType) +func prepareNPCOccurrenceInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) { + item, ok, err := referenceItem(references, NPCOccurrenceReferenceSlot, occurrencecodec.MediaType) if err != nil || !ok { return nil, err } - value, err := interactioncodec.New().Decode(item.Content) + value, err := occurrencecodec.New().Decode(item.Content) 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 { - Interactions []npcInteractionProjection `json:"npc_interactions"` - }{Interactions: projectNPCInteractions(value.Occurrences)}) + Occurrences []npcOccurrenceProjection `json:"npc_occurrences"` + }{Occurrences: projectNPCOccurrences(value.Occurrences)}) 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) { @@ -271,16 +271,16 @@ func projectCombatTurns(turns []dnd.CombatTurn) []combatTurnProjection { return projection } -type npcInteractionProjection struct { +type npcOccurrenceProjection struct { Name string `json:"name"` Kind dnd.NPCOccurrenceKind `json:"kind"` } -func projectNPCInteractions(occurrences []dnd.NPCOccurrence) []npcInteractionProjection { - projection := make([]npcInteractionProjection, 0, len(occurrences)) +func projectNPCOccurrences(occurrences []dnd.NPCOccurrence) []npcOccurrenceProjection { + projection := make([]npcOccurrenceProjection, 0, len(occurrences)) for _, occurrence := range occurrences { 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 diff --git a/internal/modules/dnd/extract/enemyevents/grounding_test.go b/internal/modules/dnd/extract/enemyevents/grounding_test.go index e72df92..b428a07 100644 --- a/internal/modules/dnd/extract/enemyevents/grounding_test.go +++ b/internal/modules/dnd/extract/enemyevents/grounding_test.go @@ -10,7 +10,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" 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" scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" @@ -36,7 +36,7 @@ func TestReferenceSlotsDescribeRequiredTypedArtifacts(t *testing.T) { {NPCRegistryReferenceSlot, dnd.NPCRegistryKind}, {SceneDescriptionReferenceSlot, dnd.SceneDescriptionListKind}, {CombatTurnReferenceSlot, dnd.CombatTurnListKind}, - {NPCInteractionReferenceSlot, dnd.NPCOccurrenceListKind}, + {NPCOccurrenceReferenceSlot, dnd.NPCOccurrenceListKind}, } { 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 { @@ -61,9 +61,9 @@ func TestGroundingProducesExactSourceFreePromptInputs(t *testing.T) { } inputs := resolved.PromptInputs() want := map[string]string{ - NPCRegistryReferenceSlot: `{"npcs":[{"name":"Ashfang"}]}`, - CombatTurnReferenceSlot: `{"combat_turns":[{"actor":"Ashfang","turn_kind":"turn"},{"actor":"Aria","turn_kind":"reaction"}]}`, - NPCInteractionReferenceSlot: `{"npc_interactions":[{"name":"Ashfang","kind":"combat_opponent"}]}`, + NPCRegistryReferenceSlot: `{"npcs":[{"name":"Ashfang"}]}`, + CombatTurnReferenceSlot: `{"combat_turns":[{"actor":"Ashfang","turn_kind":"turn"},{"actor":"Aria","turn_kind":"reaction"}]}`, + NPCOccurrenceReferenceSlot: `{"npc_occurrences":[{"name":"Ashfang","kind":"combat_opponent"}]}`, } if len(inputs) != len(want) { t.Fatalf("PromptInputs() = %#v", inputs) @@ -153,7 +153,7 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) { {NPCRegistryReferenceSlot, "NPC registry"}, {SceneDescriptionReferenceSlot, "scene descriptions"}, {CombatTurnReferenceSlot, CombatTurnReferenceSlot}, - {NPCInteractionReferenceSlot, NPCInteractionReferenceSlot}, + {NPCOccurrenceReferenceSlot, NPCOccurrenceReferenceSlot}, } { t.Run("missing "+test.slot, func(t *testing.T) { resolver, err := newGroundingResolver(withoutSlot(valid, test.slot)) @@ -176,7 +176,7 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) { set contracts.ReferenceSet }{ {"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})}, {"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 { 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("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)}}, SceneDescriptionReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(SceneDescriptionReferenceSlot, sceneContent)}}, CombatTurnReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(CombatTurnReferenceSlot, turnContent)}}, - NPCInteractionReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCInteractionReferenceSlot, interactionContent)}}, + NPCOccurrenceReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCOccurrenceReferenceSlot, interactionContent)}}, }} } diff --git a/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go b/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go index 1166ca4..e256479 100644 --- a/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/enemyevents/prompt_assets_test.go @@ -32,7 +32,7 @@ func TestRegisterPromptAssetsAndPrepareEnemyEventPrompt(t *testing.T) { func TestEnemyEventPromptRequiresGroundingInputs(t *testing.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) { inputs := enemyEventPromptInputs() delete(inputs, inputName) @@ -79,12 +79,12 @@ func newEnemyEventPromptEngine(t *testing.T) *promptkit.Engine { func enemyEventPromptInputs() map[string]promptkit.ArtifactRef { return map[string]promptkit.ArtifactRef{ - "transcript": promptkit.Inline(`{"value":"enemy-transcript"}`), - "players": promptkit.Inline("enemy-player"), - "party": promptkit.Inline("enemy-party"), - "glossary": promptkit.Inline("enemy-glossary"), - "npc_registry": promptkit.Inline(`{"npcs":[{"name":"enemy-npc"}]}`), - "combat_turns": promptkit.Inline(`{"combat_turns":[{"actor":"enemy-turn","turn_kind":"turn"}]}`), - "npc_interactions": promptkit.Inline(`{"npc_interactions":[{"name":"enemy-opponent","kind":"combat_opponent"}]}`), + "transcript": promptkit.Inline(`{"value":"enemy-transcript"}`), + "players": promptkit.Inline("enemy-player"), + "party": promptkit.Inline("enemy-party"), + "glossary": promptkit.Inline("enemy-glossary"), + "npc_registry": promptkit.Inline(`{"npcs":[{"name":"enemy-npc"}]}`), + "combat_turns": promptkit.Inline(`{"combat_turns":[{"actor":"enemy-turn","turn_kind":"turn"}]}`), + "npc_occurrences": promptkit.Inline(`{"npc_occurrences":[{"name":"enemy-opponent","kind":"combat_opponent"}]}`), } } diff --git a/internal/modules/dnd/extract/npcinteractions/model.go b/internal/modules/dnd/extract/npcinteractions/model.go deleted file mode 100644 index 9b11474..0000000 --- a/internal/modules/dnd/extract/npcinteractions/model.go +++ /dev/null @@ -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"` -} diff --git a/internal/modules/dnd/extract/npcinteractions/canonicalize.go b/internal/modules/dnd/extract/npcoccurrences/canonicalize.go similarity index 86% rename from internal/modules/dnd/extract/npcinteractions/canonicalize.go rename to internal/modules/dnd/extract/npcoccurrences/canonicalize.go index a461833..832d41a 100644 --- a/internal/modules/dnd/extract/npcinteractions/canonicalize.go +++ b/internal/modules/dnd/extract/npcoccurrences/canonicalize.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "sort" @@ -66,7 +66,7 @@ func canonicalOccurrenceList(response extractionResponse, sourceID string) dnd.N return dnd.NPCOccurrenceList{Occurrences: occurrences} } -func canonicalSourceRefs(refs []interactionSourceRefResponse, sourceID string) []source.SourceRef { +func canonicalSourceRefs(refs []occurrenceSourceRefResponse, sourceID string) []source.SourceRef { if refs == nil { return nil } @@ -77,13 +77,13 @@ func canonicalSourceRefs(refs []interactionSourceRefResponse, sourceID string) [ return out } -func occurrenceResponseRefs(refs []source.SourceRef) []interactionSourceRefResponse { +func occurrenceResponseRefs(refs []source.SourceRef) []occurrenceSourceRefResponse { if refs == nil { return nil } - values := make([]interactionSourceRefResponse, len(refs)) + values := make([]occurrenceSourceRefResponse, len(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 } diff --git a/internal/modules/dnd/extract/npcinteractions/extractor.go b/internal/modules/dnd/extract/npcoccurrences/extractor.go similarity index 94% rename from internal/modules/dnd/extract/npcinteractions/extractor.go rename to internal/modules/dnd/extract/npcoccurrences/extractor.go index fcbdabf..620254a 100644 --- a/internal/modules/dnd/extract/npcinteractions/extractor.go +++ b/internal/modules/dnd/extract/npcoccurrences/extractor.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "context" @@ -13,8 +13,8 @@ import ( ) const ( - Key = "dnd/npc-interactions" - mappingPolicy = "dnd.npc_interactions.extract_mapping.v2" + Key = "dnd/npc-occurrences" + mappingPolicy = "dnd.npc_occurrences.extract_mapping.v2" ) const ( @@ -28,21 +28,21 @@ var requiredCapabilities = []string{ } var providedCapabilities = []string{ - "dnd.npc_interactions", + "dnd.npc_occurrences", } var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ - Glossary: "Optional campaign glossary reference material used only for interaction disambiguation.", - Party: "Optional party roster reference material used only for interaction disambiguation.", - Players: "Optional player list reference material used only for interaction disambiguation.", - Roster: "Deprecated alias for party roster 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 occurrence disambiguation.", + Players: "Optional player list reference material used only for occurrence disambiguation.", + Roster: "Deprecated alias for party roster reference material used only for occurrence disambiguation.", } func referenceSlots() []contracts.ReferenceSlot { slots := shared.ReferenceSlots(referenceSlotDescriptions) slots = append(slots, contracts.ReferenceSlot{ 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, AcceptedMediaTypes: []string{"application/json"}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind}, @@ -225,5 +225,5 @@ func DecodeOptions(options map[string]any) (Options, 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...) } diff --git a/internal/modules/dnd/extract/npcinteractions/extractor_test.go b/internal/modules/dnd/extract/npcoccurrences/extractor_test.go similarity index 79% rename from internal/modules/dnd/extract/npcinteractions/extractor_test.go rename to internal/modules/dnd/extract/npcoccurrences/extractor_test.go index 64220e1..cdc106a 100644 --- a/internal/modules/dnd/extract/npcinteractions/extractor_test.go +++ b/internal/modules/dnd/extract/npcoccurrences/extractor_test.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "context" @@ -17,14 +17,14 @@ import ( ) func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) { - client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{ - {NPCID: identity.DeriveID("Other"), Name: "Other", Kind: "other", SourceRefs: interactionRefs(30, 30)}, - {NPCID: identity.DeriveID("Opponent"), Name: "Opponent", Kind: "combat_opponent", SourceRefs: interactionRefs(20, 20)}, - {NPCID: identity.DeriveID("Ally"), Name: "Ally", Kind: "combat_ally", SourceRefs: interactionRefs(5, 5)}, - {NPCID: identity.DeriveID("Speaker"), Name: "Speaker", Kind: "dialogue", SourceRefs: append(interactionRefs(2, 2), interactionRefs(2, 2)...)}, - {NPCID: identity.DeriveID("Present"), Name: "Present", Kind: "noncombat_presence", SourceRefs: interactionRefs(7, 7)}, - {NPCID: identity.DeriveID("Mentioned"), Name: "Mentioned", Kind: "mentioned", SourceRefs: interactionRefs(10, 10)}, - {NPCID: identity.DeriveID("Invalid"), Name: "Invalid", Kind: "unsupported", SourceRefs: interactionRefs(0, 0)}, + client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{ + {NPCID: identity.DeriveID("Other"), Name: "Other", Kind: "other", SourceRefs: occurrenceRefs(30, 30)}, + {NPCID: identity.DeriveID("Opponent"), Name: "Opponent", Kind: "combat_opponent", SourceRefs: occurrenceRefs(20, 20)}, + {NPCID: identity.DeriveID("Ally"), Name: "Ally", Kind: "combat_ally", SourceRefs: occurrenceRefs(5, 5)}, + {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: occurrenceRefs(7, 7)}, + {NPCID: identity.DeriveID("Mentioned"), Name: "Mentioned", Kind: "mentioned", SourceRefs: occurrenceRefs(10, 10)}, + {NPCID: identity.DeriveID("Invalid"), Name: "Invalid", Kind: "unsupported", SourceRefs: occurrenceRefs(0, 0)}, }}} references := requiredRegistryReferences(t, "Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid") req := extractionRequest() @@ -34,10 +34,10 @@ func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) { if err != nil { t.Fatalf("Extract() error = %v", err) } - if got := interactionNames(result.Value); !reflect.DeepEqual(got, []string{"Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid"}) { - t.Fatalf("interaction order = %#v", got) + if got := occurrenceNames(result.Value); !reflect.DeepEqual(got, []string{"Mentioned", "Speaker", "Present", "Ally", "Opponent", "Other", "Invalid"}) { + 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.NPCOccurrenceKindDialogue, dnd.NPCOccurrenceKindNoncombatPresence, @@ -46,7 +46,7 @@ func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) { dnd.NPCOccurrenceKindOther, "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}}) { t.Fatalf("canonical source refs = %#v", refs) @@ -59,16 +59,16 @@ func TestExtractMapsEveryKindAndOrdersBySourcePosition(t *testing.T) { } } -func TestExtractUsesDocumentOrderForReferencesAndInteractions(t *testing.T) { - client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{ - {NPCID: identity.DeriveID("Later"), Name: "Later", Kind: "dialogue", SourceRefs: interactionRefs(10, 10)}, - {NPCID: identity.DeriveID("First"), Name: "First", Kind: "mentioned", SourceRefs: []interactionSourceRefResponse{ +func TestExtractUsesDocumentOrderForReferencesAndOccurrences(t *testing.T) { + client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{ + {NPCID: identity.DeriveID("Later"), Name: "Later", Kind: "dialogue", SourceRefs: occurrenceRefs(10, 10)}, + {NPCID: identity.DeriveID("First"), Name: "First", Kind: "mentioned", SourceRefs: []occurrenceSourceRefResponse{ {StartUnitID: 10, EndUnitID: 10}, {StartUnitID: 30, EndUnitID: 30}, {StartUnitID: 30, EndUnitID: 30}, {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") req := extractionRequest() @@ -81,16 +81,16 @@ func TestExtractUsesDocumentOrderForReferencesAndInteractions(t *testing.T) { if err != nil { t.Fatalf("Extract() error = %v", err) } - if got := interactionNames(result.Value); !reflect.DeepEqual(got, []string{"First", "Second", "Later"}) { - t.Fatalf("interaction order = %#v, want document chronology with stable equal-evidence ties", got) + if got := occurrenceNames(result.Value); !reflect.DeepEqual(got, []string{"First", "Second", "Later"}) { + t.Fatalf("occurrence order = %#v, want document chronology with stable equal-evidence ties", got) } 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}) { t.Fatalf("source refs = %#v, want document order with exact duplicate removed", refs) } refs[0].StartUnitID = 777 - for _, interaction := range client.response.Occurrences { - for _, ref := range interaction.SourceRefs { + for _, occurrence := range client.response.Occurrences { + for _, ref := range occurrence.SourceRefs { if ref.StartUnitID == 777 { 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") { 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) } } func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(t *testing.T) { - client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{{ - NPCID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", Kind: "dialogue", SourceRefs: interactionRefs(10, 10), + client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{{ + NPCID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", Kind: "dialogue", SourceRefs: occurrenceRefs(10, 10), }}}} references := requiredRegistryReferences(t, "Mira Thorn", "Hooded Guard") req := extractionRequest() @@ -130,7 +130,7 @@ func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(t *testing.T) { if strings.Contains(string(request.Inputs["transcript"].Content), "other-session") { 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 { t.Fatal(err) } @@ -140,7 +140,7 @@ func TestExtractUsesRegistryIDsAndCurrentTranscriptEvidence(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") { t.Fatalf("Extract() error = %v, want required registry failure", err) } @@ -169,11 +169,11 @@ func TestExtractRejectsUnknownIDsAndMismatchedNames(t *testing.T) { occurrence occurrenceResponse 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"}, - {"mismatched name", occurrenceResponse{NPCID: identity.DeriveID("Mira Thorn"), Name: "Hooded Guard", Kind: "dialogue", SourceRefs: interactionRefs(10, 10)}, "name does not match npc_id"}, + {"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: occurrenceRefs(10, 10)}, "name does not match npc_id"}, } { 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.References = references 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) { - client := &fakeInteractionsLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{}}} + client := &fakeOccurrencesLLMClient{response: extractionResponse{Occurrences: []occurrenceResponse{}}} references := requiredRegistryReferences(t, "Mira Thorn") req := extractionRequest() req.References = references @@ -212,12 +212,12 @@ func TestExtractAcceptsEmptyBoundRegistryAndEmptyResponse(t *testing.T) { 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.References = references result, err := newExtractor(t, client, references).Extract(context.Background(), req) 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") valid := extractionRequest() valid.References = references - extractor := newExtractor(t, &fakeInteractionsLLMClient{}, references) + extractor := newExtractor(t, &fakeOccurrencesLLMClient{}, references) var nilExtractor *Extractor for _, test := range []struct { 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) } } func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) { 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) } var registrySlot contracts.ReferenceSlot @@ -267,9 +267,9 @@ func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) { if ModuleSpec().ReferenceSlots[0].AcceptedMediaTypes[0] == "changed" { t.Fatal("ModuleSpec() returned mutable reference slots") } - extractorSlots := newExtractor(t, &fakeInteractionsLLMClient{}).ReferenceSlots() + extractorSlots := newExtractor(t, &fakeOccurrencesLLMClient{}).ReferenceSlots() 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") } registry := pipeline.NewExtractorRegistry() @@ -284,7 +284,7 @@ func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) { } references := requiredRegistryReferences(t, "Mira Thorn") - extractor := newExtractor(t, &fakeInteractionsLLMClient{}, references) + extractor := newExtractor(t, &fakeOccurrencesLLMClient{}, references) metadata := extractor.ManifestMetadata() for key, want := range map[string]string{ "prompt_id": PromptID, "prompt_version": SchemaVersion, "mapping_policy": mappingPolicy, @@ -320,7 +320,7 @@ func extractionRequest() contracts.TypedExtractionRequest { return contracts.TypedExtractionRequest{ Source: doc, Chunk: chunk, 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 { - return []interactionSourceRefResponse{{StartUnitID: start, EndUnitID: end}} +func occurrenceRefs(start, end int) []occurrenceSourceRefResponse { + return []occurrenceSourceRefResponse{{StartUnitID: start, EndUnitID: end}} } -func interactionNames(value dnd.NPCOccurrenceList) []string { +func occurrenceNames(value dnd.NPCOccurrenceList) []string { names := make([]string, len(value.Occurrences)) - for index, interaction := range value.Occurrences { - names[index] = interaction.Name + for index, occurrence := range value.Occurrences { + names[index] = occurrence.Name } return names } -func interactionKinds(value dnd.NPCOccurrenceList) []dnd.NPCOccurrenceKind { +func occurrenceKinds(value dnd.NPCOccurrenceList) []dnd.NPCOccurrenceKind { kinds := make([]dnd.NPCOccurrenceKind, len(value.Occurrences)) - for index, interaction := range value.Occurrences { - kinds[index] = interaction.Kind + for index, occurrence := range value.Occurrences { + kinds[index] = occurrence.Kind } return kinds } @@ -393,13 +393,13 @@ func newExtractor(t *testing.T, client contracts.StructuredLLMClient, references return extractor } -type fakeInteractionsLLMClient struct { +type fakeOccurrencesLLMClient struct { response extractionResponse err error 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)) if client.err != nil { return contracts.StructuredCompletionResponse{}, client.err diff --git a/internal/modules/dnd/extract/npcoccurrences/model.go b/internal/modules/dnd/extract/npcoccurrences/model.go new file mode 100644 index 0000000..ff8ad36 --- /dev/null +++ b/internal/modules/dnd/extract/npcoccurrences/model.go @@ -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"` +} diff --git a/internal/modules/dnd/extract/npcinteractions/model_test.go b/internal/modules/dnd/extract/npcoccurrences/model_test.go similarity index 79% rename from internal/modules/dnd/extract/npcinteractions/model_test.go rename to internal/modules/dnd/extract/npcoccurrences/model_test.go index 49f4376..cb1cc75 100644 --- a/internal/modules/dnd/extract/npcinteractions/model_test.go +++ b/internal/modules/dnd/extract/npcoccurrences/model_test.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "encoding/json" @@ -12,7 +12,7 @@ func TestExtractionResponsePreservesValidatorOwnedSemantics(t *testing.T) { t.Fatalf("json.Unmarshal() error = %v", err) } 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) } } diff --git a/internal/modules/dnd/extract/npcinteractions/prompt_assets.go b/internal/modules/dnd/extract/npcoccurrences/prompt_assets.go similarity index 83% rename from internal/modules/dnd/extract/npcinteractions/prompt_assets.go rename to internal/modules/dnd/extract/npcoccurrences/prompt_assets.go index 733882a..791f915 100644 --- a/internal/modules/dnd/extract/npcinteractions/prompt_assets.go +++ b/internal/modules/dnd/extract/npcoccurrences/prompt_assets.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "fmt" @@ -14,7 +14,7 @@ import ( const promptAssetRoot = "assets/prompts" var promptAssetManifest = shared.PromptAssetManifest{ - ModuleDir: "dnd.npc_interactions", + ModuleDir: "dnd.npc_occurrences", ModuleFiles: []promptfs.ModulePromptFile{ {Name: "prompt.yaml", Path: "prompts/prompt.yaml"}, {Name: "instructions.md", Path: "prompts/instructions.md"}, @@ -30,9 +30,9 @@ var promptAssetManifest = shared.PromptAssetManifest{ } 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 { - return nil, fmt.Errorf("scope NPC-interaction assets: %w", err) + return nil, fmt.Errorf("scope NPC-occurrence assets: %w", err) } return assets, nil } @@ -44,14 +44,14 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error { } promptFS, err := promptAssetManifest.PromptFS(assets) 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 { return err } schemas, err := fs.Sub(assets, "schemas") 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, ".") } diff --git a/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go b/internal/modules/dnd/extract/npcoccurrences/prompt_assets_test.go similarity index 81% rename from internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go rename to internal/modules/dnd/extract/npcoccurrences/prompt_assets_test.go index 7fc0539..71a2c72 100644 --- a/internal/modules/dnd/extract/npcinteractions/prompt_assets_test.go +++ b/internal/modules/dnd/extract/npcoccurrences/prompt_assets_test.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "context" @@ -11,7 +11,7 @@ import ( "gitea.maximumdirect.net/eric/promptkit" ) -func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) { +func TestRegisterPromptAssetsAndPrepareOccurrencePrompt(t *testing.T) { registry := llm.NewAssetRegistry() if err := RegisterPromptAssets(registry); err != nil { t.Fatal(err) @@ -28,21 +28,21 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) { t.Fatal(err) } 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...) if err != nil { t.Fatal(err) } - transcript := `{"units":[{"sentinel":"interaction-transcript"}]}` + transcript := `{"units":[{"sentinel":"occurrence-transcript"}]}` 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{ "transcript": promptkit.InlineWithURI("file:///session.json", transcript), - "players": promptkit.Inline("interaction-player"), + "players": promptkit.Inline("occurrence-player"), "party": promptkit.Inline("Mira: ranger"), "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 { @@ -58,7 +58,7 @@ func TestPromptMetadataDoesNotExposeAssetContent(t *testing.T) { if err != nil || !strings.HasPrefix(hash, "sha256:") { 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"} { if strings.Contains(strings.Join(mapValues(metadata), " "), forbidden) { t.Fatalf("metadata leaked prompt or schema content %q: %#v", forbidden, metadata) diff --git a/internal/modules/dnd/extract/npcinteractions/schema.go b/internal/modules/dnd/extract/npcoccurrences/schema.go similarity index 90% rename from internal/modules/dnd/extract/npcinteractions/schema.go rename to internal/modules/dnd/extract/npcoccurrences/schema.go index a0d4c3b..d93aec1 100644 --- a/internal/modules/dnd/extract/npcinteractions/schema.go +++ b/internal/modules/dnd/extract/npcoccurrences/schema.go @@ -1,9 +1,9 @@ -package npcinteractions +package npcoccurrences import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" const ( - PromptID = "dnd.npc_interactions" + PromptID = "dnd.npc_occurrences" ResponseSchemaKey = llm.ResponseSchemaKey("dnd_npc_occurrences_llm") ResponseSchemaID = "notarius.dnd.npc_occurrences.llm" ResponseSchemaName = "notarius_dnd_npc_occurrences_llm_v1" diff --git a/internal/modules/dnd/extract/npcinteractions/schema_test.go b/internal/modules/dnd/extract/npcoccurrences/schema_test.go similarity index 88% rename from internal/modules/dnd/extract/npcinteractions/schema_test.go rename to internal/modules/dnd/extract/npcoccurrences/schema_test.go index fe1ada2..ab6810c 100644 --- a/internal/modules/dnd/extract/npcinteractions/schema_test.go +++ b/internal/modules/dnd/extract/npcoccurrences/schema_test.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "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) { t.Fatalf("schema = %#v", schema) } - valid := validInteractionResponse() + valid := validOccurrenceResponse() content, err := json.Marshal(valid) if err != nil { t.Fatal(err) @@ -26,11 +26,11 @@ func TestResponseSchemaOwnsOnlyPrivateStructuralContract(t *testing.T) { t.Fatalf("valid response rejected: %v", err) } - semanticCandidate := validInteractionResponse() - interaction := semanticCandidate["occurrences"].([]any)[0].(map[string]any) - interaction["name"] = "" - interaction["kind"] = "unsupported" - ref := interaction["source_refs"].([]any)[0].(map[string]any) + semanticCandidate := validOccurrenceResponse() + occurrence := semanticCandidate["occurrences"].([]any)[0].(map[string]any) + occurrence["name"] = "" + occurrence["kind"] = "unsupported" + ref := occurrence["source_refs"].([]any)[0].(map[string]any) ref["start_unit_id"] = 0 ref["end_unit_id"] = -1 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" }, } { - candidate := validInteractionResponse() + candidate := validOccurrenceResponse() mutate(candidate["occurrences"].([]any)[0].(map[string]any)) content, err := json.Marshal(candidate) 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{ "npc_id": "npc:sha256:test", "name": "Mira Thorn", "kind": "dialogue", "source_refs": []any{map[string]any{"start_unit_id": 1, "end_unit_id": 2}}, diff --git a/internal/modules/dnd/normalize/npcinteractions/normalizer.go b/internal/modules/dnd/normalize/npcoccurrences/normalizer.go similarity index 87% rename from internal/modules/dnd/normalize/npcinteractions/normalizer.go rename to internal/modules/dnd/normalize/npcoccurrences/normalizer.go index 98a0d1f..76f3309 100644 --- a/internal/modules/dnd/normalize/npcinteractions/normalizer.go +++ b/internal/modules/dnd/normalize/npcoccurrences/normalizer.go @@ -1,5 +1,5 @@ -// Package npcinteractions normalizes merged D&D NPC interaction candidates. -package npcinteractions +// Package npcoccurrences normalizes merged D&D NPC occurrence candidates. +package npcoccurrences import ( "context" @@ -10,21 +10,21 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "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" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" ) const ( - Key = "dnd/npc-interactions" - normalizationPolicy = "dnd.npc_interactions.normalize.v2" + Key = "dnd/npc-occurrences" + normalizationPolicy = "dnd.npc_occurrences.normalize.v2" NormalizationPolicy = normalizationPolicy - ReasonCodeSourceRefsNormalized = "source_references_normalized" - ReasonCodeInteractionsReordered = "npc_interactions_reordered" - ReasonCodeDuplicateCollapsed = "duplicate_npc_interaction_collapsed" - ReasonCodeWarningsOmitted = "npc_interaction_normalization_warnings_omitted" + ReasonCodeSourceRefsNormalized = "source_references_normalized" + ReasonCodeOccurrencesReordered = "npc_occurrences_reordered" + ReasonCodeDuplicateCollapsed = "duplicate_npc_occurrence_collapsed" + ReasonCodeWarningsOmitted = "npc_occurrence_normalization_warnings_omitted" ) const ( @@ -36,10 +36,10 @@ var requiredCapabilities = []string{"merged"} var providedCapabilities = []string{"normalized"} var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ - Glossary: "Optional campaign glossary reference material used only for interaction disambiguation.", - Party: "Optional party roster reference material used only for interaction disambiguation.", - Players: "Optional player list reference material used only for interaction disambiguation.", - Roster: "Deprecated alias for party roster 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 occurrence disambiguation.", + Players: "Optional player list reference material used only for occurrence disambiguation.", + Roster: "Deprecated alias for party roster reference material used only for occurrence disambiguation.", } 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 { - 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 { if position == record.inputIndex { @@ -160,7 +160,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd } warnings = append(warnings, contracts.Warning{ 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), }) } @@ -168,7 +168,7 @@ func normalizeList(input dnd.NPCOccurrenceList, documentIndex source.DocumentInd output, duplicateWarnings := collapseDuplicates(records, documentIndex) warnings = append(warnings, duplicateWarnings...) 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) { @@ -181,7 +181,7 @@ func normalizeOccurrence(input dnd.NPCOccurrence, order shared.SourceRefOrder, r } output := cloneOccurrence(input) 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 { @@ -205,11 +205,11 @@ func collapseDuplicates(records []normalizedRecord, documentIndex source.Documen groups := make([]duplicateGroup, 0) groupByKey := make(map[string]int) for index, record := range records { - if !interactionmodel.ValidSourceRefs(documentIndex, record.occurrence.SourceRefs) { + if !occurrencemodel.ValidSourceRefs(documentIndex, record.occurrence.SourceRefs) { keep[index] = true continue } - key := interactionmodel.ExactIdentity(record.occurrence) + key := occurrencemodel.ExactIdentity(record.occurrence) groupIndex, exists := groupByKey[key] if !exists { groupByKey[key] = len(groups) @@ -243,7 +243,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning { Scope: occurrenceScope(retainedIndex), ReasonCode: ReasonCodeDuplicateCollapsed, 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 = append(slots, contracts.ReferenceSlot{ 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, AcceptedMediaTypes: []string{"application/json"}, 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 normalizerErrorf(format string, args ...any) error { - return fmt.Errorf("dnd NPC interactions normalizer: "+format, args...) + return fmt.Errorf("dnd NPC occurrences normalizer: "+format, args...) } diff --git a/internal/modules/dnd/normalize/npcinteractions/normalizer_test.go b/internal/modules/dnd/normalize/npcoccurrences/normalizer_test.go similarity index 91% rename from internal/modules/dnd/normalize/npcinteractions/normalizer_test.go rename to internal/modules/dnd/normalize/npcoccurrences/normalizer_test.go index 8ea9722..9a39a43 100644 --- a/internal/modules/dnd/normalize/npcinteractions/normalizer_test.go +++ b/internal/modules/dnd/normalize/npcoccurrences/normalizer_test.go @@ -1,4 +1,4 @@ -package npcinteractions +package npcoccurrences import ( "context" @@ -31,7 +31,7 @@ func TestNormalizeValidatesPairsAndClones(t *testing.T) { } 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}}) { - t.Fatalf("normalized interaction = %#v", got) + t.Fatalf("normalized occurrence = %#v", got) } if !hasWarning(result.Warnings, ReasonCodeSourceRefsNormalized) { t.Fatalf("warnings = %#v", result.Warnings) @@ -77,7 +77,7 @@ func TestNormalizeRejectsUnknownIDsAndMismatchedNames(t *testing.T) { t.Fatal(err) } 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}}}, } { input := dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{occurrence}} @@ -92,14 +92,14 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) { ref := func(unit int) source.SourceRef { 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{ - interaction("Borin", dnd.NPCOccurrenceKindMentioned, ref(90)), + occurrence("Borin", dnd.NPCOccurrenceKindMentioned, ref(90)), first, first, - interaction("Ária", dnd.NPCOccurrenceKindCombatAlly, ref(50)), - interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(10)), - interaction("Ária", dnd.NPCOccurrenceKindDialogue, ref(999)), + occurrence("Ária", dnd.NPCOccurrenceKindCombatAlly, ref(50)), + occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(10)), + occurrence("Ária", dnd.NPCOccurrenceKindDialogue, ref(999)), }} normalizer, err := New(Options{}, npcReferences(t)) if err != nil { @@ -111,12 +111,12 @@ func TestNormalizeOrdersAndCollapsesExactDuplicatesOnly(t *testing.T) { } got := result.Value.Occurrences 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 { 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) } } @@ -147,7 +147,7 @@ func TestNormalizeBoundsWarnings(t *testing.T) { for index := range doc.Units { doc.Units[index].ID = index + 1 unitID := count - index - input.Occurrences[index] = interaction( + input.Occurrences[index] = occurrence( "Ária", dnd.NPCOccurrenceKindDialogue, 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}} } diff --git a/internal/modules/dnd/npcinteractions/canonical.go b/internal/modules/dnd/npcoccurrences/canonical.go similarity index 95% rename from internal/modules/dnd/npcinteractions/canonical.go rename to internal/modules/dnd/npcoccurrences/canonical.go index 431418d..61cf950 100644 --- a/internal/modules/dnd/npcinteractions/canonical.go +++ b/internal/modules/dnd/npcoccurrences/canonical.go @@ -1,6 +1,6 @@ -// Package npcinteractions owns canonical ordering and exact-identity rules for -// D&D NPC interaction artifacts. -package npcinteractions +// Package npcoccurrences owns canonical ordering and exact-identity rules for +// D&D NPC occurrence artifacts. +package npcoccurrences import ( "strconv" diff --git a/internal/modules/dnd/register/chains.go b/internal/modules/dnd/register/chains.go index 2a51abf..2b227da 100644 --- a/internal/modules/dnd/register/chains.go +++ b/internal/modules/dnd/register/chains.go @@ -7,7 +7,7 @@ import ( itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" 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" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" 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" locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences" 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" scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions" 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" 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" - occurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants" - occurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry" - occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape" - occurrencerefs "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" + locationoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants" + locationoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry" + locationoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape" + locationoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs" + 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" 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" 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" - interactionregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/registry" - interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" - interactionrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_refs" - interactionrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_relatedness" + npcoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/invariants" + npcoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/registry" + npcoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape" + npcoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/source_refs" + 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" 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" @@ -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{ Stage: pipeline.StageExtract, - Module: interactionextract.Key, + Module: occurrenceextract.Key, Validators: []pipeline.ModuleBinding{ pipeline.Binding(validjson.Key), - pipeline.Binding(interactionshape.Key), - pipeline.Binding(interactionregistry.Key), - pipeline.Binding(interactionrefs.Key), + pipeline.Binding(npcoccurrenceshape.Key), + pipeline.Binding(npcoccurrenceregistry.Key), + pipeline.Binding(npcoccurrencerefs.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{ Stage: pipeline.StageNormalize, - Module: interactionnormalize.Key, + Module: occurrencenormalize.Key, Validators: []pipeline.ModuleBinding{ pipeline.Binding(validjson.Key), - pipeline.Binding(interactionshape.Key), - pipeline.Binding(interactionregistry.Key), - pipeline.Binding(interactioninvariants.Key), - pipeline.Binding(interactionrefs.Key), + pipeline.Binding(npcoccurrenceshape.Key), + pipeline.Binding(npcoccurrenceregistry.Key), + pipeline.Binding(npcoccurrenceinvariants.Key), + pipeline.Binding(npcoccurrencerefs.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{ Stage: pipeline.StageExtract, Module: locationoccurrenceextract.Key, Validators: []pipeline.ModuleBinding{ - pipeline.Binding(validjson.Key), pipeline.Binding(occurrenceshape.Key), pipeline.Binding(occurrenceregistry.Key), - pipeline.Binding(occurrencerefs.Key), pipeline.Binding(validjsonschema.Key), pipeline.Binding(occurrencerelatedness.Key), + pipeline.Binding(validjson.Key), pipeline.Binding(locationoccurrenceshape.Key), pipeline.Binding(locationoccurrenceregistry.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{ Stage: pipeline.StageNormalize, Module: locationoccurrencenormalize.Key, Validators: []pipeline.ModuleBinding{ - pipeline.Binding(validjson.Key), pipeline.Binding(occurrenceshape.Key), pipeline.Binding(occurrenceregistry.Key), - pipeline.Binding(occurrenceinvariants.Key), pipeline.Binding(occurrencerefs.Key), pipeline.Binding(validjsonschema.Key), - pipeline.Binding(occurrencerelatedness.Key), + pipeline.Binding(validjson.Key), pipeline.Binding(locationoccurrenceshape.Key), pipeline.Binding(locationoccurrenceregistry.Key), + pipeline.Binding(locationoccurrenceinvariants.Key), pipeline.Binding(locationoccurrencerefs.Key), pipeline.Binding(validjsonschema.Key), + pipeline.Binding(locationoccurrencerelatedness.Key), }, }) }}, diff --git a/internal/modules/dnd/register/evidence.go b/internal/modules/dnd/register/evidence.go index 33455c3..ea23227 100644 --- a/internal/modules/dnd/register/evidence.go +++ b/internal/modules/dnd/register/evidence.go @@ -19,8 +19,8 @@ func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error { {name: "item events evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.ItemEventListKind, itemEventEvidence) }}, - {name: "npc interactions evidence", register: func() error { - return pipeline.RegisterArtifactEvidence(registry, dnd.NPCOccurrenceListKind, npcInteractionEvidence) + {name: "npc occurrences evidence", register: func() error { + return pipeline.RegisterArtifactEvidence(registry, dnd.NPCOccurrenceListKind, npcOccurrenceEvidence) }}, {name: "scene descriptions evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence) @@ -74,7 +74,7 @@ func itemEventEvidence(value dnd.ItemEventList) []source.SourceRef { return append([]source.SourceRef(nil), refs...) } -func npcInteractionEvidence(value dnd.NPCOccurrenceList) []source.SourceRef { +func npcOccurrenceEvidence(value dnd.NPCOccurrenceList) []source.SourceRef { var refs []source.SourceRef for _, record := range value.Occurrences { refs = append(refs, record.SourceRefs...) diff --git a/internal/modules/dnd/register/merge.go b/internal/modules/dnd/register/merge.go index 78d9e1d..65b5546 100644 --- a/internal/modules/dnd/register/merge.go +++ b/internal/modules/dnd/register/merge.go @@ -112,7 +112,7 @@ func appendItemEventLists(values []dnd.ItemEventList) (dnd.ItemEventList, error) return combined, nil } -func appendNPCInteractionLists(values []dnd.NPCOccurrenceList) (dnd.NPCOccurrenceList, error) { +func appendNPCOccurrenceLists(values []dnd.NPCOccurrenceList) (dnd.NPCOccurrenceList, error) { count := 0 present := false for _, value := range values { diff --git a/internal/modules/dnd/register/modules.go b/internal/modules/dnd/register/modules.go index 2b2a1bf..8bee2fe 100644 --- a/internal/modules/dnd/register/modules.go +++ b/internal/modules/dnd/register/modules.go @@ -10,7 +10,7 @@ import ( itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents" locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences" 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" scenedescriptioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions" 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" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" 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" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" 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" locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences" 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" scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions" 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: "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: "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 { 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: "enemy events extractor", register: func() error { return enemyeventextract.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: "locations extractor", register: func() error { return locationextract.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 { return appendorder.RegisterTyped(registries.Mergers, dnd.ItemEventListKind, appendItemEventLists) }}, - {name: "npc-interaction-list appendorder merger", register: func() error { - return appendorder.RegisterTyped(registries.Mergers, dnd.NPCOccurrenceListKind, appendNPCInteractionLists) + {name: "npc-occurrence-list appendorder merger", register: func() error { + return appendorder.RegisterTyped(registries.Mergers, dnd.NPCOccurrenceListKind, appendNPCOccurrenceLists) }}, {name: "scene-description-list appendorder merger", register: func() error { 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: "enemy events normalizer", register: func() error { return enemyeventnormalize.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: "locations normalizer", register: func() error { return locationnormalize.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 { 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) }}, {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: "enemy events prompt assets", register: func() error { return enemyeventextract.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: "locations prompt assets", register: func() error { return locationextract.RegisterPromptAssets(assets) }}, {name: "location normalization prompt assets", register: func() error { return locationnormalize.RegisterPromptAssets(assets) }}, diff --git a/internal/modules/dnd/register/prompt_cache_test.go b/internal/modules/dnd/register/prompt_cache_test.go index 5bcc1fa..bc79f71 100644 --- a/internal/modules/dnd/register/prompt_cache_test.go +++ b/internal/modules/dnd/register/prompt_cache_test.go @@ -13,7 +13,7 @@ import ( itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" 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" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" @@ -66,20 +66,20 @@ func TestExtractionPromptComposition(t *testing.T) { promptID: enemyeventextract.PromptID, promptVersion: enemyeventextract.SchemaVersion, inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ - "npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), - "combat_turns": promptkit.Inline(`{"sentinel":"combat-turns-sentinel"}`), - "npc_interactions": promptkit.Inline(`{"sentinel":"npc-interactions-sentinel"}`), + "npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), + "combat_turns": promptkit.Inline(`{"sentinel":"combat-turns-sentinel"}`), + "npc_occurrences": promptkit.Inline(`{"sentinel":"npc-occurrences-sentinel"}`), }), suffixGroups: [][]string{ {evidenceSentinel}, {npcSentinel}, - {"combat-turns-sentinel", "npc-interactions-sentinel"}, + {"combat-turns-sentinel", "npc-occurrences-sentinel"}, }, }, { - name: "npc interactions", - promptID: interactionextract.PromptID, - promptVersion: interactionextract.SchemaVersion, + name: "npc occurrences", + promptID: occurrenceextract.PromptID, + promptVersion: occurrenceextract.SchemaVersion, inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ "npc_registry": promptkit.Inline(`{"sentinel":"` + npcSentinel + `"}`), }), diff --git a/internal/modules/dnd/register/register_test.go b/internal/modules/dnd/register/register_test.go index 3dab85b..cbbd864 100644 --- a/internal/modules/dnd/register/register_test.go +++ b/internal/modules/dnd/register/register_test.go @@ -17,7 +17,7 @@ import ( itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" 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" scenedescriptionextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions" "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" locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences" 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" scenedescriptionnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/scenedescriptions" 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.enemy_events/prompt.yaml", "dnd.item_events/prompt.yaml", - "dnd.npc_interactions/prompt.yaml", + "dnd.npc_occurrences/prompt.yaml", "dnd.scene_descriptions/prompt.yaml", "dnd.npc_registry.normalize/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) } 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, "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, "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, 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.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}) @@ -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(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind}) 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}) 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", @@ -118,11 +118,11 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "extract/dnd/item-events/source_refs", "extract/dnd/item-events/source_relatedness", "normalize/dnd/item-events/invariants", - "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", + "extract/dnd/npc-occurrences/shape", + "extract/dnd/npc-occurrences/registry", + "extract/dnd/npc-occurrences/source_refs", + "extract/dnd/npc-occurrences/source_relatedness", + "normalize/dnd/npc-occurrences/invariants", "extract/dnd/scene-descriptions/shape", "extract/dnd/scene-descriptions/source_refs", "extract/dnd/scene-descriptions/source_relatedness", @@ -245,28 +245,28 @@ func TestRegisterAddsDNDFamily(t *testing.T) { 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) } - interactionExtractChain := []pipeline.ModuleBinding{ + npcOccurrenceExtractChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), - pipeline.Binding("extract/dnd/npc-interactions/shape"), - pipeline.Binding("extract/dnd/npc-interactions/registry"), - pipeline.Binding("extract/dnd/npc-interactions/source_refs"), + pipeline.Binding("extract/dnd/npc-occurrences/shape"), + pipeline.Binding("extract/dnd/npc-occurrences/registry"), + pipeline.Binding("extract/dnd/npc-occurrences/source_refs"), 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("extract/dnd/npc-interactions/shape"), - pipeline.Binding("extract/dnd/npc-interactions/registry"), - pipeline.Binding("normalize/dnd/npc-interactions/invariants"), - pipeline.Binding("extract/dnd/npc-interactions/source_refs"), + pipeline.Binding("extract/dnd/npc-occurrences/shape"), + pipeline.Binding("extract/dnd/npc-occurrences/registry"), + pipeline.Binding("normalize/dnd/npc-occurrences/invariants"), + pipeline.Binding("extract/dnd/npc-occurrences/source_refs"), 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) { - t.Fatalf("NPC interaction extract validator chain = %#v, want %#v", got, interactionExtractChain) + if got := registries.ValidatorChains.Validators(pipeline.StageExtract, occurrenceextract.Key); !reflect.DeepEqual(got, npcOccurrenceExtractChain) { + 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) { - t.Fatalf("NPC interaction normalize validator chain = %#v, want %#v", got, interactionNormalizeChain) + if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, occurrencenormalize.Key); !reflect.DeepEqual(got, npcOccurrenceNormalizeChain) { + t.Fatalf("NPC occurrence normalize validator chain = %#v, want %#v", got, npcOccurrenceNormalizeChain) } sceneExtractChain := []pipeline.ModuleBinding{ 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 { t.Fatalf("NPC merge validator chain = %#v, want absent", got) } - if got := registries.ValidatorChains.Validators(pipeline.StageMerge, interactionextract.Key); got != nil { - t.Fatalf("NPC interaction merge validator chain = %#v, want absent", got) + if got := registries.ValidatorChains.Validators(pipeline.StageMerge, occurrenceextract.Key); got != nil { + t.Fatalf("NPC occurrence merge validator chain = %#v, want absent", got) } assertAssetNamesContain(t, assets.SchemaFS, []string{ "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) } } - interactionExtractSpec, extractOK := registries.Extractors.Spec(interactionextract.Key) - interactionNormalizeSpec, normalizeOK := registries.Normalizers.Spec(interactionnormalize.Key) - if !extractOK || interactionExtractSpec.ArtifactKind != dnd.NPCOccurrenceListKind || !normalizeOK || interactionNormalizeSpec.ArtifactKind != dnd.NPCOccurrenceListKind || interactionNormalizeSpec.Stage != pipeline.StageNormalize { - t.Fatalf("NPC interaction specs = %#v / %#v, present = %t / %t", interactionExtractSpec, interactionNormalizeSpec, extractOK, normalizeOK) + occurrenceExtractSpec, extractOK := registries.Extractors.Spec(occurrenceextract.Key) + occurrenceNormalizeSpec, normalizeOK := registries.Normalizers.Spec(occurrencenormalize.Key) + if !extractOK || occurrenceExtractSpec.ArtifactKind != dnd.NPCOccurrenceListKind || !normalizeOK || occurrenceNormalizeSpec.ArtifactKind != dnd.NPCOccurrenceListKind || occurrenceNormalizeSpec.Stage != pipeline.StageNormalize { + t.Fatalf("NPC occurrence specs = %#v / %#v, present = %t / %t", occurrenceExtractSpec, occurrenceNormalizeSpec, extractOK, normalizeOK) } locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.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 { t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec) } - occurrenceExtractSpec, occurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key) - occurrenceNormalizeSpec, occurrenceNormalizeOK := registries.Normalizers.Spec(locationoccurrencenormalize.Key) - if !occurrenceExtractOK || occurrenceExtractSpec.ArtifactKind != dnd.LocationOccurrenceListKind || occurrenceExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !occurrenceNormalizeOK || occurrenceNormalizeSpec.ArtifactKind != dnd.LocationOccurrenceListKind || occurrenceNormalizeSpec.ExecutionClass != contracts.ExecutionClassDeterministic { - t.Fatalf("location occurrence specs = %#v / %#v", occurrenceExtractSpec, occurrenceNormalizeSpec) + locationOccurrenceExtractSpec, locationOccurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key) + locationOccurrenceNormalizeSpec, locationOccurrenceNormalizeOK := registries.Normalizers.Spec(locationoccurrencenormalize.Key) + 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", locationOccurrenceExtractSpec, locationOccurrenceNormalizeSpec) } - locationRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "locations") - occurrenceNormalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "locations") - if len(occurrenceExtractSpec.ReferenceSlots) != 5 || len(occurrenceNormalizeSpec.ReferenceSlots) != 1 { - t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots) + locationRegistrySlot := referenceSlot(locationOccurrenceExtractSpec.ReferenceSlots, "locations") + occurrenceNormalizeRegistrySlot := referenceSlot(locationOccurrenceNormalizeSpec.ReferenceSlots, "locations") + 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", 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) { t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots) } 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 { 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 { t.Fatalf("scene description reference slots = %#v / %#v, want extractor campaign slots only", sceneExtractSpec.ReferenceSlots, sceneNormalizeSpec.ReferenceSlots) } - extractRegistrySlot := referenceSlot(interactionExtractSpec.ReferenceSlots, "npc_registry") - normalizeRegistrySlot := referenceSlot(interactionNormalizeSpec.ReferenceSlots, "npc_registry") + extractRegistrySlot := referenceSlot(occurrenceExtractSpec.ReferenceSlots, "npc_registry") + normalizeRegistrySlot := referenceSlot(occurrenceNormalizeSpec.ReferenceSlots, "npc_registry") 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 { return itemEventEvidence(dnd.ItemEventList{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{first, second}}}}) }, want: []source.SourceRef{first, second}}, - {name: "npc interactions", project: func() []source.SourceRef { - return npcInteractionEvidence(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{first, second}}}}) + {name: "npc occurrences", project: func() []source.SourceRef { + return npcOccurrenceEvidence(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{first, second}}}}) }, want: []source.SourceRef{first, second}}, {name: "scene descriptions", project: func() []source.SourceRef { 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}} 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: "Borin", Kind: dnd.NPCOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 20, EndUnitID: 20}}}}}, } - got, err := appendNPCInteractionLists(input) + got, err := appendNPCOccurrenceLists(input) 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"}) { - t.Fatalf("combined interactions = %#v", got) + t.Fatalf("combined occurrences = %#v", got) } got.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{{}, {}}} { - result, err := appendNPCInteractionLists(values) + result, err := appendNPCOccurrenceLists(values) 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) } - interactions, err := appendNPCInteractionLists([]dnd.NPCOccurrenceList{{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{}}}}}) - if err != nil || interactions.Occurrences[0].SourceRefs == nil { - t.Fatalf("appendNPCInteractionLists() = %#v, %v; want present-empty source refs", interactions, err) + occurrences, err := appendNPCOccurrenceLists([]dnd.NPCOccurrenceList{{Occurrences: []dnd.NPCOccurrence{{SourceRefs: []source.SourceRef{}}}}}) + if err != nil || occurrences.Occurrences[0].SourceRefs == nil { + t.Fatalf("appendNPCOccurrenceLists() = %#v, %v; want present-empty source refs", occurrences, err) } events, err := appendItemEventLists([]dnd.ItemEventList{{Events: []dnd.ItemEvent{{SourceRefs: []source.SourceRef{}}}}}) diff --git a/internal/modules/dnd/register/validators.go b/internal/modules/dnd/register/validators.go index f6468f7..9ac5f37 100644 --- a/internal/modules/dnd/register/validators.go +++ b/internal/modules/dnd/register/validators.go @@ -16,20 +16,20 @@ import ( 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" 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" - occurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry" - occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape" - occurrencerefs "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" + locationoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/invariants" + locationoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/registry" + locationoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/shape" + locationoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/locationoccurrences/source_refs" + 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" 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" 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" - interactionregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/registry" - interactionshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/shape" - interactionrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_refs" - interactionrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcinteractions/source_relatedness" + npcoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/invariants" + npcoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/registry" + npcoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/shape" + npcoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/source_refs" + 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" 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" @@ -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 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: "npc interaction shape validator", register: func() error { return interactionshape.Register(registries.Validators) }}, - {name: "npc interaction registry validator", register: func() error { return interactionregistry.Register(registries.Validators) }}, - {name: "npc interaction source references validator", register: func() error { return interactionrefs.Register(registries.Validators) }}, - {name: "npc interaction source relatedness validator", register: func() error { return interactionrelatedness.Register(registries.Validators) }}, - {name: "npc interaction normalized invariants validator", register: func() error { return interactioninvariants.Register(registries.Validators) }}, + {name: "npc occurrence shape validator", register: func() error { return npcoccurrenceshape.Register(registries.Validators) }}, + {name: "npc occurrence registry validator", register: func() error { return npcoccurrenceregistry.Register(registries.Validators) }}, + {name: "npc occurrence source references validator", register: func() error { return npcoccurrencerefs.Register(registries.Validators) }}, + {name: "npc occurrence source relatedness validator", register: func() error { return npcoccurrencerelatedness.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 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) }}, @@ -82,11 +82,11 @@ func registerValidators(registries pipeline.Registries) error { {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 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 registry validator", register: func() error { return occurrenceregistry.Register(registries.Validators) }}, - {name: "location occurrence normalized invariants validator", register: func() error { return occurrenceinvariants.Register(registries.Validators) }}, - {name: "location occurrence source references validator", register: func() error { return occurrencerefs.Register(registries.Validators) }}, - {name: "location occurrence source relatedness validator", register: func() error { return occurrencerelatedness.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 locationoccurrenceregistry.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 locationoccurrencerefs.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 { 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 { 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) }}, - {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) }}, {name: "scene-description-list always accept validator", register: func() error { diff --git a/internal/modules/dnd/validate/npcinteractions/invariants/validator.go b/internal/modules/dnd/validate/npcoccurrences/invariants/validator.go similarity index 88% rename from internal/modules/dnd/validate/npcinteractions/invariants/validator.go rename to internal/modules/dnd/validate/npcoccurrences/invariants/validator.go index a012555..440e9c1 100644 --- a/internal/modules/dnd/validate/npcinteractions/invariants/validator.go +++ b/internal/modules/dnd/validate/npcoccurrences/invariants/validator.go @@ -1,4 +1,4 @@ -// Package invariants validates normalized D&D NPC interaction artifacts. +// Package invariants validates normalized D&D NPC occurrence artifacts. package invariants import ( @@ -10,17 +10,17 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "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" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "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 ( - Key = "normalize/dnd/npc-interactions/invariants" + Key = "normalize/dnd/npc-occurrences/invariants" ReasonCode = "invalid_npc_interaction_normalization" - policy = "dnd.npc_interactions.validator.normalized.v1" + policy = "dnd.npc_occurrences.validator.normalized.v1" ) type Options struct{} @@ -35,7 +35,7 @@ var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) { 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 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) { - if interactionshape.Validate(req.Value) != nil { + if occurrenceshape.Validate(req.Value) != nil { return contracts.ValidationResult{Approved: true}, nil } index := source.NewDocumentIndex(req.Source) @@ -85,14 +85,14 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq return contracts.ValidationResult{Approved: true}, 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) if err != nil { return contracts.ValidationResult{}, fmt.Errorf("resolve NPC registry: %w", err) } 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) if len(issues) == 0 { @@ -101,7 +101,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq return contracts.ValidationResult{ Approved: false, ReasonCode: ReasonCode, - Message: diagnostics.Aggregate("invalid NPC interaction normalization", issues), + Message: diagnostics.Aggregate("invalid NPC occurrence normalization", issues), }, nil } diff --git a/internal/modules/dnd/validate/npcinteractions/invariants/validator_test.go b/internal/modules/dnd/validate/npcoccurrences/invariants/validator_test.go similarity index 97% rename from internal/modules/dnd/validate/npcinteractions/invariants/validator_test.go rename to internal/modules/dnd/validate/npcoccurrences/invariants/validator_test.go index b632c26..10b0ebd 100644 --- a/internal/modules/dnd/validate/npcinteractions/invariants/validator_test.go +++ b/internal/modules/dnd/validate/npcoccurrences/invariants/validator_test.go @@ -16,7 +16,7 @@ import ( 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") result, err := newValidator(t, references).Validate(context.Background(), request(references, normalizedList())) if err != nil || !result.Approved { @@ -143,9 +143,9 @@ func normalizedList() dnd.NPCOccurrenceList { func cloneList(value dnd.NPCOccurrenceList) dnd.NPCOccurrenceList { copyValue := dnd.NPCOccurrenceList{Occurrences: make([]dnd.NPCOccurrence, len(value.Occurrences))} - for index, interaction := range value.Occurrences { - copyValue.Occurrences[index] = interaction - copyValue.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), interaction.SourceRefs...) + for index, occurrence := range value.Occurrences { + copyValue.Occurrences[index] = occurrence + copyValue.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), occurrence.SourceRefs...) } return copyValue } diff --git a/internal/modules/dnd/validate/npcinteractions/registry/validator.go b/internal/modules/dnd/validate/npcoccurrences/registry/validator.go similarity index 86% rename from internal/modules/dnd/validate/npcinteractions/registry/validator.go rename to internal/modules/dnd/validate/npcoccurrences/registry/validator.go index 9d17e31..5047aba 100644 --- a/internal/modules/dnd/validate/npcinteractions/registry/validator.go +++ b/internal/modules/dnd/validate/npcoccurrences/registry/validator.go @@ -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 import ( @@ -10,13 +10,13 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" "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 ( - Key = "extract/dnd/npc-interactions/registry" + Key = "extract/dnd/npc-occurrences/registry" ReasonCode = "invalid_npc_interaction_registry" - policy = "dnd.npc_interactions.validator.registry.v1" + policy = "dnd.npc_occurrences.validator.registry.v1" ) type Options struct{} @@ -31,7 +31,7 @@ var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) { 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 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) { - if interactionshape.Validate(req.Value) != nil { + if occurrenceshape.Validate(req.Value) != nil { return contracts.ValidationResult{Approved: true}, 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) if err != nil { @@ -107,7 +107,7 @@ func rejection(issues []string) contracts.ValidationResult { return contracts.ValidationResult{ Approved: false, ReasonCode: ReasonCode, - Message: diagnostics.Aggregate("invalid NPC interaction registry", issues), + Message: diagnostics.Aggregate("invalid NPC occurrence registry", issues), } } diff --git a/internal/modules/dnd/validate/npcinteractions/registry/validator_test.go b/internal/modules/dnd/validate/npcoccurrences/registry/validator_test.go similarity index 100% rename from internal/modules/dnd/validate/npcinteractions/registry/validator_test.go rename to internal/modules/dnd/validate/npcoccurrences/registry/validator_test.go diff --git a/internal/modules/dnd/validate/npcinteractions/shape/validator.go b/internal/modules/dnd/validate/npcoccurrences/shape/validator.go similarity index 92% rename from internal/modules/dnd/validate/npcinteractions/shape/validator.go rename to internal/modules/dnd/validate/npcoccurrences/shape/validator.go index ef685bf..4e2137a 100644 --- a/internal/modules/dnd/validate/npcinteractions/shape/validator.go +++ b/internal/modules/dnd/validate/npcoccurrences/shape/validator.go @@ -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 import ( @@ -13,9 +13,9 @@ import ( ) const ( - Key = "extract/dnd/npc-interactions/shape" + Key = "extract/dnd/npc-occurrences/shape" ReasonCode = "invalid_npc_interaction_shape" - policy = "dnd.npc_interactions.validator.shape.v1" + policy = "dnd.npc_occurrences.validator.shape.v1" ) type Options struct{} @@ -45,7 +45,7 @@ func Validate(value dnd.NPCOccurrenceList) error { if len(issues) == 0 { 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 { diff --git a/internal/modules/dnd/validate/npcinteractions/shape/validator_test.go b/internal/modules/dnd/validate/npcoccurrences/shape/validator_test.go similarity index 100% rename from internal/modules/dnd/validate/npcinteractions/shape/validator_test.go rename to internal/modules/dnd/validate/npcoccurrences/shape/validator_test.go diff --git a/internal/modules/dnd/validate/npcinteractions/source_refs/validator.go b/internal/modules/dnd/validate/npcoccurrences/source_refs/validator.go similarity index 85% rename from internal/modules/dnd/validate/npcinteractions/source_refs/validator.go rename to internal/modules/dnd/validate/npcoccurrences/source_refs/validator.go index d74d342..489937f 100644 --- a/internal/modules/dnd/validate/npcinteractions/source_refs/validator.go +++ b/internal/modules/dnd/validate/npcoccurrences/source_refs/validator.go @@ -1,4 +1,4 @@ -// Package sourcerefs validates D&D NPC interaction transcript evidence. +// Package sourcerefs validates D&D NPC occurrence transcript evidence. package sourcerefs import ( @@ -10,13 +10,13 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/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 ( - Key = "extract/dnd/npc-interactions/source_refs" + Key = "extract/dnd/npc-occurrences/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{} @@ -36,9 +36,9 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCOccurrenceList]) (contracts.ValidationResult, error) { 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 } index := source.NewDocumentIndex(req.Source) @@ -63,7 +63,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq return contracts.ValidationResult{ Approved: false, ReasonCode: ReasonCode, - Message: diagnostics.Aggregate("invalid NPC interaction source references", issues), + Message: diagnostics.Aggregate("invalid NPC occurrence source references", issues), }, nil } diff --git a/internal/modules/dnd/validate/npcinteractions/source_refs/validator_test.go b/internal/modules/dnd/validate/npcoccurrences/source_refs/validator_test.go similarity index 100% rename from internal/modules/dnd/validate/npcinteractions/source_refs/validator_test.go rename to internal/modules/dnd/validate/npcoccurrences/source_refs/validator_test.go diff --git a/internal/modules/dnd/validate/npcinteractions/source_relatedness/validator.go b/internal/modules/dnd/validate/npcoccurrences/source_relatedness/validator.go similarity index 82% rename from internal/modules/dnd/validate/npcinteractions/source_relatedness/validator.go rename to internal/modules/dnd/validate/npcoccurrences/source_relatedness/validator.go index cdaf7cf..2b7c491 100644 --- a/internal/modules/dnd/validate/npcinteractions/source_relatedness/validator.go +++ b/internal/modules/dnd/validate/npcoccurrences/source_relatedness/validator.go @@ -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 import ( @@ -10,14 +10,14 @@ import ( "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/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 ( - Key = "extract/dnd/npc-interactions/source_relatedness" + Key = "extract/dnd/npc-occurrences/source_relatedness" WarningReasonCode = "npc_interaction_not_near_source" 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{} @@ -36,7 +36,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { } 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 } resolver, err := shared.NewCitationResolver(req.Source) @@ -59,12 +59,12 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq warnings = append(warnings, contracts.Warning{ Scope: fmt.Sprintf("occurrences[%d]", index), 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{ Approved: true, - Warnings: diagnostics.LimitWarnings(warnings, "npc_interactions", OmittedReasonCode), + Warnings: diagnostics.LimitWarnings(warnings, "npc_occurrences", OmittedReasonCode), }, nil } diff --git a/internal/modules/dnd/validate/npcinteractions/source_relatedness/validator_test.go b/internal/modules/dnd/validate/npcoccurrences/source_relatedness/validator_test.go similarity index 93% rename from internal/modules/dnd/validate/npcinteractions/source_relatedness/validator_test.go rename to internal/modules/dnd/validate/npcoccurrences/source_relatedness/validator_test.go index df26895..4f34d6b 100644 --- a/internal/modules/dnd/validate/npcinteractions/source_relatedness/validator_test.go +++ b/internal/modules/dnd/validate/npcoccurrences/source_relatedness/validator_test.go @@ -13,7 +13,7 @@ import ( "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{ {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}}}, @@ -53,9 +53,9 @@ func TestValidatorDefersMalformedShapeAndInvalidRanges(t *testing.T) { func TestValidatorBoundsWarnings(t *testing.T) { count := diagnostics.MaxWarnings + 5 - interactions := make([]dnd.NPCOccurrence, count) - for index := range interactions { - interactions[index] = dnd.NPCOccurrence{ + occurrences := make([]dnd.NPCOccurrence, count) + for index := range occurrences { + occurrences[index] = dnd.NPCOccurrence{ NPCID: "npc:test", Name: "Missing NPC", Kind: dnd.NPCOccurrenceKindMentioned, @@ -64,7 +64,7 @@ func TestValidatorBoundsWarnings(t *testing.T) { } 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."}}}, - Value: dnd.NPCOccurrenceList{Occurrences: interactions}, + Value: dnd.NPCOccurrenceList{Occurrences: occurrences}, }) if err != nil || !result.Approved { t.Fatalf("Validate() = %#v, %v", result, err) diff --git a/internal/modules/integration/dnd_npc_interactions_runner_test.go b/internal/modules/integration/dnd_npc_occurrences_runner_test.go similarity index 68% rename from internal/modules/integration/dnd_npc_interactions_runner_test.go rename to internal/modules/integration/dnd_npc_occurrences_runner_test.go index d16ff91..c5cf206 100644 --- a/internal/modules/integration/dnd_npc_interactions_runner_test.go +++ b/internal/modules/integration/dnd_npc_occurrences_runner_test.go @@ -14,73 +14,73 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "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" - 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" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" ) -func TestNPCInteractionPipelineUsesAcceptedRegistryAndCurrentEvidence(t *testing.T) { +func TestNPCOccurrencePipelineUsesAcceptedRegistryAndCurrentEvidence(t *testing.T) { registries := productionNPCRegistries(t) - resolved := resolveNPCInteractionPipeline(t, registries) - client := &npcInteractionLLMClient{} + resolved := resolveNPCOccurrencePipeline(t, registries) + client := &npcOccurrenceLLMClient{} output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)}) if err != nil { t.Fatalf("Run() error = %v", err) } 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"}]}` 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 { - 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") - if serialized.Artifact.Schema.ID != interactioncodec.SchemaID || serialized.Artifact.Schema.Version != interactioncodec.SchemaVersion { - t.Fatalf("interaction artifact schema = %#v", serialized.Artifact.Schema) + serialized := normalizedLane(t, output, "occurrences") + if serialized.Artifact.Schema.ID != occurrencecodec.SchemaID || serialized.Artifact.Schema.Version != occurrencecodec.SchemaVersion { + 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 { - t.Fatalf("Decode(interaction output) error = %v", err) + t.Fatalf("Decode(occurrence output) error = %v", err) } - if len(interactions.Occurrences) != 2 { - t.Fatalf("interactions = %#v, want two occurrences", interactions) + if len(occurrences.Occurrences) != 2 { + 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" { - 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) - assertInteractionEvidence(t, second.SourceRefs) + assertOccurrenceEvidence(t, first.SourceRefs) + assertOccurrenceEvidence(t, second.SourceRefs) 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 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 { - 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) { registries := productionNPCRegistries(t) - cfg := loadNPCInteractionPipelineConfig(t) - profile := cfg.Pipelines["dnd-npc-interactions-fixture"] + cfg := loadNPCOccurrencePipelineConfig(t) + profile := cfg.Pipelines["dnd-npc-occurrences-fixture"] profile.Chunk = pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}} - cfg.Pipelines["dnd-npc-interactions-fixture"] = profile - effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-npc-interactions-fixture", Catalog: moduleCatalog(registries)}) + cfg.Pipelines["dnd-npc-occurrences-fixture"] = profile + effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-npc-occurrences-fixture", Catalog: moduleCatalog(registries)}) if err != nil { t.Fatal(err) } @@ -88,7 +88,7 @@ func TestSemanticNPCNormalizationCrossesOrderedRegistryHandoff(t *testing.T) { if err != nil || len(warnings) != 0 { 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."}]}`) output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: raw}) 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}}) { t.Fatalf("NPC evidence = %#v, want original extraction evidence union", refs) } - interactionOutput := normalizedLane(t, output, "interactions") - if interactionOutput.StepID != "extract-interactions" { - t.Fatalf("interaction output step = %q, want ordered downstream step", interactionOutput.StepID) + occurrenceOutput := normalizedLane(t, output, "occurrences") + if occurrenceOutput.StepID != "extract-occurrences" { + 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"}]}` { 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) - resolved := resolveNPCInteractionPipeline(t, registries) - client := &npcInteractionLLMClient{rejectNPCs: true} + resolved := resolveNPCOccurrencePipeline(t, registries) + client := &npcOccurrenceLLMClient{rejectNPCs: true} output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)}) if err == nil { @@ -139,15 +139,15 @@ func TestNPCInteractionPipelineSkipsConsumerWhenNPCProducerIsRejected(t *testing if client.requestCount(npcregistry.PromptID) != 1 { t.Fatalf("NPC requests = %d, want rejected producer", client.requestCount(npcregistry.PromptID)) } - if client.requestCount(interactionextract.PromptID) != 0 { - t.Fatalf("interaction requests = %d, want none after rejected producer", client.requestCount(interactionextract.PromptID)) + if client.requestCount(occurrenceextract.PromptID) != 0 { + 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() - configValue := loadNPCInteractionPipelineConfig(t) - effective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-npc-interactions-fixture", Catalog: moduleCatalog(registries)}) + configValue := loadNPCOccurrencePipelineConfig(t) + effective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-npc-occurrences-fixture", Catalog: moduleCatalog(registries)}) if err != nil { t.Fatalf("Resolve() error = %v", err) } @@ -158,11 +158,11 @@ func resolveNPCInteractionPipeline(t *testing.T, registries pipeline.Registries) return resolved } -func loadNPCInteractionPipelineConfig(t *testing.T) config.Config { +func loadNPCOccurrencePipelineConfig(t *testing.T) config.Config { 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 { - 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) if err != nil { @@ -186,25 +186,25 @@ func normalizedLane(t *testing.T, output pipeline.RunOutput, laneID string) cont return contracts.SerializedOutput{} } -func assertInteractionEvidence(t *testing.T, references []source.SourceRef) { +func assertOccurrenceEvidence(t *testing.T, references []source.SourceRef) { t.Helper() if len(references) == 0 { - t.Fatal("interaction has no current-source evidence") + t.Fatal("occurrence has no current-source evidence") } for _, reference := range references { 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 requests []contracts.StructuredCompletionRequest 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 { return contracts.StructuredCompletionResponse{}, err } @@ -227,25 +227,25 @@ func (client *npcInteractionLLMClient) CompleteStructured(ctx context.Context, r } case npcnormalize.PromptID: payload = map[string]any{"duplicate_groups": []any{}} - case interactionextract.PromptID: + case occurrenceextract.PromptID: 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("Mira Thorn"), "name": "Mira Thorn", "kind": "dialogue", "source_refs": []any{map[string]int{"start_unit_id": 2, "end_unit_id": 2}}}, }} 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) if err != nil { return contracts.StructuredCompletionResponse{}, err } 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() client.mu.Lock() defer client.mu.Unlock() @@ -258,7 +258,7 @@ func (client *npcInteractionLLMClient) requestFor(t *testing.T, promptID string) return contracts.StructuredCompletionRequest{} } -func (client *npcInteractionLLMClient) requestCount(promptID string) int { +func (client *npcOccurrenceLLMClient) requestCount(promptID string) int { client.mu.Lock() defer client.mu.Unlock() count := 0 @@ -270,14 +270,14 @@ func (client *npcInteractionLLMClient) requestCount(promptID string) int { return count } -var _ contracts.StructuredLLMClient = (*npcInteractionLLMClient)(nil) +var _ contracts.StructuredLLMClient = (*npcOccurrenceLLMClient)(nil) -type semanticNPCInteractionClient struct { +type semanticNPCOccurrenceClient struct { requests []contracts.StructuredCompletionRequest 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)) var payload any 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}}}}} case npcnormalize.PromptID: 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}}}}} default: 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 } -func (client *semanticNPCInteractionClient) requestCount(promptID string) int { +func (client *semanticNPCOccurrenceClient) requestCount(promptID string) int { count := 0 for _, request := range client.requests { if request.PromptID == promptID { @@ -315,7 +315,7 @@ func (client *semanticNPCInteractionClient) requestCount(promptID string) int { 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() for _, request := range client.requests { if request.PromptID == promptID { @@ -326,4 +326,4 @@ func (client *semanticNPCInteractionClient) requestFor(t *testing.T, promptID st return contracts.StructuredCompletionRequest{} } -var _ contracts.StructuredLLMClient = (*semanticNPCInteractionClient)(nil) +var _ contracts.StructuredLLMClient = (*semanticNPCOccurrenceClient)(nil) diff --git a/internal/modules/integration/testdata/dnd_npc_interactions_pipeline.yml b/internal/modules/integration/testdata/dnd_npc_occurrences_pipeline.yml similarity index 73% rename from internal/modules/integration/testdata/dnd_npc_interactions_pipeline.yml rename to internal/modules/integration/testdata/dnd_npc_occurrences_pipeline.yml index c228aaa..9d0e4eb 100644 --- a/internal/modules/integration/testdata/dnd_npc_interactions_pipeline.yml +++ b/internal/modules/integration/testdata/dnd_npc_occurrences_pipeline.yml @@ -8,7 +8,7 @@ cache: debug: directory: ./notarius-debug pipelines: - dnd-npc-interactions-fixture: + dnd-npc-occurrences-fixture: input: seriatim steps: - id: identify-npcs @@ -16,13 +16,13 @@ pipelines: npc_registry: extract: dnd/npc-registry normalize: dnd/npc-registry - - id: extract-interactions + - id: extract-occurrences references: npc_registry: artifact: step: identify-npcs lane: npc_registry artifacts: - interactions: - extract: dnd/npc-interactions - normalize: dnd/npc-interactions + occurrences: + extract: dnd/npc-occurrences + normalize: dnd/npc-occurrences