Move NPC occurrences to their canonical namespace

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

View File

@@ -4,7 +4,7 @@ Notarius is a Go CLI for turning source material into structured artifacts with
configured extraction pipelines. The implemented D&D workflow reads Seriatim
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

View File

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

View File

@@ -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:

View File

@@ -1,12 +1,12 @@
Extract Dungeons & Dragons NPC interaction occurrences from the supplied
Extract Dungeons & Dragons NPC occurrence occurrences from the supplied
transcript. Include an occurrence only when the transcript establishes one
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.

View File

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

View File

@@ -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 |

View File

@@ -63,7 +63,7 @@ chronology; and collapses only duplicates with the same actor identity, turn
kind, and complete valid evidence. It does not infer turns, initiative, or
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,

View File

@@ -66,7 +66,7 @@ are positive and the start may not follow the end.
| `incapacitated` | The subject is explicitly rendered unable to continue acting without being established as killed or captured. A missed turn is insufficient. |
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.

View File

@@ -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.

View File

@@ -1,7 +1,7 @@
# D&D NPC Interaction Artifact
# D&D NPC Occurrence Artifact
This contract defines the durable occurrence list produced by
`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).

View File

@@ -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

View File

@@ -72,7 +72,7 @@ Each `lanes/<safe-lane-id>.json` file is the codec-owned normalized JSON for
that lane. Consumers should use the index descriptors schema identity rather
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

View File

@@ -16,7 +16,7 @@ does not repeat their JSON shapes or schemas.
| NPC registry | [NPC registry artifacts](../integrations/dnd-npc-registry-artifacts.md) |
| 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 chunks 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. |

View File

@@ -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:

View File

@@ -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 {

View File

@@ -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)},
}},
},
}

View File

@@ -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 {

View File

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

View File

@@ -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"

View File

@@ -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,

View File

@@ -49,8 +49,8 @@ func TestExtractMapsEnemyEventsInSourceOrder(t *testing.T) {
if got := string(request.Inputs[CombatTurnReferenceSlot].Content); !strings.Contains(got, `"actor":"Ashfang"`) || strings.Contains(got, "source_ref") {
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)
}
}

View File

@@ -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

View File

@@ -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)}},
}}
}

View File

@@ -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"}]}`),
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package npcinteractions
package npcoccurrences
import (
"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
}

View File

@@ -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...)
}

View File

@@ -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

View File

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

View File

@@ -1,4 +1,4 @@
package npcinteractions
package npcoccurrences
import (
"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)
}
}

View File

@@ -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, ".")
}

View File

@@ -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)

View File

@@ -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"

View File

@@ -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}},

View File

@@ -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...)
}

View File

@@ -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}}
}

View File

@@ -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"

View File

@@ -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),
},
})
}},

View File

@@ -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...)

View File

@@ -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 {

View File

@@ -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) }},

View File

@@ -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 + `"}`),
}),

View File

@@ -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{}}}}})

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
// Package invariants validates normalized D&D NPC interaction artifacts.
// Package invariants validates normalized D&D NPC occurrence artifacts.
package invariants
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
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
// Package registry validates D&D NPC interaction names against NPC grounding.
// Package registry validates D&D NPC occurrence names against NPC grounding.
package registry
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),
}
}

View File

@@ -1,4 +1,4 @@
// Package shape validates required D&D NPC interaction candidate fields.
// Package shape validates required D&D NPC occurrence candidate fields.
package shape
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 {

View File

@@ -1,4 +1,4 @@
// Package sourcerefs validates D&D NPC interaction transcript evidence.
// Package sourcerefs validates D&D NPC occurrence transcript evidence.
package sourcerefs
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
}

View File

@@ -1,4 +1,4 @@
// Package sourcerelatedness warns about NPC interaction evidence unrelated to its NPC.
// Package sourcerelatedness warns about NPC occurrence evidence unrelated to its NPC.
package sourcerelatedness
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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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