Move item occurrences to canonical namespace

This commit is contained in:
2026-08-05 20:00:20 +00:00
parent 3dfefd0e14
commit a6e176e160
46 changed files with 342 additions and 337 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err)
}
if example.name == "complete" {
if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:item-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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
type extractionResponse struct {
Occurrences []itemOccurrenceResponse `json:"occurrences"`

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"reflect"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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