Move item occurrences to canonical namespace
This commit is contained in:
@@ -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.
|
||||
@@ -1,4 +1,4 @@
|
||||
id: dnd.item_events
|
||||
id: dnd.item_occurrences
|
||||
version: "v1"
|
||||
default_profile: dnd-extraction
|
||||
inputs:
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}]}]}`)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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":[]}`)
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
@@ -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{
|
||||
@@ -1,4 +1,4 @@
|
||||
package itemevents
|
||||
package itemoccurrences
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package itemevents
|
||||
package itemoccurrences
|
||||
|
||||
type extractionResponse struct {
|
||||
Occurrences []itemOccurrenceResponse `json:"occurrences"`
|
||||
@@ -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, ".")
|
||||
}
|
||||
@@ -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"}]}`),
|
||||
@@ -1,4 +1,4 @@
|
||||
package itemevents
|
||||
package itemoccurrences
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -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"
|
||||
@@ -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{},
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
@@ -1,4 +1,4 @@
|
||||
package itemevents
|
||||
package itemoccurrences
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
}},
|
||||
|
||||
@@ -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) }},
|
||||
|
||||
@@ -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}}},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) }},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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."},
|
||||
Reference in New Issue
Block a user