From a6e176e1608b4ba2ed2674fb5d29a6e79ff23220 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 5 Aug 2026 20:00:20 +0000 Subject: [PATCH] Move item occurrences to canonical namespace --- .../prompts/instructions.md | 6 +- .../prompts/item-registry.md | 0 .../prompts/prompt.yaml | 2 +- .../schemas/dnd_item_occurrences_llm.v1.json | 0 docs/config.md | 12 ++-- ...ts.md => dnd-item-occurrence-artifacts.md} | 2 +- docs/integrations/json-output.md | 2 +- docs/internal/dnd.md | 6 +- examples/dnd-complete.config.yml | 8 +-- .../cli/dnd_enemy_events_contract_test.go | 4 +- internal/cli/example_contract_test.go | 10 +-- internal/cli/production_contract_test.go | 70 +++++++++---------- .../schemas/dnd_item_occurrences.v1.json | 0 .../{itemevents => itemoccurrences}/codec.go | 42 +++++------ .../codec_test.go | 8 +-- .../canonicalize.go | 30 ++++---- .../extractor.go | 10 +-- .../extractor_test.go | 2 +- .../{itemevents => itemoccurrences}/model.go | 2 +- .../prompt_assets.go | 10 +-- .../prompt_assets_test.go | 10 +-- .../registry_test.go | 2 +- .../{itemevents => itemoccurrences}/schema.go | 4 +- .../schema_test.go | 14 ++-- .../test_helpers_test.go | 4 +- .../itemoccurrences.go} | 30 ++++---- .../itemoccurrences_test.go} | 51 +++++++------- .../normalizer.go | 58 +++++++-------- .../normalizer_test.go | 2 +- internal/modules/dnd/register/chains.go | 36 +++++----- internal/modules/dnd/register/modules.go | 16 +++-- .../modules/dnd/register/prompt_cache_test.go | 4 +- .../modules/dnd/register/register_test.go | 46 ++++++------ internal/modules/dnd/register/validators.go | 20 +++--- .../invariants/validator.go | 46 ++++++------ .../invariants/validator_test.go | 2 +- .../registry/validator.go | 4 +- .../registry/validator_test.go | 0 .../shape/validator.go | 34 ++++----- .../shape/validator_test.go | 8 +-- .../source_refs/validator.go | 20 +++--- .../source_refs/validator_test.go | 0 .../source_relatedness/validator.go | 24 +++---- .../source_relatedness/validator_test.go | 8 +-- ....go => dnd_itemoccurrences_runner_test.go} | 6 +- ...=> seriatim_item_occurrences_session.json} | 4 +- 46 files changed, 342 insertions(+), 337 deletions(-) rename assets/dnd/{item-events => item-occurrences}/prompts/instructions.md (87%) rename assets/dnd/{item-events => item-occurrences}/prompts/item-registry.md (100%) rename assets/dnd/{item-events => item-occurrences}/prompts/prompt.yaml (97%) rename assets/dnd/{item-events => item-occurrences}/schemas/dnd_item_occurrences_llm.v1.json (100%) rename docs/integrations/{dnd-item-event-artifacts.md => dnd-item-occurrence-artifacts.md} (96%) rename internal/modules/dnd/codec/{itemevents => itemoccurrences}/assets/schemas/dnd_item_occurrences.v1.json (100%) rename internal/modules/dnd/codec/{itemevents => itemoccurrences}/codec.go (80%) rename internal/modules/dnd/codec/{itemevents => itemoccurrences}/codec_test.go (95%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/canonicalize.go (76%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/extractor.go (97%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/extractor_test.go (99%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/model.go (96%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/prompt_assets.go (87%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/prompt_assets_test.go (86%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/registry_test.go (98%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/schema.go (91%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/schema_test.go (84%) rename internal/modules/dnd/extract/{itemevents => itemoccurrences}/test_helpers_test.go (97%) rename internal/modules/dnd/{itemevents/itemevents.go => itemoccurrences/itemoccurrences.go} (88%) rename internal/modules/dnd/{itemevents/itemevents_test.go => itemoccurrences/itemoccurrences_test.go} (81%) rename internal/modules/dnd/normalize/{itemevents => itemoccurrences}/normalizer.go (82%) rename internal/modules/dnd/normalize/{itemevents => itemoccurrences}/normalizer_test.go (99%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/invariants/validator.go (70%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/invariants/validator_test.go (98%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/registry/validator.go (97%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/registry/validator_test.go (100%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/shape/validator.go (77%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/shape/validator_test.go (96%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/source_refs/validator.go (85%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/source_refs/validator_test.go (100%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/source_relatedness/validator.go (75%) rename internal/modules/dnd/validate/{itemevents => itemoccurrences}/source_relatedness/validator_test.go (91%) rename internal/modules/integration/{dnd_itemevents_runner_test.go => dnd_itemoccurrences_runner_test.go} (91%) rename internal/modules/integration/testdata/{seriatim_item_events_session.json => seriatim_item_occurrences_session.json} (83%) diff --git a/assets/dnd/item-events/prompts/instructions.md b/assets/dnd/item-occurrences/prompts/instructions.md similarity index 87% rename from assets/dnd/item-events/prompts/instructions.md rename to assets/dnd/item-occurrences/prompts/instructions.md index 85537a3..72d2a4a 100644 --- a/assets/dnd/item-events/prompts/instructions.md +++ b/assets/dnd/item-occurrences/prompts/instructions.md @@ -1,5 +1,5 @@ Extract meaningful Dungeons & Dragons item and currency occurrences: discoveries and changes -in party possession established by the transcript. This is an event history, +in party possession established by the transcript. This is an occurrence history, not an inventory or ledger: do not calculate balances, resolve item identity across records, or infer ownership that the transcript does not establish. @@ -18,13 +18,13 @@ when the transcript explicitly describes it being physically destroyed or expended as a non-payment component. Use `transferred` only when possession moves between two distinct named party members. -Return both `from` and `to` for every event, using `null` when a holder does not +Return both `from` and `to` for every occurrence, using `null` when a holder does not apply. For `discovered`, set both holders to `null`. For `acquired`, set `from` to `null` and provide `to`; for `lost` and `consumed`, provide `from` and set `to` to `null`; and for `transferred`, provide both holders. Use `party` only for collective or unresolved party possession, never for either side of a transfer. Do not emit a transfer for a gift, sale, or payment outside the party. -Ordinary non-depleting use is not an event. Do not infer acquisition from a +Ordinary non-depleting use is not an occurrence. Do not infer acquisition from a discovery, or discovery from an acquisition: emit both only when each is independently established. diff --git a/assets/dnd/item-events/prompts/item-registry.md b/assets/dnd/item-occurrences/prompts/item-registry.md similarity index 100% rename from assets/dnd/item-events/prompts/item-registry.md rename to assets/dnd/item-occurrences/prompts/item-registry.md diff --git a/assets/dnd/item-events/prompts/prompt.yaml b/assets/dnd/item-occurrences/prompts/prompt.yaml similarity index 97% rename from assets/dnd/item-events/prompts/prompt.yaml rename to assets/dnd/item-occurrences/prompts/prompt.yaml index e9ff044..a8fef9b 100644 --- a/assets/dnd/item-events/prompts/prompt.yaml +++ b/assets/dnd/item-occurrences/prompts/prompt.yaml @@ -1,4 +1,4 @@ -id: dnd.item_events +id: dnd.item_occurrences version: "v1" default_profile: dnd-extraction inputs: diff --git a/assets/dnd/item-events/schemas/dnd_item_occurrences_llm.v1.json b/assets/dnd/item-occurrences/schemas/dnd_item_occurrences_llm.v1.json similarity index 100% rename from assets/dnd/item-events/schemas/dnd_item_occurrences_llm.v1.json rename to assets/dnd/item-occurrences/schemas/dnd_item_occurrences_llm.v1.json diff --git a/docs/config.md b/docs/config.md index 57c11a3..03f4c68 100644 --- a/docs/config.md +++ b/docs/config.md @@ -404,15 +404,15 @@ generated bindings. | --- | --- | | Input | **seriatim** | | Chunk | **generic**, **dnd/scenes** | -| Extract | **dnd/spells**, **dnd/npc-registry**, **dnd/combat-turns**, **dnd/item-events**, **dnd/item-registry**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/location-registry**, **dnd/location-occurrences** | +| Extract | **dnd/spells**, **dnd/npc-registry**, **dnd/combat-turns**, **dnd/item-occurrences**, **dnd/item-registry**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/location-registry**, **dnd/location-occurrences** | | Merge | **appendorder** | -| Normalize | **noop**, **dnd/spells**, **dnd/npc-registry**, **dnd/combat-turns**, **dnd/item-events**, **dnd/item-registry**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/location-registry**, **dnd/location-occurrences** | +| Normalize | **noop**, **dnd/spells**, **dnd/npc-registry**, **dnd/combat-turns**, **dnd/item-occurrences**, **dnd/item-registry**, **dnd/npc-occurrences**, **dnd/scene-descriptions**, **dnd/enemy-events**, **dnd/location-registry**, **dnd/location-occurrences** | | Output | **json** | `dnd/location-registry` and `dnd/item-registry` extraction and normalization are `llm_backed`; their normalizers may use the pipeline's selected LLM profile for bounded duplicate proposals. `dnd/location-occurrences` and -`dnd/item-events` extraction are `llm_backed`, while their normalizers are +`dnd/item-occurrences` extraction are `llm_backed`, while their normalizers are `deterministic`. The complete example binds each registry in an earlier step before its occurrence consumer. @@ -421,7 +421,7 @@ The D&D artifact contracts define each emitted schema: [NPC registry](integrations/dnd-npc-registry-artifacts.md), [NPC occurrences](integrations/dnd-npc-occurrence-artifacts.md), [combat turns](integrations/dnd-combat-turn-artifacts.md), -[item events](integrations/dnd-item-event-artifacts.md), +[item occurrences](integrations/dnd-item-occurrence-artifacts.md), [item registry](integrations/dnd-item-registry-artifacts.md), [scene descriptions](integrations/dnd-scene-description-artifacts.md), and [enemy events](integrations/dnd-enemy-event-artifacts.md), @@ -438,7 +438,7 @@ Available validator keys are: | Spells | **extract/dnd/spells/shape**, **extract/dnd/spells/catalog**, **extract/dnd/spells/source_refs**, **extract/dnd/spells/source_relatedness** | | 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/registry**, **extract/dnd/item-events/source_refs**, **extract/dnd/item-events/source_relatedness**, **normalize/dnd/item-events/invariants** | +| Item occurrences | **extract/dnd/item-occurrences/shape**, **extract/dnd/item-occurrences/registry**, **extract/dnd/item-occurrences/source_refs**, **extract/dnd/item-occurrences/source_relatedness**, **normalize/dnd/item-occurrences/invariants** | | Item registry | **extract/dnd/item-registry/shape**, **extract/dnd/item-registry/source_refs**, **extract/dnd/item-registry/source_relatedness**, **normalize/dnd/item-registry/identity** | | NPC occurrences | **extract/dnd/npc-occurrences/shape**, **extract/dnd/npc-occurrences/registry**, **extract/dnd/npc-occurrences/source_refs**, **extract/dnd/npc-occurrences/source_relatedness**, **normalize/dnd/npc-occurrences/invariants** | | Scene descriptions | **extract/dnd/scene-descriptions/shape**, **extract/dnd/scene-descriptions/source_refs**, **extract/dnd/scene-descriptions/source_relatedness**, **normalize/dnd/scene-descriptions/invariants** | @@ -455,7 +455,7 @@ same at both stages. | Spells | generic/valid_json, extract/dnd/spells/shape, extract/dnd/spells/catalog, extract/dnd/spells/source_refs, generic/valid_json_schema, extract/dnd/spells/source_relatedness | Same as extract | | 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/registry, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | generic/valid_json, extract/dnd/item-events/shape, extract/dnd/item-events/registry, normalize/dnd/item-events/invariants, extract/dnd/item-events/source_refs, generic/valid_json_schema, extract/dnd/item-events/source_relatedness | +| Item occurrences | generic/valid_json, extract/dnd/item-occurrences/shape, extract/dnd/item-occurrences/registry, extract/dnd/item-occurrences/source_refs, generic/valid_json_schema, extract/dnd/item-occurrences/source_relatedness | generic/valid_json, extract/dnd/item-occurrences/shape, extract/dnd/item-occurrences/registry, normalize/dnd/item-occurrences/invariants, extract/dnd/item-occurrences/source_refs, generic/valid_json_schema, extract/dnd/item-occurrences/source_relatedness | | Item registry | generic/valid_json, extract/dnd/item-registry/shape, extract/dnd/item-registry/source_refs, generic/valid_json_schema, extract/dnd/item-registry/source_relatedness | generic/valid_json, extract/dnd/item-registry/shape, normalize/dnd/item-registry/identity, extract/dnd/item-registry/source_refs, generic/valid_json_schema, extract/dnd/item-registry/source_relatedness | | NPC occurrences | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness | generic/valid_json, extract/dnd/npc-occurrences/shape, extract/dnd/npc-occurrences/registry, normalize/dnd/npc-occurrences/invariants, extract/dnd/npc-occurrences/source_refs, generic/valid_json_schema, extract/dnd/npc-occurrences/source_relatedness | | Scene descriptions | generic/valid_json, extract/dnd/scene-descriptions/shape, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | generic/valid_json, extract/dnd/scene-descriptions/shape, normalize/dnd/scene-descriptions/invariants, extract/dnd/scene-descriptions/source_refs, generic/valid_json_schema, extract/dnd/scene-descriptions/source_relatedness | diff --git a/docs/integrations/dnd-item-event-artifacts.md b/docs/integrations/dnd-item-occurrence-artifacts.md similarity index 96% rename from docs/integrations/dnd-item-event-artifacts.md rename to docs/integrations/dnd-item-occurrence-artifacts.md index 5993a73..d538a15 100644 --- a/docs/integrations/dnd-item-event-artifacts.md +++ b/docs/integrations/dnd-item-occurrence-artifacts.md @@ -1,6 +1,6 @@ # D&D Item-Occurrence Artifact -`dnd/item-events` currently produces this source-grounded item and currency +`dnd/item-occurrences` currently produces this source-grounded item and currency occurrence list. It records discoveries and possession changes, not an inventory, balance, or ledger. diff --git a/docs/integrations/json-output.md b/docs/integrations/json-output.md index 956ee78..899dfa9 100644 --- a/docs/integrations/json-output.md +++ b/docs/integrations/json-output.md @@ -74,7 +74,7 @@ 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 occurrences](dnd-npc-occurrence-artifacts.md), [combat turns](dnd-combat-turn-artifacts.md), -[item events](dnd-item-event-artifacts.md), +[item occurrences](dnd-item-occurrence-artifacts.md), [scene descriptions](dnd-scene-description-artifacts.md), and [enemy events](dnd-enemy-event-artifacts.md), [location registry](dnd-location-registry-artifacts.md), and diff --git a/docs/internal/dnd.md b/docs/internal/dnd.md index 2008e73..62f8f99 100644 --- a/docs/internal/dnd.md +++ b/docs/internal/dnd.md @@ -15,7 +15,7 @@ does not repeat their JSON shapes or schemas. | Spells | [spell artifacts](../integrations/dnd-spell-artifacts.md) | | NPC registry | [NPC registry artifacts](../integrations/dnd-npc-registry-artifacts.md) | | Combat turns | [combat-turn artifacts](../integrations/dnd-combat-turn-artifacts.md) | -| Item events | [item-event artifacts](../integrations/dnd-item-event-artifacts.md) | +| Item occurrences | [item-occurrence artifacts](../integrations/dnd-item-occurrence-artifacts.md) | | Item registry | [item-registry artifacts](../integrations/dnd-item-registry-artifacts.md) | | NPC occurrences | [NPC-occurrence artifacts](../integrations/dnd-npc-occurrence-artifacts.md) | | Scene descriptions | [scene-description artifacts](../integrations/dnd-scene-description-artifacts.md) | @@ -123,7 +123,7 @@ the same comparison identity within one scene-scoped result. Normalization may combine results from distinct scenes, so it intentionally does not apply that rule. Configuration owns the exact validator key and chain position. -Normalizers are deterministic for spells, combat turns, item events, NPC +Normalizers are deterministic for spells, combat turns, item occurrences, NPC occurrences, scene descriptions, enemy events, and location occurrences. They canonicalize display values and evidence, use source-document order for stable output, and issue bounded warnings for changes or collapsed duplicates. The NPC and location @@ -163,7 +163,7 @@ shared helper changes. | Spells | May use a spell-catalog overlay and optional NPC grounding; the catalog validator supplies domain-specific semantic checks. | | NPCs | Does not consume an NPC registry. Its normalizer is the LLM-assisted reconciliation exception described above. | | Combat turns | Requires a scene-description artifact. It calls the LLM only for an exact `combat` classification; exact non-combat classifications return an accepted empty result, while missing or mismatched classifications return an empty result with a bounded warning. Optional NPC grounding never becomes evidence. | -| Item events | Uses campaign context for disambiguation but has no NPC-registry or scene-description dependency. | +| Item occurrences | Uses campaign context for disambiguation but has no NPC-registry or scene-description dependency. | | Item registry | Produces source-grounded item types and unique designations. Its LLM-assisted reconciliation is proposal-only, preserves distinct currency denominations and item types, and does not create per-instance identities. | | NPC occurrences | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. | | Scene descriptions | Produces the classifications consumed by combat routing; it does not consume an NPC registry or provide evidence for combat artifacts. | diff --git a/examples/dnd-complete.config.yml b/examples/dnd-complete.config.yml index b20c4a1..7d989f0 100644 --- a/examples/dnd-complete.config.yml +++ b/examples/dnd-complete.config.yml @@ -35,7 +35,7 @@ pipelines: enabled: true window_units: 3 lanes: - - item-events + - item-occurrences - item-registry - locations - location-occurrences @@ -97,12 +97,12 @@ pipelines: step: describe-session lane: item-registry artifacts: - item-events: + item-occurrences: extract: - module: dnd/item-events + module: dnd/item-occurrences retries: 2 merge: appendorder - normalize: dnd/item-events + normalize: dnd/item-occurrences spells: extract: module: dnd/spells diff --git a/internal/cli/dnd_enemy_events_contract_test.go b/internal/cli/dnd_enemy_events_contract_test.go index 29691aa..270938c 100644 --- a/internal/cli/dnd_enemy_events_contract_test.go +++ b/internal/cli/dnd_enemy_events_contract_test.go @@ -24,7 +24,7 @@ import ( locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry" combat "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" enemyevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" - itemevents "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" + itemoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences" itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry" locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry" @@ -259,7 +259,7 @@ func (client *enemyEventLLMClient) CompleteStructured(ctx context.Context, reque content = []byte(`{"items":[]}`) case itemregistrynormalize.PromptID: content = []byte(`{"duplicate_groups":[]}`) - case itemevents.PromptID: + case itemoccurrences.PromptID: content = []byte(`{"occurrences":[]}`) case combat.PromptID: content = []byte(`{"combat_turns":[{"actor":"Kesh","turn_kind":"turn","source_refs":[{"start_unit_id":8,"end_unit_id":8}]}]}`) diff --git a/internal/cli/example_contract_test.go b/internal/cli/example_contract_test.go index 9085e2b..b9ec044 100644 --- a/internal/cli/example_contract_test.go +++ b/internal/cli/example_contract_test.go @@ -58,7 +58,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) { t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err) } if example.name == "complete" { - if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-registry,locations,npc_registry,scene-descriptions|extract-events:combat-turns,item-events,location-occurrences,npc-occurrences,spells|track-enemies:enemy-events" { + if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-registry,locations,npc_registry,scene-descriptions|extract-events:combat-turns,item-occurrences,location-occurrences,npc-occurrences,spells|track-enemies:enemy-events" { t.Fatalf("complete example steps and lanes = %v, want the documented D&D extractor composition", got) } locationLane := referenceContractLane(t, materialized, "locations") @@ -88,13 +88,13 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) { len(spellLane.NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 { t.Fatalf("complete example spell catalog reference was not materialized: %#v", spellLane) } - itemEventLane := referenceContractLane(t, materialized, "item-events") - for _, references := range []pipeline.ResolvedReferenceTarget{itemEventLane.ExtractReferences, itemEventLane.NormalizeReferences} { + itemOccurrenceLane := referenceContractLane(t, materialized, "item-occurrences") + for _, references := range []pipeline.ResolvedReferenceTarget{itemOccurrenceLane.ExtractReferences, itemOccurrenceLane.NormalizeReferences} { if _, found := references.ReferenceSet.Slots["npc_registry"]; found { - t.Fatalf("item event lane unexpectedly depends on generated NPCs: %#v", itemEventLane) + t.Fatalf("item occurrence lane unexpectedly depends on generated NPCs: %#v", itemOccurrenceLane) } if _, found := references.ReferenceSet.Slots["scene_descriptions"]; found { - t.Fatalf("item event lane unexpectedly depends on generated scene descriptions: %#v", itemEventLane) + t.Fatalf("item occurrence lane unexpectedly depends on generated scene descriptions: %#v", itemOccurrenceLane) } } enemyEventLane := referenceContractLane(t, materialized, "enemy-events") diff --git a/internal/cli/production_contract_test.go b/internal/cli/production_contract_test.go index ed130fc..c35b2e6 100644 --- a/internal/cli/production_contract_test.go +++ b/internal/cli/production_contract_test.go @@ -29,16 +29,16 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns" enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents" - itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents" + itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences" spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" - itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" + itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences" itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" - itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" + itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences" itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry" spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells" "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop" @@ -51,9 +51,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { assertProductionContains(t, "inputs", registries.Inputs.RegisteredKeys(), []string{"seriatim"}) assertProductionContains(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes", "generic"}) - assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", "dnd/npc-registry", combatextract.Key, itemeventextract.Key, enemyeventextract.Key}) + assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", "dnd/npc-registry", combatextract.Key, itemoccurrenceextract.Key, enemyeventextract.Key}) assertProductionContains(t, "mergers", registries.Mergers.RegisteredKeys(), []string{"appendorder"}) - assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{"noop", spellnormalize.Key, "dnd/npc-registry", combatnormalize.Key, itemeventnormalize.Key, enemyeventnormalize.Key}) + assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{"noop", spellnormalize.Key, "dnd/npc-registry", combatnormalize.Key, itemoccurrencenormalize.Key, enemyeventnormalize.Key}) assertProductionContains(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"}) assertProductionContains(t, "validators", registries.Validators.RegisteredKeys(), []string{ "extract/dnd/spells/catalog", @@ -64,10 +64,10 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { "extract/dnd/combat-turns/source_refs", "extract/dnd/combat-turns/source_relatedness", "normalize/dnd/combat-turns/invariants", - "extract/dnd/item-events/shape", - "extract/dnd/item-events/source_refs", - "extract/dnd/item-events/source_relatedness", - "normalize/dnd/item-events/invariants", + "extract/dnd/item-occurrences/shape", + "extract/dnd/item-occurrences/source_refs", + "extract/dnd/item-occurrences/source_relatedness", + "normalize/dnd/item-occurrences/invariants", "extract/dnd/enemy-events/shape", "extract/dnd/enemy-events/engagements", "extract/dnd/enemy-events/source_refs", @@ -83,7 +83,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemOccurrenceListKind, dnd.EnemyEventListKind}) assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind}) assertProductionContains(t, "combat normalizer variants", registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind}) - assertProductionContains(t, "item event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind}) + assertProductionContains(t, "item occurrence normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind}) assertProductionContains(t, "enemy event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind}) wantChain := []pipeline.ModuleBinding{ @@ -121,28 +121,28 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, combatNormalizeChain) { t.Fatalf("combat normalize validator chain = %#v, want %#v", got, combatNormalizeChain) } - itemEventExtractChain := []pipeline.ModuleBinding{ + itemOccurrenceExtractChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), - pipeline.Binding("extract/dnd/item-events/shape"), - pipeline.Binding("extract/dnd/item-events/registry"), - pipeline.Binding("extract/dnd/item-events/source_refs"), + pipeline.Binding("extract/dnd/item-occurrences/shape"), + pipeline.Binding("extract/dnd/item-occurrences/registry"), + pipeline.Binding("extract/dnd/item-occurrences/source_refs"), pipeline.Binding("generic/valid_json_schema"), - pipeline.Binding("extract/dnd/item-events/source_relatedness"), + pipeline.Binding("extract/dnd/item-occurrences/source_relatedness"), } - itemEventNormalizeChain := []pipeline.ModuleBinding{ + itemOccurrenceNormalizeChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), - pipeline.Binding("extract/dnd/item-events/shape"), - pipeline.Binding("extract/dnd/item-events/registry"), - pipeline.Binding("normalize/dnd/item-events/invariants"), - pipeline.Binding("extract/dnd/item-events/source_refs"), + pipeline.Binding("extract/dnd/item-occurrences/shape"), + pipeline.Binding("extract/dnd/item-occurrences/registry"), + pipeline.Binding("normalize/dnd/item-occurrences/invariants"), + pipeline.Binding("extract/dnd/item-occurrences/source_refs"), pipeline.Binding("generic/valid_json_schema"), - pipeline.Binding("extract/dnd/item-events/source_relatedness"), + pipeline.Binding("extract/dnd/item-occurrences/source_relatedness"), } - if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemeventextract.Key); !reflect.DeepEqual(got, itemEventExtractChain) { - t.Fatalf("item event extract validator chain = %#v, want %#v", got, itemEventExtractChain) + if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemoccurrenceextract.Key); !reflect.DeepEqual(got, itemOccurrenceExtractChain) { + t.Fatalf("item occurrence extract validator chain = %#v, want %#v", got, itemOccurrenceExtractChain) } - 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) + if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemoccurrencenormalize.Key); !reflect.DeepEqual(got, itemOccurrenceNormalizeChain) { + t.Fatalf("item occurrence normalize validator chain = %#v, want %#v", got, itemOccurrenceNormalizeChain) } enemyEventExtractChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), @@ -168,7 +168,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { {stage: pipeline.StageExtract, key: "dnd/spells", want: contracts.ExecutionClassLLMBacked}, {stage: pipeline.StageExtract, key: "dnd/npc-registry", want: contracts.ExecutionClassLLMBacked}, {stage: pipeline.StageExtract, key: "dnd/combat-turns", want: contracts.ExecutionClassLLMBacked}, - {stage: pipeline.StageExtract, key: "dnd/item-events", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/item-occurrences", 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}, @@ -177,7 +177,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { {stage: pipeline.StageNormalize, key: "dnd/spells", want: contracts.ExecutionClassDeterministic}, {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/item-occurrences", 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}, @@ -200,9 +200,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { if !ok || combatCodecSpec.Kind != dnd.CombatTurnListKind || combatCodecSpec.Schema.ID != combatcodec.SchemaID { t.Fatalf("combat codec spec = %#v, ok=%t, want typed D&D combat codec", combatCodecSpec, ok) } - itemEventCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.ItemOccurrenceListKind) - if !ok || itemEventCodecSpec.Kind != dnd.ItemOccurrenceListKind || itemEventCodecSpec.Schema.ID != itemeventcodec.SchemaID { - t.Fatalf("item event codec spec = %#v, ok=%t, want typed D&D item-event codec", itemEventCodecSpec, ok) + itemOccurrenceCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.ItemOccurrenceListKind) + if !ok || itemOccurrenceCodecSpec.Kind != dnd.ItemOccurrenceListKind || itemOccurrenceCodecSpec.Schema.ID != itemoccurrencecodec.SchemaID { + t.Fatalf("item occurrence codec spec = %#v, ok=%t, want typed D&D item-occurrence codec", itemOccurrenceCodecSpec, ok) } enemyEventCodecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.EnemyEventListKind) if !ok || enemyEventCodecSpec.Kind != dnd.EnemyEventListKind || enemyEventCodecSpec.Schema.ID != enemyeventcodec.SchemaID { @@ -783,8 +783,8 @@ func TestProductionSceneRunRecordsAnnotationFreeChunkPlanAndProvenance(t *testin if len(warnings.Warnings) != 0 { t.Fatalf("warnings = %#v, want none", warnings.Warnings) } - if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 || len(fake.requestsFor(itemeventextract.PromptID)) != 1 { - t.Fatalf("fake prompt requests = %#v, want one scene, spell, and item-event request", fake.requestPrompts()) + if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 || len(fake.requestsFor(itemoccurrenceextract.PromptID)) != 1 { + t.Fatalf("fake prompt requests = %#v, want one scene, spell, and item-occurrence request", fake.requestPrompts()) } } @@ -935,8 +935,8 @@ pipelines: artifacts: spells: extract: dnd/spells - item-events: - extract: dnd/item-events + item-occurrences: + extract: dnd/item-occurrences `, outputRoot, filepath.Join(filepath.Dir(outputRoot), "debug"), chunkModule) } @@ -1013,7 +1013,7 @@ func (client *productionFakeLLMClient) CompleteStructured(ctx context.Context, r } else { content = []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`) } - case itemeventextract.PromptID: + case itemoccurrenceextract.PromptID: content = []byte(`{"occurrences":[]}`) case itemregistryextract.PromptID: content = []byte(`{"items":[]}`) diff --git a/internal/modules/dnd/codec/itemevents/assets/schemas/dnd_item_occurrences.v1.json b/internal/modules/dnd/codec/itemoccurrences/assets/schemas/dnd_item_occurrences.v1.json similarity index 100% rename from internal/modules/dnd/codec/itemevents/assets/schemas/dnd_item_occurrences.v1.json rename to internal/modules/dnd/codec/itemoccurrences/assets/schemas/dnd_item_occurrences.v1.json diff --git a/internal/modules/dnd/codec/itemevents/codec.go b/internal/modules/dnd/codec/itemoccurrences/codec.go similarity index 80% rename from internal/modules/dnd/codec/itemevents/codec.go rename to internal/modules/dnd/codec/itemoccurrences/codec.go index 42ff39c..5d4bdd5 100644 --- a/internal/modules/dnd/codec/itemevents/codec.go +++ b/internal/modules/dnd/codec/itemoccurrences/codec.go @@ -1,5 +1,5 @@ -// Package itemevents encodes durable D&D item-event artifacts. -package itemevents +// Package itemoccurrences encodes durable D&D item-occurrence artifacts. +package itemoccurrences import ( "embed" @@ -10,7 +10,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences" ) const ( @@ -86,35 +86,35 @@ func (c *Codec) DecodeCandidate(content []byte) (dnd.ItemOccurrenceList, error) func validate(value dnd.ItemOccurrenceList) error { if value.Occurrences == nil { - return fmt.Errorf("events must be present") + return fmt.Errorf("occurrences must be present") } - for index, event := range value.Occurrences { + for index, occurrence := range value.Occurrences { prefix := fmt.Sprintf("occurrences[%d]", index) - if strings.TrimSpace(event.ItemID) == "" { + if strings.TrimSpace(occurrence.ItemID) == "" { return fmt.Errorf("%s.item_id must not be empty", prefix) } - if strings.TrimSpace(event.Name) == "" { + if strings.TrimSpace(occurrence.Name) == "" { return fmt.Errorf("%s.name must not be empty", prefix) } - if !itemevents.SupportedKind(event.Kind) { + if !itemoccurrences.SupportedKind(occurrence.Kind) { return fmt.Errorf("%s.kind must be supported", prefix) } - if event.From != "" && strings.TrimSpace(event.From) == "" { + if occurrence.From != "" && strings.TrimSpace(occurrence.From) == "" { return fmt.Errorf("%s.from must not be empty when present", prefix) } - if event.To != "" && strings.TrimSpace(event.To) == "" { + if occurrence.To != "" && strings.TrimSpace(occurrence.To) == "" { return fmt.Errorf("%s.to must not be empty when present", prefix) } - if !itemevents.ValidHolderCombination(event.Kind, event.From, event.To) { - return fmt.Errorf("%s holders are incompatible with %q", prefix, event.Kind) + if !itemoccurrences.ValidHolderCombination(occurrence.Kind, occurrence.From, occurrence.To) { + return fmt.Errorf("%s holders are incompatible with %q", prefix, occurrence.Kind) } - if event.Quantity != nil && *event.Quantity < 1 { + if occurrence.Quantity != nil && *occurrence.Quantity < 1 { return fmt.Errorf("%s.quantity must be positive when present", prefix) } - if len(event.SourceRefs) == 0 { + if len(occurrence.SourceRefs) == 0 { return fmt.Errorf("%s.source_refs must contain at least one reference", prefix) } - for refIndex, ref := range event.SourceRefs { + for refIndex, ref := range occurrence.SourceRefs { refPrefix := fmt.Sprintf("%s.source_refs[%d]", prefix, refIndex) if strings.TrimSpace(ref.SourceID) == "" { return fmt.Errorf("%s.source_id must not be empty", refPrefix) @@ -135,14 +135,14 @@ func cloneList(value dnd.ItemOccurrenceList) dnd.ItemOccurrenceList { return dnd.ItemOccurrenceList{} } cloned := dnd.ItemOccurrenceList{Occurrences: make([]dnd.ItemOccurrence, len(value.Occurrences))} - for index, event := range value.Occurrences { - cloned.Occurrences[index] = event - if event.Quantity != nil { - quantity := *event.Quantity + for index, occurrence := range value.Occurrences { + cloned.Occurrences[index] = occurrence + if occurrence.Quantity != nil { + quantity := *occurrence.Quantity cloned.Occurrences[index].Quantity = &quantity } - if event.SourceRefs != nil { - cloned.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), event.SourceRefs...) + if occurrence.SourceRefs != nil { + cloned.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), occurrence.SourceRefs...) } } return cloned diff --git a/internal/modules/dnd/codec/itemevents/codec_test.go b/internal/modules/dnd/codec/itemoccurrences/codec_test.go similarity index 95% rename from internal/modules/dnd/codec/itemevents/codec_test.go rename to internal/modules/dnd/codec/itemoccurrences/codec_test.go index 7f146df..c1006b0 100644 --- a/internal/modules/dnd/codec/itemevents/codec_test.go +++ b/internal/modules/dnd/codec/itemoccurrences/codec_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "bytes" @@ -93,10 +93,10 @@ func TestCodecRejectsStrictJSONAndApprovedBoundaries(t *testing.T) { }{ {"malformed", `{`, "decode dnd item occurrence list"}, {"unknown top level", `{"occurrences":[],"unexpected":true}`, "unknown field"}, - {"unknown event field", strings.Replace(validJSON, `"to":"party"`, `"to":"party","unexpected":true`, 1), "unknown field"}, + {"unknown occurrence field", strings.Replace(validJSON, `"to":"party"`, `"to":"party","unexpected":true`, 1), "unknown field"}, {"unknown reference field", strings.Replace(validJSON, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"}, {"trailing", `{"occurrences":[]} {}`, "multiple JSON values"}, - {"missing list", `{}`, "events must be present"}, + {"missing list", `{}`, "occurrences must be present"}, {"zero quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":0,"to":"party"`, 1), "quantity must be positive"}, {"negative quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":-1,"to":"party"`, 1), "quantity must be positive"}, {"party transfer", strings.Replace(validJSON, `"kind":"acquired","to":"party"`, `"kind":"transferred","from":"party","to":"Borin"`, 1), "holders are incompatible"}, @@ -125,7 +125,7 @@ func TestCodecDeepCopiesBoundaryValuesAndMetadata(t *testing.T) { t.Fatal(err) } if decoded.Occurrences[1].Quantity == value.Occurrences[1].Quantity || &decoded.Occurrences[1].SourceRefs[0] == &value.Occurrences[1].SourceRefs[0] { - t.Fatal("DecodeCandidate() retained caller-owned event fields") + t.Fatal("DecodeCandidate() retained caller-owned occurrence fields") } *decoded.Occurrences[1].Quantity = 99 decoded.Occurrences[1].SourceRefs[0].SourceID = "changed" diff --git a/internal/modules/dnd/extract/itemevents/canonicalize.go b/internal/modules/dnd/extract/itemoccurrences/canonicalize.go similarity index 76% rename from internal/modules/dnd/extract/itemevents/canonicalize.go rename to internal/modules/dnd/extract/itemoccurrences/canonicalize.go index 67a5d8b..11c09ea 100644 --- a/internal/modules/dnd/extract/itemevents/canonicalize.go +++ b/internal/modules/dnd/extract/itemoccurrences/canonicalize.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "sort" @@ -42,12 +42,12 @@ func canonicalizeResponse(response *extractionResponse, order shared.SourceRefOr } } -func canonicalizeItemOccurrence(event *itemOccurrenceResponse, order shared.SourceRefOrder, sourceID string) (int, bool) { - if event == nil { +func canonicalizeItemOccurrence(occurrence *itemOccurrenceResponse, order shared.SourceRefOrder, sourceID string) (int, bool) { + if occurrence == nil { return 0, false } - refs := order.Canonicalize(itemOccurrenceSourceRefs(event.SourceRefs, sourceID)) - event.SourceRefs = itemOccurrenceResponseRefs(refs) + refs := order.Canonicalize(itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID)) + occurrence.SourceRefs = itemOccurrenceResponseRefs(refs) return order.EarliestValid(refs) } @@ -56,19 +56,19 @@ func canonicalItemOccurrenceList(response extractionResponse, sourceID string, r return dnd.ItemOccurrenceList{} } occurrences := make([]dnd.ItemOccurrence, 0, len(response.Occurrences)) - for _, event := range response.Occurrences { - item, found := registry.LookupID(event.ItemID) - if !found || item.Name != event.Name { + for _, occurrence := range response.Occurrences { + item, found := registry.LookupID(occurrence.ItemID) + if !found || item.Name != occurrence.Name { continue } occurrences = append(occurrences, dnd.ItemOccurrence{ - ItemID: event.ItemID, - Name: event.Name, - Kind: dnd.ItemOccurrenceKind(event.Kind), - Quantity: cloneQuantity(event.Quantity), - From: event.From, - To: event.To, - SourceRefs: itemOccurrenceSourceRefs(event.SourceRefs, sourceID), + ItemID: occurrence.ItemID, + Name: occurrence.Name, + Kind: dnd.ItemOccurrenceKind(occurrence.Kind), + Quantity: cloneQuantity(occurrence.Quantity), + From: occurrence.From, + To: occurrence.To, + SourceRefs: itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID), }) } return dnd.ItemOccurrenceList{Occurrences: occurrences} diff --git a/internal/modules/dnd/extract/itemevents/extractor.go b/internal/modules/dnd/extract/itemoccurrences/extractor.go similarity index 97% rename from internal/modules/dnd/extract/itemevents/extractor.go rename to internal/modules/dnd/extract/itemoccurrences/extractor.go index 16a04cb..2de9970 100644 --- a/internal/modules/dnd/extract/itemevents/extractor.go +++ b/internal/modules/dnd/extract/itemoccurrences/extractor.go @@ -1,5 +1,5 @@ -// Package itemevents extracts source-grounded D&D item occurrences. -package itemevents +// Package itemoccurrences extracts source-grounded D&D item occurrences. +package itemoccurrences import ( "context" @@ -13,14 +13,14 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) -const Key = "dnd/item-events" +const Key = "dnd/item-occurrences" const ( ItemRegistryReferenceSlot = itemregistry.ReferenceSlot ItemRegistryMaxBytes = itemregistry.MaxBytes ) -const mappingPolicy = "dnd.item_events.extract_mapping.v1" +const mappingPolicy = "dnd.item_occurrences.extract_mapping.v1" var requiredCapabilities = []string{ "chunks", @@ -28,7 +28,7 @@ var requiredCapabilities = []string{ } var providedCapabilities = []string{ - "dnd.item_events", + "dnd.item_occurrences", } var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ diff --git a/internal/modules/dnd/extract/itemevents/extractor_test.go b/internal/modules/dnd/extract/itemoccurrences/extractor_test.go similarity index 99% rename from internal/modules/dnd/extract/itemevents/extractor_test.go rename to internal/modules/dnd/extract/itemoccurrences/extractor_test.go index e4c6cbe..e8bd657 100644 --- a/internal/modules/dnd/extract/itemevents/extractor_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/extractor_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "context" diff --git a/internal/modules/dnd/extract/itemevents/model.go b/internal/modules/dnd/extract/itemoccurrences/model.go similarity index 96% rename from internal/modules/dnd/extract/itemevents/model.go rename to internal/modules/dnd/extract/itemoccurrences/model.go index 769b500..c205d80 100644 --- a/internal/modules/dnd/extract/itemevents/model.go +++ b/internal/modules/dnd/extract/itemoccurrences/model.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences type extractionResponse struct { Occurrences []itemOccurrenceResponse `json:"occurrences"` diff --git a/internal/modules/dnd/extract/itemevents/prompt_assets.go b/internal/modules/dnd/extract/itemoccurrences/prompt_assets.go similarity index 87% rename from internal/modules/dnd/extract/itemevents/prompt_assets.go rename to internal/modules/dnd/extract/itemoccurrences/prompt_assets.go index c5602b7..5babe99 100644 --- a/internal/modules/dnd/extract/itemevents/prompt_assets.go +++ b/internal/modules/dnd/extract/itemoccurrences/prompt_assets.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "fmt" @@ -14,7 +14,7 @@ import ( const promptAssetRoot = "assets/prompts" var promptAssetManifest = shared.PromptAssetManifest{ - ModuleDir: "dnd.item_events", + ModuleDir: "dnd.item_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/item-events") + assets, err := fs.Sub(rootassets.FS(), "dnd/item-occurrences") if err != nil { - return nil, fmt.Errorf("scope item-event assets: %w", err) + return nil, fmt.Errorf("scope item-occurrence assets: %w", err) } return assets, nil } @@ -51,7 +51,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error { } schemas, err := fs.Sub(assets, "schemas") if err != nil { - return fmt.Errorf("scope item-event schemas: %w", err) + return fmt.Errorf("scope item-occurrence schemas: %w", err) } return registry.RegisterSchemaFS(schemas, ".") } diff --git a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go b/internal/modules/dnd/extract/itemoccurrences/prompt_assets_test.go similarity index 86% rename from internal/modules/dnd/extract/itemevents/prompt_assets_test.go rename to internal/modules/dnd/extract/itemoccurrences/prompt_assets_test.go index 4f10e79..77640d6 100644 --- a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/prompt_assets_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "context" @@ -21,17 +21,17 @@ func TestPromptAssetsPrepareItemOccurrencePrompt(t *testing.T) { t.Fatal(err) } options = append(options, promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{ - ID: "item-events-test-profile", Endpoint: "http://127.0.0.1:1/v1", Model: "item-events-test-model", + ID: "item-occurrences-test-profile", Endpoint: "http://127.0.0.1:1/v1", Model: "item-occurrences-test-model", }))) engine, err := promptkit.NewEngine(promptkit.Config{Timeout: time.Second}, options...) if err != nil { t.Fatal(err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ - PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "item-events-test-profile", + PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "item-occurrences-test-profile", Inputs: map[string]promptkit.ArtifactRef{ - "transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-event-transcript"}]}`), - "players": promptkit.Inline("item-event-player"), + "transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-occurrence-transcript"}]}`), + "players": promptkit.Inline("item-occurrence-player"), "party": promptkit.Inline(" "), "glossary": promptkit.Inline(" "), "item_registry": promptkit.Inline(`{"items":[{"id":"item:sha256:test","name":"Torch"}]}`), diff --git a/internal/modules/dnd/extract/itemevents/registry_test.go b/internal/modules/dnd/extract/itemoccurrences/registry_test.go similarity index 98% rename from internal/modules/dnd/extract/itemevents/registry_test.go rename to internal/modules/dnd/extract/itemoccurrences/registry_test.go index e29e30f..d50454c 100644 --- a/internal/modules/dnd/extract/itemevents/registry_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/registry_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "reflect" diff --git a/internal/modules/dnd/extract/itemevents/schema.go b/internal/modules/dnd/extract/itemoccurrences/schema.go similarity index 91% rename from internal/modules/dnd/extract/itemevents/schema.go rename to internal/modules/dnd/extract/itemoccurrences/schema.go index a02828e..506c30e 100644 --- a/internal/modules/dnd/extract/itemevents/schema.go +++ b/internal/modules/dnd/extract/itemoccurrences/schema.go @@ -1,9 +1,9 @@ -package itemevents +package itemoccurrences import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" const ( - PromptID = "dnd.item_events" + PromptID = "dnd.item_occurrences" ResponseSchemaKey = llm.ResponseSchemaKey("dnd_item_occurrences_llm") ResponseSchemaID = "notarius.dnd.item_occurrences.llm" ResponseSchemaName = "notarius_dnd_item_occurrences_llm_v1" diff --git a/internal/modules/dnd/extract/itemevents/schema_test.go b/internal/modules/dnd/extract/itemoccurrences/schema_test.go similarity index 84% rename from internal/modules/dnd/extract/itemevents/schema_test.go rename to internal/modules/dnd/extract/itemoccurrences/schema_test.go index e6ed93d..c2c1fc5 100644 --- a/internal/modules/dnd/extract/itemevents/schema_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/schema_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "bytes" @@ -39,11 +39,11 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) { value map[string]any }{ {"missing occurrences", map[string]any{}}, - {"missing event name", map[string]any{"occurrences": []any{withoutField(responseEvent(), "name")}}}, - {"missing nullable field", map[string]any{"occurrences": []any{withoutField(responseEvent(), "quantity")}}}, - {"unknown event field", map[string]any{"occurrences": []any{withField(responseEvent(), "extra", true)}}}, - {"unknown reference field", map[string]any{"occurrences": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1, "end_segment": 1, "extra": true}})}}}, - {"noninteger range", map[string]any{"occurrences": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1.5, "end_segment": 1}})}}}, + {"missing occurrence name", map[string]any{"occurrences": []any{withoutField(responseOccurrence(), "name")}}}, + {"missing nullable field", map[string]any{"occurrences": []any{withoutField(responseOccurrence(), "quantity")}}}, + {"unknown occurrence field", map[string]any{"occurrences": []any{withField(responseOccurrence(), "extra", true)}}}, + {"unknown reference field", map[string]any{"occurrences": []any{withField(responseOccurrence(), "source_refs", []any{map[string]any{"start_segment": 1, "end_segment": 1, "extra": true}})}}}, + {"noninteger range", map[string]any{"occurrences": []any{withField(responseOccurrence(), "source_refs", []any{map[string]any{"start_segment": 1.5, "end_segment": 1}})}}}, } { t.Run(test.name, func(t *testing.T) { content, err := json.Marshal(test.value) @@ -66,7 +66,7 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) { } } -func responseEvent() map[string]any { +func responseOccurrence() map[string]any { return map[string]any{ "item_id": "item:sha256:test", "name": "Ring", "kind": "acquired", "quantity": nil, "from": nil, "to": "party", "source_refs": []any{}, } diff --git a/internal/modules/dnd/extract/itemevents/test_helpers_test.go b/internal/modules/dnd/extract/itemoccurrences/test_helpers_test.go similarity index 97% rename from internal/modules/dnd/extract/itemevents/test_helpers_test.go rename to internal/modules/dnd/extract/itemoccurrences/test_helpers_test.go index d491546..80fe78d 100644 --- a/internal/modules/dnd/extract/itemevents/test_helpers_test.go +++ b/internal/modules/dnd/extract/itemoccurrences/test_helpers_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "context" @@ -26,7 +26,7 @@ func extractionRequest() contracts.TypedExtractionRequest { Chunk: chunk, SourceInput: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "sha256:chunk", "file:///session-alpha.json"), SessionID: "session-123", - LLMProfile: "profile-item-events", + LLMProfile: "profile-item-occurrences", } } diff --git a/internal/modules/dnd/itemevents/itemevents.go b/internal/modules/dnd/itemoccurrences/itemoccurrences.go similarity index 88% rename from internal/modules/dnd/itemevents/itemevents.go rename to internal/modules/dnd/itemoccurrences/itemoccurrences.go index 1936156..5a40f1e 100644 --- a/internal/modules/dnd/itemevents/itemevents.go +++ b/internal/modules/dnd/itemoccurrences/itemoccurrences.go @@ -1,6 +1,6 @@ -// Package itemevents owns canonical ordering and durable domain rules for D&D -// item-event artifacts. -package itemevents +// Package itemoccurrences owns canonical ordering and durable domain rules for D&D +// item-occurrence artifacts. +package itemoccurrences import ( "strconv" @@ -90,7 +90,7 @@ func ValidSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool { return true } -// Less defines the canonical event order. Invalid source references remain +// Less defines the canonical occurrence order. Invalid source references remain // comparable through SourceRefOrder's literal fallback so malformed candidates // are still safe to sort and diagnose. func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool { @@ -126,7 +126,7 @@ func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool { return sourceRefsLess(order, order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs)) } -// ExactEqual reports whether events are exact duplicates after their display +// ExactEqual reports whether occurrences are exact duplicates after their display // fields and evidence have been canonicalized for the supplied source order. func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool { if left.ItemID != right.ItemID || DisplayValue(left.Name) != DisplayValue(right.Name) || left.Kind != right.Kind || @@ -142,21 +142,21 @@ func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) boo // ExactIdentity returns a collision-safe duplicate key after display and // evidence canonicalization. It is intended for callers that have already -// decided the event is eligible for duplicate handling. -func ExactIdentity(order shared.SourceRefOrder, event dnd.ItemOccurrence) string { +// decided the occurrence is eligible for duplicate handling. +func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) string { var key strings.Builder - writeKeyString(&key, event.ItemID) - writeKeyString(&key, DisplayValue(event.Name)) - writeKeyString(&key, string(event.Kind)) - writeKeyString(&key, DisplayValue(event.From)) - writeKeyString(&key, DisplayValue(event.To)) - if event.Quantity == nil { + writeKeyString(&key, occurrence.ItemID) + writeKeyString(&key, DisplayValue(occurrence.Name)) + writeKeyString(&key, string(occurrence.Kind)) + writeKeyString(&key, DisplayValue(occurrence.From)) + writeKeyString(&key, DisplayValue(occurrence.To)) + if occurrence.Quantity == nil { key.WriteByte('0') } else { key.WriteByte('1') - writeKeyInt(&key, *event.Quantity) + writeKeyInt(&key, *occurrence.Quantity) } - for _, ref := range order.Canonicalize(event.SourceRefs) { + for _, ref := range order.Canonicalize(occurrence.SourceRefs) { writeKeyString(&key, ref.SourceID) writeKeyInt(&key, ref.StartUnitID) writeKeyInt(&key, ref.EndUnitID) diff --git a/internal/modules/dnd/itemevents/itemevents_test.go b/internal/modules/dnd/itemoccurrences/itemoccurrences_test.go similarity index 81% rename from internal/modules/dnd/itemevents/itemevents_test.go rename to internal/modules/dnd/itemoccurrences/itemoccurrences_test.go index bb1bd6c..9885055 100644 --- a/internal/modules/dnd/itemevents/itemevents_test.go +++ b/internal/modules/dnd/itemoccurrences/itemoccurrences_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "sort" @@ -136,13 +136,13 @@ func TestSourceReferenceHelpers(t *testing.T) { func TestLessSortsMalformedReferencesDeterministically(t *testing.T) { order := testOrder() - events := []dnd.ItemOccurrence{{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 999, EndUnitID: 999}}}, + occurrences := []dnd.ItemOccurrence{{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 999, EndUnitID: 999}}}, {ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 1}}}, {ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}}, } - sort.SliceStable(events, func(left, right int) bool { return Less(order, events[left], events[right]) }) - if events[0].SourceRefs[0].StartUnitID != 10 || events[1].SourceRefs[0].SourceID != "other" || events[2].SourceRefs[0].StartUnitID != 999 { - t.Fatalf("canonical sort = %#v", events) + sort.SliceStable(occurrences, func(left, right int) bool { return Less(order, occurrences[left], occurrences[right]) }) + if occurrences[0].SourceRefs[0].StartUnitID != 10 || occurrences[1].SourceRefs[0].SourceID != "other" || occurrences[2].SourceRefs[0].StartUnitID != 999 { + t.Fatalf("canonical sort = %#v", occurrences) } } @@ -150,28 +150,31 @@ func testOrder() shared.SourceRefOrder { return shared.NewSourceRefOrder(&source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}, {ID: 20}, {ID: 30}}}) } -func withName(event dnd.ItemOccurrence, value string) dnd.ItemOccurrence { - event.Name = value - return event +func withName(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence { + occurrence.Name = value + return occurrence } -func withKind(event dnd.ItemOccurrence, value dnd.ItemOccurrenceKind) dnd.ItemOccurrence { - event.Kind = value - return event +func withKind(occurrence dnd.ItemOccurrence, value dnd.ItemOccurrenceKind) dnd.ItemOccurrence { + occurrence.Kind = value + return occurrence } -func withFrom(event dnd.ItemOccurrence, value string) dnd.ItemOccurrence { - event.From = value - return event +func withFrom(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence { + occurrence.From = value + return occurrence } -func withTo(event dnd.ItemOccurrence, value string) dnd.ItemOccurrence { - event.To = value - return event +func withTo(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence { + occurrence.To = value + return occurrence } -func withoutTo(event dnd.ItemOccurrence) dnd.ItemOccurrence { event.To = ""; return event } -func withQuantity(event dnd.ItemOccurrence, value *int) dnd.ItemOccurrence { - event.Quantity = value - return event +func withoutTo(occurrence dnd.ItemOccurrence) dnd.ItemOccurrence { + occurrence.To = "" + return occurrence } -func withRefs(event dnd.ItemOccurrence, value []source.SourceRef) dnd.ItemOccurrence { - event.SourceRefs = value - return event +func withQuantity(occurrence dnd.ItemOccurrence, value *int) dnd.ItemOccurrence { + occurrence.Quantity = value + return occurrence +} +func withRefs(occurrence dnd.ItemOccurrence, value []source.SourceRef) dnd.ItemOccurrence { + occurrence.SourceRefs = value + return occurrence } diff --git a/internal/modules/dnd/normalize/itemevents/normalizer.go b/internal/modules/dnd/normalize/itemoccurrences/normalizer.go similarity index 82% rename from internal/modules/dnd/normalize/itemevents/normalizer.go rename to internal/modules/dnd/normalize/itemoccurrences/normalizer.go index 2a03802..dc6c0f4 100644 --- a/internal/modules/dnd/normalize/itemevents/normalizer.go +++ b/internal/modules/dnd/normalize/itemoccurrences/normalizer.go @@ -1,5 +1,5 @@ -// Package itemevents normalizes merged D&D item-event candidates. -package itemevents +// Package itemoccurrences normalizes merged D&D item-occurrence candidates. +package itemoccurrences import ( "context" @@ -10,15 +10,15 @@ 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" - itemeventmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents" + itemoccurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences" itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" ) const ( - Key = "dnd/item-events" - normalizationPolicy = "dnd.item_events.normalize.v1" + Key = "dnd/item-occurrences" + normalizationPolicy = "dnd.item_occurrences.normalize.v1" NormalizationPolicy = normalizationPolicy ReasonCodeNameCanonicalized = "item_occurrence_name_canonicalized" @@ -111,7 +111,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize } type normalizedRecord struct { - event dnd.ItemOccurrence + occurrence dnd.ItemOccurrence inputIndex int } @@ -122,44 +122,44 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord records := make([]normalizedRecord, len(input.Occurrences)) warnings := make([]contracts.Warning, 0) - for index, inputEvent := range input.Occurrences { - event, changedFields, found, refsChanged := normalizeEvent(inputEvent, order, registry) - records[index] = normalizedRecord{event: event, inputIndex: index} + for index, inputOccurrence := range input.Occurrences { + occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry) + records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index} if len(changedFields) != 0 { warnings = append(warnings, contracts.Warning{ - Scope: eventScope(index), + Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized, Message: fmt.Sprintf("input index %d: normalized display whitespace in %s", index, diagnostics.Aggregate("fields", changedFields)), }) } - if found && inputEvent.Name != event.Name { - warnings = append(warnings, contracts.Warning{Scope: eventScope(index), ReasonCode: ReasonCodeNameCanonicalized, - Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputEvent.Name), diagnostics.Quote(event.Name))}) + if found && inputOccurrence.Name != occurrence.Name { + warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized, + Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputOccurrence.Name), diagnostics.Quote(occurrence.Name))}) } if !found { - warnings = append(warnings, contracts.Warning{Scope: eventScope(index), ReasonCode: ReasonCodeUnknownItemID, - Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputEvent.ItemID))}) + warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID, + Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.ItemID))}) } if refsChanged { warnings = append(warnings, contracts.Warning{ - Scope: eventScope(index), + Scope: occurrenceScope(index), ReasonCode: ReasonCodeSourceRefsNormalized, Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)", - index, len(inputEvent.SourceRefs), len(event.SourceRefs)), + index, len(inputOccurrence.SourceRefs), len(occurrence.SourceRefs)), }) } } sort.SliceStable(records, func(left, right int) bool { - return itemeventmodel.Less(order, records[left].event, records[right].event) + return itemoccurrencemodel.Less(order, records[left].occurrence, records[right].occurrence) }) for position, record := range records { if position == record.inputIndex { continue } warnings = append(warnings, contracts.Warning{ - Scope: eventScope(record.inputIndex), + Scope: occurrenceScope(record.inputIndex), ReasonCode: ReasonCodeOccurrencesReordered, Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position), }) @@ -170,8 +170,8 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord return dnd.ItemOccurrenceList{Occurrences: output}, diagnostics.LimitWarnings(warnings, "item_occurrences", ReasonCodeWarningsOmitted) } -func normalizeEvent(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) { - output := cloneEvent(input) +func normalizeOccurrence(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) { + output := cloneOccurrence(input) canonical, found := registry.LookupID(input.ItemID) if found { output.Name = canonical.Name @@ -184,17 +184,17 @@ func normalizeEvent(input dnd.ItemOccurrence, order shared.SourceRefOrder, regis {name: "from", value: &output.From}, {name: "to", value: &output.To}, } { - trimmed := itemeventmodel.DisplayValue(*field.value) + trimmed := itemoccurrencemodel.DisplayValue(*field.value) if *field.value != trimmed { *field.value = trimmed changedFields = append(changedFields, field.name) } } output.SourceRefs = order.Canonicalize(input.SourceRefs) - return output, changedFields, found, !itemeventmodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs) + return output, changedFields, found, !itemoccurrencemodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs) } -func cloneEvent(input dnd.ItemOccurrence) dnd.ItemOccurrence { +func cloneOccurrence(input dnd.ItemOccurrence) dnd.ItemOccurrence { output := input if input.Quantity != nil { quantity := *input.Quantity @@ -220,11 +220,11 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex, groups := make([]duplicateGroup, 0) groupByKey := make(map[string]int) for recordIndex, record := range records { - if !itemeventmodel.ValidSourceRefs(index, record.event.SourceRefs) { + if !itemoccurrencemodel.ValidSourceRefs(index, record.occurrence.SourceRefs) { keep[recordIndex] = true continue } - key := itemeventmodel.ExactIdentity(order, record.event) + key := itemoccurrencemodel.ExactIdentity(order, record.occurrence) groupIndex, exists := groupByKey[key] if !exists { groupByKey[key] = len(groups) @@ -238,7 +238,7 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex, output := make([]dnd.ItemOccurrence, 0, len(records)) for recordIndex, record := range records { if keep[recordIndex] { - output = append(output, cloneEvent(record.event)) + output = append(output, cloneOccurrence(record.occurrence)) } } warnings := make([]contracts.Warning, 0) @@ -256,14 +256,14 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning { issues[index] = fmt.Sprintf("removed input index %d", removedIndex) } return contracts.Warning{ - Scope: eventScope(retainedIndex), + Scope: occurrenceScope(retainedIndex), ReasonCode: ReasonCodeDuplicateCollapsed, Message: diagnostics.Aggregate( fmt.Sprintf("duplicate item occurrence collapsed; retained input index %d", retainedIndex), issues), } } -func eventScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) } +func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ diff --git a/internal/modules/dnd/normalize/itemevents/normalizer_test.go b/internal/modules/dnd/normalize/itemoccurrences/normalizer_test.go similarity index 99% rename from internal/modules/dnd/normalize/itemevents/normalizer_test.go rename to internal/modules/dnd/normalize/itemoccurrences/normalizer_test.go index 29fb571..ff732cd 100644 --- a/internal/modules/dnd/normalize/itemevents/normalizer_test.go +++ b/internal/modules/dnd/normalize/itemoccurrences/normalizer_test.go @@ -1,4 +1,4 @@ -package itemevents +package itemoccurrences import ( "context" diff --git a/internal/modules/dnd/register/chains.go b/internal/modules/dnd/register/chains.go index 1a5d4ce..c94b762 100644 --- a/internal/modules/dnd/register/chains.go +++ b/internal/modules/dnd/register/chains.go @@ -4,7 +4,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" - itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" + itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences" itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry" @@ -14,7 +14,7 @@ import ( spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" - itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" + itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences" itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry" locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences" locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry" @@ -31,11 +31,11 @@ import ( enemyeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/shape" enemyeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_refs" enemyeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_relatedness" - itemeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/invariants" - itemeventregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/registry" - 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" + itemoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/invariants" + itemoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/registry" + itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape" + itemoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_refs" + itemoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_relatedness" itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/identity" itemshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/shape" itemrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/source_refs" @@ -185,29 +185,29 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error { {name: "item occurrences validator chain", register: func() error { return registry.Register(pipeline.ValidatorChainMapping{ Stage: pipeline.StageExtract, - Module: itemeventextract.Key, + Module: itemoccurrenceextract.Key, Validators: []pipeline.ModuleBinding{ pipeline.Binding(validjson.Key), - pipeline.Binding(itemeventshape.Key), - pipeline.Binding(itemeventregistry.Key), - pipeline.Binding(itemeventrefs.Key), + pipeline.Binding(itemoccurrenceshape.Key), + pipeline.Binding(itemoccurrenceregistry.Key), + pipeline.Binding(itemoccurrencerefs.Key), pipeline.Binding(validjsonschema.Key), - pipeline.Binding(itemeventrelatedness.Key), + pipeline.Binding(itemoccurrencerelatedness.Key), }, }) }}, {name: "item occurrences normalize validator chain", register: func() error { return registry.Register(pipeline.ValidatorChainMapping{ Stage: pipeline.StageNormalize, - Module: itemeventnormalize.Key, + Module: itemoccurrencenormalize.Key, Validators: []pipeline.ModuleBinding{ pipeline.Binding(validjson.Key), - pipeline.Binding(itemeventshape.Key), - pipeline.Binding(itemeventregistry.Key), - pipeline.Binding(itemeventinvariants.Key), - pipeline.Binding(itemeventrefs.Key), + pipeline.Binding(itemoccurrenceshape.Key), + pipeline.Binding(itemoccurrenceregistry.Key), + pipeline.Binding(itemoccurrenceinvariants.Key), + pipeline.Binding(itemoccurrencerefs.Key), pipeline.Binding(validjsonschema.Key), - pipeline.Binding(itemeventrelatedness.Key), + pipeline.Binding(itemoccurrencerelatedness.Key), }, }) }}, diff --git a/internal/modules/dnd/register/modules.go b/internal/modules/dnd/register/modules.go index 66dd4e6..3303fa1 100644 --- a/internal/modules/dnd/register/modules.go +++ b/internal/modules/dnd/register/modules.go @@ -7,7 +7,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns" enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents" - itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents" + itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences" itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry" locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences" locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry" @@ -17,7 +17,7 @@ import ( spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" - itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" + itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences" itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry" @@ -27,7 +27,7 @@ import ( spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" - itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" + itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences" itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry" locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences" locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry" @@ -47,7 +47,9 @@ func registerModules(registries pipeline.Registries) error { {name: "npc registry codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, npccodec.New()) }}, {name: "combat turns codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, combatcodec.New()) }}, {name: "enemy events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, enemyeventcodec.New()) }}, - {name: "item occurrences codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemeventcodec.New()) }}, + {name: "item occurrences codec", register: func() error { + return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemoccurrencecodec.New()) + }}, {name: "item registry codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemregistrycodec.New()) }}, @@ -64,7 +66,7 @@ func registerModules(registries pipeline.Registries) error { {name: "npc registry extractor", register: func() error { return npcextract.Register(registries.Extractors) }}, {name: "combat turns extractor", register: func() error { return combatextract.Register(registries.Extractors) }}, {name: "enemy events extractor", register: func() error { return enemyeventextract.Register(registries.Extractors) }}, - {name: "item occurrences extractor", register: func() error { return itemeventextract.Register(registries.Extractors) }}, + {name: "item occurrences extractor", register: func() error { return itemoccurrenceextract.Register(registries.Extractors) }}, {name: "item registry extractor", register: func() error { return itemregistryextract.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) }}, @@ -104,7 +106,7 @@ func registerModules(registries pipeline.Registries) error { {name: "npc registry normalizer", register: func() error { return npcnormalize.Register(registries.Normalizers) }}, {name: "combat turns normalizer", register: func() error { return combatnormalize.Register(registries.Normalizers) }}, {name: "enemy events normalizer", register: func() error { return enemyeventnormalize.Register(registries.Normalizers) }}, - {name: "item occurrences normalizer", register: func() error { return itemeventnormalize.Register(registries.Normalizers) }}, + {name: "item occurrences normalizer", register: func() error { return itemoccurrencenormalize.Register(registries.Normalizers) }}, {name: "item registry normalizer", register: func() error { return itemregistrynormalize.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) }}, @@ -152,7 +154,7 @@ func registerPromptAssets(assets *llm.AssetRegistry) error { {name: "npc normalization prompt assets", register: func() error { return npcnormalize.RegisterPromptAssets(assets) }}, {name: "combat turns prompt assets", register: func() error { return combatextract.RegisterPromptAssets(assets) }}, {name: "enemy events prompt assets", register: func() error { return enemyeventextract.RegisterPromptAssets(assets) }}, - {name: "item occurrences prompt assets", register: func() error { return itemeventextract.RegisterPromptAssets(assets) }}, + {name: "item occurrences prompt assets", register: func() error { return itemoccurrenceextract.RegisterPromptAssets(assets) }}, {name: "item registry prompt assets", register: func() error { return itemregistryextract.RegisterPromptAssets(assets) }}, {name: "item registry normalization prompt assets", register: func() error { return itemregistrynormalize.RegisterPromptAssets(assets) }}, {name: "npc occurrences prompt assets", register: func() error { return occurrenceextract.RegisterPromptAssets(assets) }}, diff --git a/internal/modules/dnd/register/prompt_cache_test.go b/internal/modules/dnd/register/prompt_cache_test.go index ee4826d..9308c6f 100644 --- a/internal/modules/dnd/register/prompt_cache_test.go +++ b/internal/modules/dnd/register/prompt_cache_test.go @@ -10,7 +10,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" - itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" + itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences" itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry" @@ -51,7 +51,7 @@ func TestExtractionPromptComposition(t *testing.T) { }{ {name: "npcs", promptID: npcextract.PromptID, promptVersion: npcextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, {name: "locations", promptID: locationextract.PromptID, promptVersion: locationextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, - {name: "item occurrences", promptID: itemeventextract.PromptID, promptVersion: itemeventextract.SchemaVersion, inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ + {name: "item occurrences", promptID: itemoccurrenceextract.PromptID, promptVersion: itemoccurrenceextract.SchemaVersion, inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{ "item_registry": promptkit.Inline(`{"items":[{"id":"item-registry-sentinel","name":"Torch"}]}`), }), suffixGroups: [][]string{{evidenceSentinel}, {"item-registry-sentinel"}}}, {name: "item registry", promptID: itemregistryextract.PromptID, promptVersion: itemregistryextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, diff --git a/internal/modules/dnd/register/register_test.go b/internal/modules/dnd/register/register_test.go index c8f62ce..40aa289 100644 --- a/internal/modules/dnd/register/register_test.go +++ b/internal/modules/dnd/register/register_test.go @@ -14,7 +14,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns" enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents" - itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents" + itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences" itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry" locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry" @@ -24,7 +24,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns" enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents" - itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents" + itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences" itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry" locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences" locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry" @@ -53,7 +53,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "dnd.npc_registry/prompt.yaml", "dnd.combat_turns/prompt.yaml", "dnd.enemy_events/prompt.yaml", - "dnd.item_events/prompt.yaml", + "dnd.item_occurrences/prompt.yaml", "dnd.item_registry/prompt.yaml", "dnd.item_registry.normalize/prompt.yaml", "dnd.npc_occurrences/prompt.yaml", @@ -86,8 +86,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, itemregistryextract.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, itemregistrynormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule}) + assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemoccurrenceextract.Key, itemregistryextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key}) + assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemoccurrencenormalize.Key, itemregistrynormalize.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.ItemOccurrenceListKind, dnd.ItemRegistryKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemOccurrenceListKind, dnd.ItemRegistryKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemOccurrenceListKind, dnd.ItemRegistryKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) @@ -95,7 +95,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) { assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind}) 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.ItemOccurrenceListKind}) + assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemregistrynormalize.Key), []contracts.ArtifactKind{dnd.ItemRegistryKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(occurrencenormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(scenedescriptionnormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind}) @@ -119,10 +119,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) { "extract/dnd/enemy-events/source_refs", "extract/dnd/enemy-events/source_relatedness", "normalize/dnd/enemy-events/invariants", - "extract/dnd/item-events/shape", - "extract/dnd/item-events/source_refs", - "extract/dnd/item-events/source_relatedness", - "normalize/dnd/item-events/invariants", + "extract/dnd/item-occurrences/shape", + "extract/dnd/item-occurrences/source_refs", + "extract/dnd/item-occurrences/source_relatedness", + "normalize/dnd/item-occurrences/invariants", "extract/dnd/item-registry/shape", "normalize/dnd/item-registry/identity", "extract/dnd/item-registry/source_refs", "extract/dnd/item-registry/source_relatedness", "extract/dnd/npc-occurrences/shape", "extract/dnd/npc-occurrences/registry", @@ -232,25 +232,25 @@ func TestRegisterAddsDNDFamily(t *testing.T) { } itemOccurrenceExtractChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), - pipeline.Binding("extract/dnd/item-events/shape"), - pipeline.Binding("extract/dnd/item-events/registry"), - pipeline.Binding("extract/dnd/item-events/source_refs"), + pipeline.Binding("extract/dnd/item-occurrences/shape"), + pipeline.Binding("extract/dnd/item-occurrences/registry"), + pipeline.Binding("extract/dnd/item-occurrences/source_refs"), pipeline.Binding("generic/valid_json_schema"), - pipeline.Binding("extract/dnd/item-events/source_relatedness"), + pipeline.Binding("extract/dnd/item-occurrences/source_relatedness"), } itemOccurrenceNormalizeChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), - pipeline.Binding("extract/dnd/item-events/shape"), - pipeline.Binding("extract/dnd/item-events/registry"), - pipeline.Binding("normalize/dnd/item-events/invariants"), - pipeline.Binding("extract/dnd/item-events/source_refs"), + pipeline.Binding("extract/dnd/item-occurrences/shape"), + pipeline.Binding("extract/dnd/item-occurrences/registry"), + pipeline.Binding("normalize/dnd/item-occurrences/invariants"), + pipeline.Binding("extract/dnd/item-occurrences/source_refs"), pipeline.Binding("generic/valid_json_schema"), - pipeline.Binding("extract/dnd/item-events/source_relatedness"), + pipeline.Binding("extract/dnd/item-occurrences/source_relatedness"), } - if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemeventextract.Key); !reflect.DeepEqual(got, itemOccurrenceExtractChain) { + if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemoccurrenceextract.Key); !reflect.DeepEqual(got, itemOccurrenceExtractChain) { t.Fatalf("item occurrence extract validator chain = %#v, want %#v", got, itemOccurrenceExtractChain) } - if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemeventnormalize.Key); !reflect.DeepEqual(got, itemOccurrenceNormalizeChain) { + if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemoccurrencenormalize.Key); !reflect.DeepEqual(got, itemOccurrenceNormalizeChain) { t.Fatalf("item occurrence normalize validator chain = %#v, want %#v", got, itemOccurrenceNormalizeChain) } itemRegistryExtractChain := []pipeline.ModuleBinding{ @@ -370,8 +370,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) { if !npcRegistrySlot.Required || !enemyNormalizeRegistrySlot.Required || npcRegistrySlot.MaxBytes != enemyNormalizeRegistrySlot.MaxBytes || !reflect.DeepEqual(npcRegistrySlot.AcceptedMediaTypes, enemyNormalizeRegistrySlot.AcceptedMediaTypes) || !reflect.DeepEqual(npcRegistrySlot.AcceptedArtifactKinds, enemyNormalizeRegistrySlot.AcceptedArtifactKinds) { t.Fatalf("enemy-event NPC registry slots disagree: %#v / %#v", enemyEventExtractSpec.ReferenceSlots, enemyEventNormalizeSpec.ReferenceSlots) } - itemOccurrenceExtractSpec, itemOccurrenceExtractOK := registries.Extractors.Spec(itemeventextract.Key) - itemOccurrenceNormalizeSpec, itemOccurrenceNormalizeOK := registries.Normalizers.Spec(itemeventnormalize.Key) + itemOccurrenceExtractSpec, itemOccurrenceExtractOK := registries.Extractors.Spec(itemoccurrenceextract.Key) + itemOccurrenceNormalizeSpec, itemOccurrenceNormalizeOK := registries.Normalizers.Spec(itemoccurrencenormalize.Key) if !itemOccurrenceExtractOK || itemOccurrenceExtractSpec.ArtifactKind != dnd.ItemOccurrenceListKind || !itemOccurrenceNormalizeOK || itemOccurrenceNormalizeSpec.ArtifactKind != dnd.ItemOccurrenceListKind || itemOccurrenceNormalizeSpec.Stage != pipeline.StageNormalize { t.Fatalf("item occurrence specs = %#v / %#v, present = %t / %t", itemOccurrenceExtractSpec, itemOccurrenceNormalizeSpec, itemOccurrenceExtractOK, itemOccurrenceNormalizeOK) } diff --git a/internal/modules/dnd/register/validators.go b/internal/modules/dnd/register/validators.go index a216ded..10d13ff 100644 --- a/internal/modules/dnd/register/validators.go +++ b/internal/modules/dnd/register/validators.go @@ -12,11 +12,11 @@ import ( enemyeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/shape" enemyeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_refs" enemyeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_relatedness" - itemeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/invariants" - itemeventregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/registry" - 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" + itemoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/invariants" + itemoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/registry" + itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape" + itemoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_refs" + itemoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_relatedness" itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/identity" itemshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/shape" itemrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/source_refs" @@ -70,11 +70,11 @@ func registerValidators(registries pipeline.Registries) error { {name: "enemy event source references validator", register: func() error { return enemyeventrefs.Register(registries.Validators) }}, {name: "enemy event source relatedness validator", register: func() error { return enemyeventrelatedness.Register(registries.Validators) }}, {name: "enemy event normalized invariants validator", register: func() error { return enemyeventinvariants.Register(registries.Validators) }}, - {name: "item occurrence shape validator", register: func() error { return itemeventshape.Register(registries.Validators) }}, - {name: "item occurrence registry validator", register: func() error { return itemeventregistry.Register(registries.Validators) }}, - {name: "item occurrence source references validator", register: func() error { return itemeventrefs.Register(registries.Validators) }}, - {name: "item occurrence source relatedness validator", register: func() error { return itemeventrelatedness.Register(registries.Validators) }}, - {name: "item occurrence normalized invariants validator", register: func() error { return itemeventinvariants.Register(registries.Validators) }}, + {name: "item occurrence shape validator", register: func() error { return itemoccurrenceshape.Register(registries.Validators) }}, + {name: "item occurrence registry validator", register: func() error { return itemoccurrenceregistry.Register(registries.Validators) }}, + {name: "item occurrence source references validator", register: func() error { return itemoccurrencerefs.Register(registries.Validators) }}, + {name: "item occurrence source relatedness validator", register: func() error { return itemoccurrencerelatedness.Register(registries.Validators) }}, + {name: "item occurrence normalized invariants validator", register: func() error { return itemoccurrenceinvariants.Register(registries.Validators) }}, {name: "item shape validator", register: func() error { return itemshape.Register(registries.Validators) }}, {name: "item identity validator", register: func() error { return itemidentity.Register(registries.Validators) }}, {name: "item source references validator", register: func() error { return itemrefs.Register(registries.Validators) }}, diff --git a/internal/modules/dnd/validate/itemevents/invariants/validator.go b/internal/modules/dnd/validate/itemoccurrences/invariants/validator.go similarity index 70% rename from internal/modules/dnd/validate/itemevents/invariants/validator.go rename to internal/modules/dnd/validate/itemoccurrences/invariants/validator.go index 92a98b0..c16d244 100644 --- a/internal/modules/dnd/validate/itemevents/invariants/validator.go +++ b/internal/modules/dnd/validate/itemoccurrences/invariants/validator.go @@ -1,4 +1,4 @@ -// Package invariants validates normalized D&D item-event artifacts. +// Package invariants validates normalized D&D item-occurrence artifacts. package invariants import ( @@ -9,16 +9,16 @@ 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" - itemeventmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents" + itemoccurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" - itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" + itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape" ) const ( - Key = "normalize/dnd/item-events/invariants" - ReasonCode = "invalid_normalized_item_event_invariants" - policy = "dnd.item_events.normalize_invariants.v1" + Key = "normalize/dnd/item-occurrences/invariants" + ReasonCode = "invalid_normalized_item_occurrence_invariants" + policy = "dnd.item_occurrences.normalize_invariants.v1" ) type Options struct{} @@ -43,10 +43,10 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq return contracts.ValidationResult{Approved: true}, nil } -// Validate checks only invariants introduced by item-event normalization. +// Validate checks only invariants introduced by item-occurrence normalization. // Shape and source-reference failures remain owned by their earlier validators. func Validate(doc *source.SourceDocument, value dnd.ItemOccurrenceList) error { - if itemeventshape.Validate(value) != nil { + if itemoccurrenceshape.Validate(value) != nil { return nil } index := source.NewDocumentIndex(doc) @@ -63,44 +63,44 @@ func Validate(doc *source.SourceDocument, value dnd.ItemOccurrenceList) error { func issuesFor(order shared.SourceRefOrder, value dnd.ItemOccurrenceList) []string { issues := make([]string, 0) seenIdentity := make(map[string]int) - for eventIndex, event := range value.Occurrences { - prefix := fmt.Sprintf("occurrences[%d]", eventIndex) - if event.Name != itemeventmodel.DisplayValue(event.Name) { + for occurrenceIndex, occurrence := range value.Occurrences { + prefix := fmt.Sprintf("occurrences[%d]", occurrenceIndex) + if occurrence.Name != itemoccurrencemodel.DisplayValue(occurrence.Name) { issues = append(issues, prefix+".name is not display-normalized") } - if event.From != itemeventmodel.DisplayValue(event.From) { + if occurrence.From != itemoccurrencemodel.DisplayValue(occurrence.From) { issues = append(issues, prefix+".from is not display-normalized") } - if event.To != itemeventmodel.DisplayValue(event.To) { + if occurrence.To != itemoccurrencemodel.DisplayValue(occurrence.To) { issues = append(issues, prefix+".to is not display-normalized") } - for refIndex := 1; refIndex < len(event.SourceRefs); refIndex++ { - previous := event.SourceRefs[refIndex-1] - current := event.SourceRefs[refIndex] + for refIndex := 1; refIndex < len(occurrence.SourceRefs); refIndex++ { + previous := occurrence.SourceRefs[refIndex-1] + current := occurrence.SourceRefs[refIndex] if order.Less(current, previous) { issues = append(issues, fmt.Sprintf("%s.source_refs are not in canonical order at index %d", prefix, refIndex)) } else if current == previous { issues = append(issues, fmt.Sprintf("%s.source_refs[%d] duplicates the previous reference", prefix, refIndex)) } } - key := itemeventmodel.ExactIdentity(order, event) + key := itemoccurrencemodel.ExactIdentity(order, occurrence) if previous, exists := seenIdentity[key]; exists { issues = append(issues, fmt.Sprintf("%s duplicates item occurrence %d under normalized identity", prefix, previous)) } else { - seenIdentity[key] = eventIndex + seenIdentity[key] = occurrenceIndex } } - for eventIndex := 1; eventIndex < len(value.Occurrences); eventIndex++ { - if itemeventmodel.Less(order, value.Occurrences[eventIndex], value.Occurrences[eventIndex-1]) { - issues = append(issues, fmt.Sprintf("occurrences[%d] is out of canonical order", eventIndex)) + for occurrenceIndex := 1; occurrenceIndex < len(value.Occurrences); occurrenceIndex++ { + if itemoccurrencemodel.Less(order, value.Occurrences[occurrenceIndex], value.Occurrences[occurrenceIndex-1]) { + issues = append(issues, fmt.Sprintf("occurrences[%d] is out of canonical order", occurrenceIndex)) } } return issues } func sourceRefsValid(index source.DocumentIndex, value dnd.ItemOccurrenceList) bool { - for _, event := range value.Occurrences { - if !itemeventmodel.ValidSourceRefs(index, event.SourceRefs) { + for _, occurrence := range value.Occurrences { + if !itemoccurrencemodel.ValidSourceRefs(index, occurrence.SourceRefs) { return false } } diff --git a/internal/modules/dnd/validate/itemevents/invariants/validator_test.go b/internal/modules/dnd/validate/itemoccurrences/invariants/validator_test.go similarity index 98% rename from internal/modules/dnd/validate/itemevents/invariants/validator_test.go rename to internal/modules/dnd/validate/itemoccurrences/invariants/validator_test.go index 3b76afb..81ead55 100644 --- a/internal/modules/dnd/validate/itemevents/invariants/validator_test.go +++ b/internal/modules/dnd/validate/itemoccurrences/invariants/validator_test.go @@ -45,7 +45,7 @@ func TestValidateRejectsOwnedNormalizedInvariants(t *testing.T) { {name: "list order", mutate: func(value *dnd.ItemOccurrenceList) { value.Occurrences = []dnd.ItemOccurrence{value.Occurrences[1], value.Occurrences[0]} }, want: "out of canonical order"}, - {name: "duplicate event", mutate: func(value *dnd.ItemOccurrenceList) { + {name: "duplicate occurrence", mutate: func(value *dnd.ItemOccurrenceList) { value.Occurrences = append(value.Occurrences, value.Occurrences[0]) }, want: "duplicates item occurrence"}, } diff --git a/internal/modules/dnd/validate/itemevents/registry/validator.go b/internal/modules/dnd/validate/itemoccurrences/registry/validator.go similarity index 97% rename from internal/modules/dnd/validate/itemevents/registry/validator.go rename to internal/modules/dnd/validate/itemoccurrences/registry/validator.go index 8f2c5cc..2585df7 100644 --- a/internal/modules/dnd/validate/itemevents/registry/validator.go +++ b/internal/modules/dnd/validate/itemoccurrences/registry/validator.go @@ -10,11 +10,11 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" - occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" + occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape" ) const ( - Key = "extract/dnd/item-events/registry" + Key = "extract/dnd/item-occurrences/registry" ReasonCode = "invalid_item_occurrence_registry" policy = "dnd.item_occurrences.validator.registry.v1" ) diff --git a/internal/modules/dnd/validate/itemevents/registry/validator_test.go b/internal/modules/dnd/validate/itemoccurrences/registry/validator_test.go similarity index 100% rename from internal/modules/dnd/validate/itemevents/registry/validator_test.go rename to internal/modules/dnd/validate/itemoccurrences/registry/validator_test.go diff --git a/internal/modules/dnd/validate/itemevents/shape/validator.go b/internal/modules/dnd/validate/itemoccurrences/shape/validator.go similarity index 77% rename from internal/modules/dnd/validate/itemevents/shape/validator.go rename to internal/modules/dnd/validate/itemoccurrences/shape/validator.go index 1c041f8..db5a140 100644 --- a/internal/modules/dnd/validate/itemevents/shape/validator.go +++ b/internal/modules/dnd/validate/itemoccurrences/shape/validator.go @@ -1,4 +1,4 @@ -// Package shape validates required D&D item-event candidate fields. +// Package shape validates required D&D item-occurrence candidate fields. package shape import ( @@ -9,14 +9,14 @@ 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" - "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" ) const ( - Key = "extract/dnd/item-events/shape" - ReasonCode = "invalid_item_event_shape" - policy = "dnd.item_events.shape.v1" + Key = "extract/dnd/item-occurrences/shape" + ReasonCode = "invalid_item_occurrence_shape" + policy = "dnd.item_occurrences.shape.v1" ) type Options struct{} @@ -41,7 +41,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq return contracts.ValidationResult{Approved: true}, nil } -// Validate returns one bounded error for every owned item-event shape issue. +// Validate returns one bounded error for every owned item-occurrence shape issue. func Validate(value dnd.ItemOccurrenceList) error { issues := issuesFor(value) if len(issues) == 0 { @@ -55,23 +55,23 @@ func issuesFor(value dnd.ItemOccurrenceList) []string { return []string{"occurrences must be present"} } issues := make([]string, 0) - for index, event := range value.Occurrences { + for index, occurrence := range value.Occurrences { prefix := fmt.Sprintf("occurrences[%d]", index) - if strings.TrimSpace(event.ItemID) == "" { - issues = append(issues, prefix+".item_id must not be empty: "+diagnostics.Quote(event.ItemID)) + if strings.TrimSpace(occurrence.ItemID) == "" { + issues = append(issues, prefix+".item_id must not be empty: "+diagnostics.Quote(occurrence.ItemID)) } - if strings.TrimSpace(event.Name) == "" { - issues = append(issues, prefix+".name must not be empty: "+diagnostics.Quote(event.Name)) + if strings.TrimSpace(occurrence.Name) == "" { + issues = append(issues, prefix+".name must not be empty: "+diagnostics.Quote(occurrence.Name)) } - if !itemevents.SupportedKind(event.Kind) { - issues = append(issues, prefix+".kind is unsupported: "+diagnostics.Quote(string(event.Kind))) - } else if !itemevents.ValidHolderCombination(event.Kind, event.From, event.To) { - issues = append(issues, prefix+".from and .to are incompatible with "+diagnostics.Quote(string(event.Kind))) + if !itemoccurrences.SupportedKind(occurrence.Kind) { + issues = append(issues, prefix+".kind is unsupported: "+diagnostics.Quote(string(occurrence.Kind))) + } else if !itemoccurrences.ValidHolderCombination(occurrence.Kind, occurrence.From, occurrence.To) { + issues = append(issues, prefix+".from and .to are incompatible with "+diagnostics.Quote(string(occurrence.Kind))) } - if event.Quantity != nil && *event.Quantity < 1 { + if occurrence.Quantity != nil && *occurrence.Quantity < 1 { issues = append(issues, prefix+".quantity must be positive when present") } - if len(event.SourceRefs) == 0 { + if len(occurrence.SourceRefs) == 0 { issues = append(issues, prefix+".source_refs must contain at least one reference") } } diff --git a/internal/modules/dnd/validate/itemevents/shape/validator_test.go b/internal/modules/dnd/validate/itemoccurrences/shape/validator_test.go similarity index 96% rename from internal/modules/dnd/validate/itemevents/shape/validator_test.go rename to internal/modules/dnd/validate/itemoccurrences/shape/validator_test.go index 8277d5c..21eeb88 100644 --- a/internal/modules/dnd/validate/itemevents/shape/validator_test.go +++ b/internal/modules/dnd/validate/itemoccurrences/shape/validator_test.go @@ -13,7 +13,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" ) -func TestValidatorAcceptsEveryEventRuleAndNormalizableWhitespace(t *testing.T) { +func TestValidatorAcceptsEveryOccurrenceRuleAndNormalizableWhitespace(t *testing.T) { quantity := 1 value := dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{{ItemID: "item", Name: " Hidden Cache ", Kind: dnd.ItemOccurrenceKindDiscovered, From: " ", To: " ", SourceRefs: refs(1, 1)}, {ItemID: "item", Name: "Gold Pieces", Kind: dnd.ItemOccurrenceKindAcquired, Quantity: &quantity, To: " party ", SourceRefs: refs(2, 2)}, @@ -40,7 +40,7 @@ func TestValidatorRejectsOwnedSemanticBoundaries(t *testing.T) { value dnd.ItemOccurrenceList want string }{ - {"missing events", dnd.ItemOccurrenceList{}, "occurrences must be present"}, + {"missing occurrences", dnd.ItemOccurrenceList{}, "occurrences must be present"}, {"blank name", listWith(dnd.ItemOccurrence{ItemID: "item", Name: " \t", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: refs(1, 1)}), "name must not be empty"}, {"unsupported kind", listWith(dnd.ItemOccurrence{ItemID: "item", Name: "Ring", Kind: "unknown", SourceRefs: refs(1, 1)}), "kind is unsupported"}, {"discovered holder", listWith(dnd.ItemOccurrence{ItemID: "item", Name: "Ring", Kind: dnd.ItemOccurrenceKindDiscovered, From: "Aria", SourceRefs: refs(1, 1)}), "incompatible"}, @@ -99,8 +99,8 @@ func validList() dnd.ItemOccurrenceList { return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{{ItemID: "item", Name: "Ring", Kind: dnd.ItemOccurrenceKindAcquired, To: "party", SourceRefs: refs(1, 1)}}} } -func listWith(event dnd.ItemOccurrence) dnd.ItemOccurrenceList { - return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{event}} +func listWith(occurrence dnd.ItemOccurrence) dnd.ItemOccurrenceList { + return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{occurrence}} } func refs(start, end int) []source.SourceRef { diff --git a/internal/modules/dnd/validate/itemevents/source_refs/validator.go b/internal/modules/dnd/validate/itemoccurrences/source_refs/validator.go similarity index 85% rename from internal/modules/dnd/validate/itemevents/source_refs/validator.go rename to internal/modules/dnd/validate/itemoccurrences/source_refs/validator.go index 96b8f7e..344d0dc 100644 --- a/internal/modules/dnd/validate/itemevents/source_refs/validator.go +++ b/internal/modules/dnd/validate/itemoccurrences/source_refs/validator.go @@ -1,4 +1,4 @@ -// Package sourcerefs validates D&D item-event transcript evidence. +// Package sourcerefs validates D&D item-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" - itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" + itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape" ) const ( - Key = "extract/dnd/item-events/source_refs" - ReasonCode = "invalid_item_event_source_references" - policy = "dnd.item_events.source_refs.v1" + Key = "extract/dnd/item-occurrences/source_refs" + ReasonCode = "invalid_item_occurrence_source_references" + policy = "dnd.item_occurrences.source_refs.v1" ) type Options struct{} @@ -38,7 +38,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil { return contracts.ValidationResult{}, fmt.Errorf("item occurrence source-reference validator requires the current extraction chunk") } - if itemeventshape.Validate(req.Value) != nil { + if itemoccurrenceshape.Validate(req.Value) != nil { return contracts.ValidationResult{Approved: true}, nil } index := source.NewDocumentIndex(req.Source) @@ -47,14 +47,14 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq coverage = newChunkCoverage(req.Chunk) } issues := make([]string, 0) - for eventIndex, event := range req.Value.Occurrences { - for refIndex, ref := range event.SourceRefs { + for occurrenceIndex, occurrence := range req.Value.Occurrences { + for refIndex, ref := range occurrence.SourceRefs { if err := index.ValidateRef(ref); err != nil { - issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: %s", eventIndex, refIndex, diagnostics.Truncate(err.Error()))) + issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: %s", occurrenceIndex, refIndex, diagnostics.Truncate(err.Error()))) continue } if coverage != nil && !coverage.contains(req.Source, ref) { - issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: source reference is outside the current extraction chunk", eventIndex, refIndex)) + issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: source reference is outside the current extraction chunk", occurrenceIndex, refIndex)) } } } diff --git a/internal/modules/dnd/validate/itemevents/source_refs/validator_test.go b/internal/modules/dnd/validate/itemoccurrences/source_refs/validator_test.go similarity index 100% rename from internal/modules/dnd/validate/itemevents/source_refs/validator_test.go rename to internal/modules/dnd/validate/itemoccurrences/source_refs/validator_test.go diff --git a/internal/modules/dnd/validate/itemevents/source_relatedness/validator.go b/internal/modules/dnd/validate/itemoccurrences/source_relatedness/validator.go similarity index 75% rename from internal/modules/dnd/validate/itemevents/source_relatedness/validator.go rename to internal/modules/dnd/validate/itemoccurrences/source_relatedness/validator.go index dacbcd6..75681d0 100644 --- a/internal/modules/dnd/validate/itemevents/source_relatedness/validator.go +++ b/internal/modules/dnd/validate/itemoccurrences/source_relatedness/validator.go @@ -1,4 +1,4 @@ -// Package sourcerelatedness warns when item-event evidence does not mention its item. +// Package sourcerelatedness warns when item-occurrence evidence does not mention its item. 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" - itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape" + itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape" ) const ( - Key = "extract/dnd/item-events/source_relatedness" - WarningReasonCode = "item_event_source_unrelated" - OmittedReasonCode = "item_event_relatedness_warnings_omitted" - policy = "dnd.item_events.source_relatedness.v1" + Key = "extract/dnd/item-occurrences/source_relatedness" + WarningReasonCode = "item_occurrence_source_unrelated" + OmittedReasonCode = "item_occurrence_relatedness_warnings_omitted" + policy = "dnd.item_occurrences.source_relatedness.v1" ) type Options struct{} @@ -38,7 +38,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { // Validate performs advisory source grounding only. Shape and range failures // are intentionally left to their blocking owners. func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.ItemOccurrenceList]) (contracts.ValidationResult, error) { - if itemeventshape.Validate(req.Value) != nil { + if itemoccurrenceshape.Validate(req.Value) != nil { return contracts.ValidationResult{Approved: true}, nil } resolver, err := shared.NewCitationResolver(req.Source) @@ -47,20 +47,20 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq } warnings := make([]contracts.Warning, 0) - for index, event := range req.Value.Occurrences { - citedText, err := resolver.CitedText(event.SourceRefs) - if err != nil || shared.ContainsTokenSequence(citedText, event.Name) { + for index, occurrence := range req.Value.Occurrences { + citedText, err := resolver.CitedText(occurrence.SourceRefs) + if err != nil || shared.ContainsTokenSequence(citedText, occurrence.Name) { continue } warnings = append(warnings, contracts.Warning{ Scope: fmt.Sprintf("occurrences[%d]", index), ReasonCode: WarningReasonCode, - Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(event.Name)), + Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)), }) } return contracts.ValidationResult{ Approved: true, - Warnings: diagnostics.LimitWarnings(warnings, "item_events", OmittedReasonCode), + Warnings: diagnostics.LimitWarnings(warnings, "item_occurrences", OmittedReasonCode), }, nil } diff --git a/internal/modules/dnd/validate/itemevents/source_relatedness/validator_test.go b/internal/modules/dnd/validate/itemoccurrences/source_relatedness/validator_test.go similarity index 91% rename from internal/modules/dnd/validate/itemevents/source_relatedness/validator_test.go rename to internal/modules/dnd/validate/itemoccurrences/source_relatedness/validator_test.go index cbc5bc5..cd749d6 100644 --- a/internal/modules/dnd/validate/itemevents/source_relatedness/validator_test.go +++ b/internal/modules/dnd/validate/itemoccurrences/source_relatedness/validator_test.go @@ -49,13 +49,13 @@ func TestValidatorDefersMalformedAndUnreadableEvidence(t *testing.T) { func TestValidatorBoundsWarningsAndRegistersPolicy(t *testing.T) { count := diagnostics.MaxWarnings + 5 - events := make([]dnd.ItemOccurrence, count) - for index := range events { - events[index] = dnd.ItemOccurrence{ItemID: "item", Name: "missing item", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}} + occurrences := make([]dnd.ItemOccurrence, count) + for index := range occurrences { + occurrences[index] = dnd.ItemOccurrence{ItemID: "item", Name: "missing item", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}} } result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemOccurrenceList]{ Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "nothing useful"}}}, - Value: dnd.ItemOccurrenceList{Occurrences: events}, + Value: dnd.ItemOccurrenceList{Occurrences: occurrences}, }) if err != nil || !result.Approved || len(result.Warnings) != diagnostics.MaxWarnings || result.Warnings[len(result.Warnings)-1].ReasonCode != OmittedReasonCode { t.Fatalf("Validate() = %#v, %v", result, err) diff --git a/internal/modules/integration/dnd_itemevents_runner_test.go b/internal/modules/integration/dnd_itemoccurrences_runner_test.go similarity index 91% rename from internal/modules/integration/dnd_itemevents_runner_test.go rename to internal/modules/integration/dnd_itemoccurrences_runner_test.go index 0010307..e9a3004 100644 --- a/internal/modules/integration/dnd_itemevents_runner_test.go +++ b/internal/modules/integration/dnd_itemoccurrences_runner_test.go @@ -30,9 +30,9 @@ pipelines: lane: registry artifacts: occurrences: - extract: dnd/item-events + extract: dnd/item-occurrences merge: appendorder - normalize: dnd/item-events + normalize: dnd/item-occurrences `) if err != nil { t.Fatal(err) @@ -51,7 +51,7 @@ pipelines: - id: record-occurrences artifacts: occurrences: - extract: dnd/item-events + extract: dnd/item-occurrences `, "record-occurrences", "record-occurrences", 1)) if err != nil { t.Fatal(err) diff --git a/internal/modules/integration/testdata/seriatim_item_events_session.json b/internal/modules/integration/testdata/seriatim_item_occurrences_session.json similarity index 83% rename from internal/modules/integration/testdata/seriatim_item_events_session.json rename to internal/modules/integration/testdata/seriatim_item_occurrences_session.json index 3da707c..2fc5090 100644 --- a/internal/modules/integration/testdata/seriatim_item_events_session.json +++ b/internal/modules/integration/testdata/seriatim_item_occurrences_session.json @@ -1,7 +1,7 @@ { "metadata": { - "id": "item-events-session", - "title": "Synthetic D&D item-event session" + "id": "item-occurrences-session", + "title": "Synthetic D&D item-occurrence session" }, "segments": [ {"id": 1, "start": 0, "end": 4, "speaker": "DM", "text": "An ancient coin rests in the chest."},