diff --git a/assets/dnd/npc-occurrences/prompts/instructions.md b/assets/dnd/npc-occurrences/prompts/instructions.md index f12ada8..7fd75dd 100644 --- a/assets/dnd/npc-occurrences/prompts/instructions.md +++ b/assets/dnd/npc-occurrences/prompts/instructions.md @@ -1,4 +1,4 @@ -Extract Dungeons & Dragons NPC occurrence occurrences from the supplied +Extract Dungeons & Dragons NPC occurrences from the supplied transcript. Include an occurrence only when the transcript establishes one supplied NPC, one occurrence kind, and a coherent passage supporting both. Use the exact `npc_id` and matching `name` pair from the supplied NPC registry; diff --git a/assets/dnd/npc-registry/extract/prompts/instructions.md b/assets/dnd/npc-registry/extract/prompts/instructions.md index 32ccf92..4a7e4df 100644 --- a/assets/dnd/npc-registry/extract/prompts/instructions.md +++ b/assets/dnd/npc-registry/extract/prompts/instructions.md @@ -1,15 +1,19 @@ Extract the individually identifiable Dungeons & Dragons non-player characters established by the provided transcript. -Include an in-world non-PC participant only when the transcript gives it a -proper name or a stable, individually distinguishing title or alias. +Include an in-world non-PC only when the transcript factually establishes a +proper name or a stable, individually distinguishing title or alias. A factual +third-party mention establishes that identity even when the NPC is not +physically present, does not speak, and takes no direct action in this chunk. +Record only the NPC identity and the transcript evidence that establishes it; +do not infer or classify a separate occurrence. Exclude human players, transcript speakers, and the GM as out-of-world people; -player characters identified by the player or party references; incidental or -hypothetical name drops; corrected transcription mistakes; anonymous or generic -roles; indistinguishable crowds or groups; invented descriptive labels; and -temporary summoned creatures or spell effects without a persistent individual -identity. +player characters identified by the player or party references; names used only +in hypothetical, speculative, or imagined examples; corrected transcription +mistakes; anonymous or generic roles; indistinguishable crowds or groups; +invented descriptive labels; and temporary summoned creatures or spell effects +without a persistent individual identity. Preserve observed display spelling. Do not invent a label for an anonymous creature, crowd, or generic role. diff --git a/docs/integrations/dnd-npc-occurrence-artifacts.md b/docs/integrations/dnd-npc-occurrence-artifacts.md index 58d4ff3..4abf947 100644 --- a/docs/integrations/dnd-npc-occurrence-artifacts.md +++ b/docs/integrations/dnd-npc-occurrence-artifacts.md @@ -71,6 +71,9 @@ The required normalized [NPC registry artifact](dnd-npc-registry-artifacts.md) r the exact `{npc_id, name}` pair. Unknown IDs and names that do not match their ID are rejected; normalization does not repair names by similarity. Registry references are provenance only and never replace an occurrence's own evidence. +The registry may include an identity established by a factual third-party +mention; that provenance alone does not create a `mentioned` occurrence. Each +occurrence remains a separately cited fact in the current transcript. Normalization validates the exact pair, orders and deduplicates exact source references, then orders occurrences by valid source chronology, NPC comparison identity, display name, kind, and reference sequence. diff --git a/docs/integrations/dnd-npc-registry-artifacts.md b/docs/integrations/dnd-npc-registry-artifacts.md index b3c585e..eb5da55 100644 --- a/docs/integrations/dnd-npc-registry-artifacts.md +++ b/docs/integrations/dnd-npc-registry-artifacts.md @@ -68,10 +68,16 @@ descriptor. ## Scope and consumers Only individually identifiable NPC names with transcript evidence belong in -this artifact. Groups, generic roles, invented labels, and descriptive -enrichment are excluded. Its source references prove registry provenance; they -do not become evidence for a spell, occurrence, combat, or enemy-event -occurrence. +this artifact. A factual third-party mention can establish an identity even if +the NPC is not present, speaking, or acting in the cited passage. Names used +only in hypothetical, speculative, or imagined examples are excluded, as are +groups, generic roles, invented labels, and descriptive enrichment. Its source +references prove registry provenance; they do not become evidence for a spell, +occurrence, combat, or enemy-event occurrence. + +Registry evidence establishes an identity, not an [NPC occurrence](dnd-npc-occurrence-artifacts.md). +That later artifact independently records any current-transcript occurrence +with its own cited evidence and category. This registry can ground actor or caster names in the [spell](dnd-spell-artifacts.md) and [combat-turn](dnd-combat-turn-artifacts.md) artifacts. It is required to diff --git a/docs/internal/dnd.md b/docs/internal/dnd.md index cdf6037..e4b024f 100644 --- a/docs/internal/dnd.md +++ b/docs/internal/dnd.md @@ -168,11 +168,11 @@ checkpoint fingerprint. | Lane | Intentional behavior | | --- | --- | | Spells | May use a spell-catalog overlay and optional NPC grounding; the catalog validator supplies domain-specific semantic checks. | -| NPC registry | Does not consume an NPC registry. Its normalizer is the LLM-assisted reconciliation exception described above. | +| NPC registry | Establishes transcript-grounded NPC identities, including factual third-party mentions, without assigning occurrence categories. It does not consume an NPC registry, and 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 occurrences | Requires the normalized item registry for exact ID/name grounding at extraction and normalization. Campaign context may disambiguate, but the registry never becomes occurrence evidence. | | 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. | +| NPC occurrences | Requires the normalized NPC registry at extraction and normalization, using it for canonical actor grounding only. It separately emits cited current-transcript occurrence facts, including `mentioned`, rather than deriving them from registry provenance. | | Scene descriptions | Produces the classifications consumed by combat routing; it does not consume an NPC registry or provide evidence for combat artifacts. | | Enemy events | Requires NPC, scene-description, combat-turn, and NPC-occurrence artifacts. It calls the LLM only for an exact `combat` classification, records ordered observations rather than terminal state, and normalizes recognized names through the NPC registry while preserving grounded collective labels. | | Location registry | Produces a source-anchored, session-scoped registry from stable proper names or unique in-world designations. Its LLM-assisted reconciliation is proposal-only and never collapses same-name places without validated identity and evidence rules. | diff --git a/internal/modules/dnd/extract/enemyevents/grounding_test.go b/internal/modules/dnd/extract/enemyevents/grounding_test.go index b428a07..a20ffa3 100644 --- a/internal/modules/dnd/extract/enemyevents/grounding_test.go +++ b/internal/modules/dnd/extract/enemyevents/grounding_test.go @@ -220,7 +220,7 @@ func groundingReferences(t *testing.T, enemy string, sceneKind dnd.SceneKind) co if err != nil { t.Fatal(err) } - interactionContent, err := occurrencecodec.New().Encode(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{ + occurrenceContent, err := occurrencecodec.New().Encode(dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{ {NPCID: identity.DeriveID(enemy), Name: enemy, Kind: dnd.NPCOccurrenceKindCombatOpponent, SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 1, EndUnitID: 1}}}, {NPCID: identity.DeriveID("Aria"), Name: "Aria", Kind: dnd.NPCOccurrenceKindCombatAlly, SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 2, EndUnitID: 2}}}, }}) @@ -231,7 +231,7 @@ func groundingReferences(t *testing.T, enemy string, sceneKind dnd.SceneKind) co NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCRegistryReferenceSlot, npcContent)}}, SceneDescriptionReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(SceneDescriptionReferenceSlot, sceneContent)}}, CombatTurnReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(CombatTurnReferenceSlot, turnContent)}}, - NPCOccurrenceReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCOccurrenceReferenceSlot, interactionContent)}}, + NPCOccurrenceReferenceSlot: {Items: []contracts.ReferenceItem{newReferenceItem(NPCOccurrenceReferenceSlot, occurrenceContent)}}, }} } diff --git a/internal/modules/dnd/validate/npcoccurrences/invariants/validator.go b/internal/modules/dnd/validate/npcoccurrences/invariants/validator.go index b27feca..8eab5ce 100644 --- a/internal/modules/dnd/validate/npcoccurrences/invariants/validator.go +++ b/internal/modules/dnd/validate/npcoccurrences/invariants/validator.go @@ -10,7 +10,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" - interactionmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcoccurrences" + occurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcoccurrences" npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" @@ -107,7 +107,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq func allSourceRefsValid(index source.DocumentIndex, value dnd.NPCOccurrenceList) bool { for _, occurrence := range value.Occurrences { - if !interactionmodel.ValidSourceRefs(index, occurrence.SourceRefs) { + if !occurrencemodel.ValidSourceRefs(index, occurrence.SourceRefs) { return false } } @@ -136,13 +136,13 @@ func issuesFor(order shared.SourceRefOrder, value dnd.NPCOccurrenceList, npcRegi } if !sort.SliceIsSorted(value.Occurrences, func(left, right int) bool { - return interactionmodel.Less(order, value.Occurrences[left], value.Occurrences[right]) + return occurrencemodel.Less(order, value.Occurrences[left], value.Occurrences[right]) }) { issues = append(issues, "occurrences are not in canonical order") } seen := make(map[string]int) for index, occurrence := range value.Occurrences { - key := interactionmodel.ExactIdentity(occurrence) + key := occurrencemodel.ExactIdentity(occurrence) if previous, ok := seen[key]; ok { issues = append(issues, fmt.Sprintf("occurrences[%d] duplicates occurrence %d", index, previous)) continue