chore(meta): register D-079–D-083, close Q-024/Q-025/Q-026, ticket Sprint 17 knowledge flow work

Workshop outputs from Knowledge Flow & NPC Information Boundaries workshop (2026-02-24):

Decisions registered in decisions/perception.md:
- D-079: Knowledge Grant Architecture (unified KnowledgeGranted event, Fact+Entity enum, ContentEntityRegistry)
- D-080: NPC-to-NPC Knowledge Propagation (transfer_npc_knowledge system, trust-tier gating, ToldBy source)
- D-081: Unprompted Disclosure Design (DisclosureCandidates component, two-stage trait filter, trigger gates)
- D-082: NPC Information Boundaries MVP Scope (tell_state + disclosure; pathfinding KG integration explicitly not planned)
- D-083: Contradiction Detection Pipeline (event-driven at observe_entity, ContradictionClaim struct, downstream chain)

Questions closed in decisions/questions.md:
- Q-024: closed by D-080 (immediate during conversation, not queued)
- Q-025: confirmed closed (gossip propagation ships Sprint 17; ~12MB peak, no cap needed before v0.3)
- Q-026: closed by D-083 (event-driven at KG write time, ContradictionClaim struct)

Tickets created (#545–#551, Sprint 17, team server):
- #545 KnowledgeGrant schema + ContentEntityRegistry (critical, blocks all downstream)
- #546 KnowledgeGranted event + process_knowledge_events handler
- #547 ContradictionClaim struct + detection in observe_entity
- #548 NPC-to-NPC knowledge transfer system
- #549 tell_state.rs KG awareness — MVP information boundary
- #550 Contradiction monologue + event chain completion
- #551 DisclosureCandidates + unprompted disclosure

Existing tickets updated: #141, #142 (sprint 17 + team assigned, descriptions updated); #172, #173 (full mechanical spec from D-081).

Workshop source files committed: all round docs + workshop-outcomes.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 01:59:58 +01:00
co-authored by Claude Sonnet 4.6
parent 0561387de9
commit f685cb7324
13 changed files with 4345 additions and 16 deletions
+49 -1
View File
@@ -398,6 +398,54 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Raised by:** Lead (passive panel directive, occlusion filter, monologue separation)
- **Dissent:** None
### D-079: Knowledge Grant Architecture
- **Date:** 2026-02-24
- **Decision:** All knowledge input (dialogue testimony, physical evidence, POI discovery, NPC-authored initial knowledge) flows through a single `KnowledgeEventType::KnowledgeGranted` event type. No separate event types for different grant sources. The `KnowledgeGrant` YAML schema is extended to an untagged enum supporting `Fact { fact_id, confidence }` and `Entity { entity_ref, attributes, confidence }` variants. The source field (`KnowledgeSource`) distinguishes provenance: `ToldBy { source_id: StableId, tick }` for NPC testimony, `DirectObservation { tick }` for physical evidence, `Heard { tick, range }` for overheard conversations. Grants fire at line selection time in `process_talk_interaction`, server-side, pushed to `KnowledgeEventQueue` and processed on the same tick. A `ContentEntityRegistry` resource (`BTreeMap<String, StableId>`, populated at NPC spawn time) resolves `entity_ref` strings to `StableId`s at grant processing time. Content-load validation enforces: confidence strings parse to valid `KnowledgeConfidence` variants; fact_ids conform to `"category.topic"` format; entity_refs resolve to registered StableIds. Runtime guardrail: if the granting NPC's KG does not contain the fact being granted, the grant is dropped with `tracing::warn!`.
- **Scope note — `Compound` grant variant:** `Compound { grants: Vec<KnowledgeGrant> }` (multiple grants from a single line) deferred to Sprint 18. Sprint 17 ships `Fact` and `Entity` variants only. Sprint 18 `EntityGrant { attributes: BTreeMap }` full variant is committed scope, not aspirational (Paula condition, 2026-02-24).
- **Rationale:** Unified event type keeps the knowledge system composable. Grant timing at line selection (not client display) ensures D-010 determinism — tick-stamped and server-authoritative. Entity grants are required for contradiction detection: testimony must create `EntityKnowledge` with `ToldBy` source so that a subsequent `DirectObservation` can detect a discrepancy. Physical evidence uses the same mechanism with `DirectObservation` source, which is treated as higher-confidence and cannot be contradicted by the NPC-denial path.
- **Raised by:** Knowledge Flow & NPC Information Boundaries Workshop — unanimous on grant timing and unified event type. Entity grants in Sprint 17 per team lead decision (2026-02-24), overriding Paula's Round 2 acceptance of Dudley's FactId workaround.
- **Dissent:** Paula (Round 2) accepted the FactId workaround with 3 binding conditions; team lead overrode in favour of entity grants path championed by Tyre, Gestalt, and Dudley. Paula's conditions honoured where applicable: (1) `ToldBy` source flows through `ContradictionDetected` event payload — satisfied by D-083; (2) Sprint 18 entity grants are committed scope; (3) formal record of this commitment.
- **Implements:** Tickets #545 (schema), #546 (event + handler)
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-010](architecture.md#d-010-multiplayer-ready-architectural-baseline), [D-083](#d-083-contradiction-detection-pipeline)
### D-080: NPC-to-NPC Knowledge Propagation
- **Date:** 2026-02-24
- **Decision:** NPC-to-NPC knowledge transfer is implemented via a `transfer_npc_knowledge` Bevy system that runs `after(run_npc_conversations)`. The system uses `kg_query.get_many_mut([entity_a, entity_b])` for dual-mutable KG access. Knowledge transfer occurs once per conversation at conversation start, not per-line. Rate: flat `rng.random_range(1..=3)` facts per conversation, drawn from eligible entries sorted by `last_updated_tick` descending (most recent first). Trust-gated filtering: `RelationshipEdge.trust` value determines eligible entries (None tier < 0: no transfer; Surface 03: Active facts at KnowsOf+ only; Real 37: facts at any confidence + entity observations; Secret 710: all Active entries). Confidence cap: `transferred_confidence = min(source_confidence, KnowledgeConfidence::KnowsOf)`. Facts with `disclosure_blocked: true` are never transferred regardless of trust tier. Source construction: `KnowledgeSource::ToldBy { source_id: speaker_sid, tick: time.tick }`. Player overhearing: when the player is within `VOICE_RANGE_TILES` (8) of a knowledge-transferring NPC pair, the player gains entity-level KG entries at `Suspects` confidence with `KnowledgeSource::Heard { tick, range: Medium }`. Specific overheard fact transfer to player deferred to Sprint 18.
- **Rationale:** Reuses `run_npc_conversations` proximity detection, deterministic StableId-sorted pairing, and conversation lifecycle — avoids building a separate propagation system. Confidence capping at `KnowsOf` prevents gossip chains from amplifying information. `ToldBy` source construction is the prerequisite for contradiction detection. The `disclosure_blocked` flag models secrets whose sharing is existentially dangerous regardless of trust (D-034 NPC design requirement).
- **Resolves:** Q-024
- **Raised by:** Workshop — unanimous
- **Dissent:** None
- **Implements:** Ticket #548
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-083](#d-083-contradiction-detection-pipeline), [D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter)
### D-081: Unprompted Disclosure Design
- **Date:** 2026-02-24
- **Decision:** Unprompted disclosure is implemented as a filtered KG query producing a `DisclosureCandidates` component, consumed by Layer 4 of the dialogue pipeline. The system runs as `derive_disclosure_candidates` (Active-tier NPCs within player range only; recomputed every 30 ticks via `computed_tick` freshness check). NPCs check their own KG only — no cross-entity KG queries (D-010 principle 2). Candidate selection filters: Active state only; confidence ≥ KnowsOf (Cautious trait raises threshold); not in `per_fact_history` (not already disclosed this cooldown period); `disclosure_blocked != true`. Trait two-stage filter: Stage 1 (what) filters candidate pool by trait-based predicates (Cautious raises confidence floor, Gossipy lowers it, Loyal suppresses facts about protected entities, Talkative overrides witness inhibition). Stage 2 (how) biases line pool scoring in Layer 4. Trigger gates (all must pass): trust tier ≥ Surface toward player; mood not Hostile; contentment ≥ 10; candidates not empty; witness inhibition (no non-trusted NPCs within 5 tiles, OR NPC-player trust = Secret tier, OR Talkative trait override); location privacy (`disclosure_context` tag compatible with current zone type: `private`/`semi_private`/`any`); per-NPC cooldown (300 ticks / 30 game-minutes) not active; global rate limit not exceeded. Global rate limit: 1 disclosure per 10 ticks maximum across all NPCs; when multiple candidates ready in same window, select by ascending StableId (deterministic, D-010). Rate limiting layers: (1) per-fact per-NPC `per_fact_history` — primary; (2) per-NPC 300-tick cooldown — secondary; (3) global 1/10-tick cap — tertiary.
- **Rationale:** NPCs checking only their own KG (Option A) is both diegetically correct and mechanically richer: NPCs can say things the player already knows, which creates dramatic irony. Option B (NPC checks player's KG) would collapse this asymmetry at exactly the moments their speech would be most dramatically charged. Separate `DisclosureCandidates` component prevents DerivedTellState serialization bloat. The location privacy gate creates learnable spatial behavior patterns. The per-fact `per_fact_history` is the primary narrative quality gate.
- **Raised by:** Workshop — unanimous on architecture; Paula and Gestalt on trigger gates; Tyre and Dudley on implementation structure
- **Dissent:** Gestalt (Round 1) opposed global rate limit as creating invisible NPC competition. Resolved in Round 2 via deterministic StableId selection. Included.
- **Implements:** Tickets #551, #172, #173
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-082](#d-082-npc-information-boundaries--mvp-scope), [D-034](content.md#d-034-the-friend-npc-archetype)
### D-082: NPC Information Boundaries — MVP Scope
- **Date:** 2026-02-24
- **Decision:** Sprint 17 MVP information boundary scope: (1) `tell_state.rs` reads NPC relationship state from KG for OTHER-entity state (replaces direct `relationships.entries` read for Friendly tell derivation); self-axis components (Secret, Contentment, Tolerance, Mood) continue as ground-truth reads. (2) Unprompted disclosure (D-081) uses KG for candidate selection. No other system retrofits in Sprint 17. `derive_tell_state` adds `Option<&KnowledgeGraph>` to query; `None` falls through to current axis-based behavior (existing tests continue passing). All KG-driven behavior scoped to `With<ActiveSim>` — Background-tier NPCs receive no KG-based boundary changes. Deferred: conversation partner KG-awareness check (Sprint 18 candidate, `kg.knows_entity(&partner_sid)` in pairing loop); routine KG awareness (v0.2+); pathfinding KG integration (not planned — failure mode has no safe recovery). Fallback on missing KG entry: fall through to ground truth with `tracing::debug!` structured logging.
- **Rationale:** The minimum viable boundary produces maximum gameplay-visible difference for minimum risk. Pathfinding from KG has an unresolvable failure mode (NPC forgets path nodes = stuck NPCs = undefined movement behavior) and must not be implemented. Self-knowledge always comes from axis components because the NPC is the continuous observer of its own state. The `Option<&KnowledgeGraph>` addition is backward-compatible.
- **Raised by:** Workshop — unanimous
- **Dissent:** None. Paula adds that `tell_state` should eventually incorporate KG-derived secret-exposure intensity — deferred to a future sprint as enhancement.
- **Implements:** Tickets #549 (step 1), #551 (step 2), ticket #142
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-081](#d-081-unprompted-disclosure-design), [D-026](architecture.md#d-026-npc-simulation-tier-system)
### D-083: Contradiction Detection Pipeline
- **Date:** 2026-02-24
- **Decision:** Contradiction detection is event-driven at KG write time — fires inside `observe_entity()` (`server/src/knowledge/graph.rs`) before overwriting an existing entry. Detection algorithm: if the existing entry has `source: ToldBy { tick: told_tick }` AND `last_known_position` differs from incoming position AND `|current_tick - told_tick| < CONTRADICTION_WINDOW_TICKS` (default 600; configurable per contradiction type), then: (1) `entry.state = KnowledgeState::Contradicted`; (2) `entry.contradicted_claim = Some(ContradictionClaim { source: entry.source.clone(), claimed_position: entry.last_known_position, detected_at_tick: current_tick })`; (3) proceed to write new observation. A `ContradictionDetected { observer, entity_sid, source_display_name: Option<String>, subject_display_name: Option<String> }` event is pushed; display names are resolved at detection time via `EntityRegistry + NpcName` so the monologue system is a pure string consumer. `EntityKnowledge` gains one new field: `pub contradicted_claim: Option<ContradictionClaim>` with `#[serde(skip_serializing_if = "Option::is_none")]` and `#[serde(default)]` — backward-compatible, zero migration required. Downstream event chain: `ContradictionDetected` → monologue system (named contradiction line) → relationship shift (`ToldBy` source entity → `PersonOfInterest`) → D-033 amber via existing pipeline → `AnomalyMarker` via `detect_anomalies` (already tested). Both the `ToldBy` entry and the `DirectObservation` entry receive `Contradicted` state (epistemic neutrality). Location contradiction is automatic (Sprint 17). Attribute contradiction uses content-authored YAML pairs (Sprint 18). Fact contradiction uses content-authored `contradicts` field on KnowledgeGrant entries (Sprint 18). `CONTRADICTION_WINDOW_TICKS = 600` (1 game-hour) as default constant, with content-authored override per claim type.
- **Rationale:** Event-driven detection avoids per-tick KG scan overhead (99% of ticks have no new information). The `ContradictionClaim` struct (typed) was chosen over string encoding in `known_attributes`: the struct is accessed with direct field reads (no string parsing in the detection hot path); the monologue system needs a typed `KnowledgeSource` to resolve the source entity's name; string encoding in `known_attributes` mixes semantic metadata with internal bookkeeping; one optional struct field is additive (no breaking changes, no migration). Steps 25 of the downstream chain (anomaly marker, D-033 color, relationship shift, monologue selection) are already implemented and passing tests. Contradiction detection is the missing link that enables THE FRIEND arc.
- **Resolves:** Q-026
- **Raised by:** Workshop — unanimous on architecture and downstream chain; Tyre and Gestalt on struct approach; Dudley conceded Option B (string encoding).
- **Dissent:** Dudley (Round 1): Option B string encoding as Sprint 17 workaround. Conceded in Round 2. No standing dissent.
- **Implements:** Tickets #547 (struct + detection), #550 (monologue + event chain)
- **Cross-reference:** [D-034](content.md#d-034-the-friend-npc-archetype), [D-033](perception.md#d-033-entity-color--relationship-to-player), [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-079](#d-079-knowledge-grant-architecture), [D-080](#d-080-npc-to-npc-knowledge-propagation)
---
*32 decisions. Last updated: 2026-02-20 (D-061: amended with unified conversation log architecture from Sprint 14 #535)*
*37 decisions. Last updated: 2026-02-24 (D-079D-083: Knowledge Flow & NPC Information Boundaries Workshop)*