# Workshop Outcomes — Knowledge Flow & NPC Information Boundaries **Workshop date:** 2026-02-23 — 2026-02-24 **Documented by:** Qatux **Participants:** Tyre, Gestalt, Dudley, Paula **Source files:** round-1-notes.md, round-2-notes.md, tyre-round1/2.md, gestalt-round1/2.md, dudley-round1/2.md, paula-round1/2.md --- ## D-Record Drafts The following D-records are produced by this workshop. They are ready for registration in the appropriate decisions/ domain files. The D-numbers D-079 through D-083 are assigned sequentially after D-078 (current highest in perception.md). --- ### 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`, 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!`. - **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:** 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. See round-2-notes.md §Tension A for full workshop split and rationale. - **Dissent:** Paula (Round 2) accepted the FactId workaround with 3 binding conditions; team lead overrode in favour of the entity grants path championed by Tyre, Gestalt, and Dudley. Paula's conditions remain relevant where applicable: (1) `ToldBy` source must flow through `ContradictionDetected` event payload — satisfied by D-083 design; (2) Sprint 18 entity grants (full `EntityGrant { attributes: BTreeMap }` variant + `Compound` variant) are **committed scope**, not aspirational. Paula's Condition 3 is formally recorded here as a commitment. - **Scope note — `Compound` grant variant:** `Compound { grants: Vec }` (multiple grants from a single line) deferred to Sprint 18. Sprint 17 ships `Fact` and `Entity` variants only. --- ### 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 immediately after `run_npc_conversations` (system ordering: `transfer_npc_knowledge.after(run_npc_conversations)`). The system uses `kg_query.get_many_mut([entity_a, entity_b])` for dual-mutable KG access (required by Bevy ECS — two mutable borrows of the same component type cannot occur in a single query). 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 0–3: Active facts at KnowsOf+ only; Real 3–7: facts at any confidence + entity observations; Secret 7–10: 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:** The existing `run_npc_conversations` system already provides proximity detection, deterministic StableId-sorted pairing, and conversation lifecycle — reusing it avoids building a separate propagation system and keeps NPC conversations as the natural information-sharing hook. Confidence capping at `KnowsOf` prevents gossip chains from amplifying information; only direct observation produces `KnowsDetails` or `Direct`. `ToldBy` source construction is the prerequisite for contradiction detection (Topic 5). The `disclosure_blocked` flag models secrets whose sharing is existentially dangerous regardless of relationship trust (D-034 NPC design requirement). Closes Q-024. - **Raised by:** Workshop — unanimous - **Dissent:** None --- ### D-081: Unprompted Disclosure Design - **Date:** 2026-02-24 - **Decision:** Unprompted disclosure (ticket #172) 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). 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 candidate's `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, prevents repeats; (2) per-NPC 300-tick cooldown — prevents spam; (3) global 1/10-tick cap — prevents degenerate multi-NPC simultaneous fire. - **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 (Sera praising Kael's integrity after the player found discrepancies). Option B (NPC checks player's KG) would collapse this asymmetry — NPCs would go silent at exactly the moments their speech would be most dramatically charged. Separate `DisclosureCandidates` component (not added to `DerivedTellState`) prevents DerivedTellState serialization bloat and allows different update frequencies for different concerns. The location privacy gate creates learnable spatial behavior patterns: "Sera talks freely at Lera's; she clams up at The Terminal." The per-fact `per_fact_history` is the primary narrative quality gate — an NPC who repeats the same fact is a quest marker, not a person. Implements ticket #172. - **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: Paula withdrew support, Gestalt conditionally accepted with deterministic StableId selection. Included. --- ### 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` — Background-tier NPCs (D-026, 500–2000) 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: NPCs whose tells reflect their actual relationship knowledge (not just raw axes) and NPCs who only volunteer what they know. 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 information boundary applies to external knowledge only. The `Option<&KnowledgeGraph>` addition is backward-compatible. Implements ticket #142 MVP. - **Raised by:** Workshop — unanimous - **Dissent:** None. All participants agreed MVP boundary is tell_state + disclosure. Paula adds that `tell_state` should eventually incorporate KG-derived secret-exposure intensity (NPC whose KG shows investigation proximity acts more stressed) — deferred to a future sprint as enhancement. --- ### 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, subject_display_name: Option }` 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` 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 — the engine does not determine which is wrong). Location contradiction is automatic (position comparison + time window). 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. Closes Q-026. - **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` (Dudley's Option B) because: (1) the struct is accessed with direct field reads — no string parsing in the detection hot path; (2) the monologue system needs a typed `KnowledgeSource` to resolve the source entity's name; (3) string encoding in `known_attributes` mixes semantic metadata with internal bookkeeping; (4) one optional struct field is additive — no breaking changes, no migration. Steps 2–5 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 to fire its canonical sequence (D-034). - **Raised by:** Workshop — unanimous on architecture and downstream chain; Tyre and Gestalt on struct approach (converged independently); Dudley conceded Option B - **Dissent:** Dudley (Round 1): Option B string encoding as Sprint 17 workaround. Conceded in Round 2. No standing dissent. --- ## Question Closures ### Q-024: NPC-to-NPC knowledge propagation timing - **Status:** CLOSED - **Resolution:** Knowledge transfer occurs via a separate `transfer_npc_knowledge` Bevy system running after `run_npc_conversations`. Transfer fires once per conversation at conversation start. See D-080 for full specification. - **Closed by:** Workshop — unanimous --- ### Q-025: KG memory pressure and eviction strategy - **Status:** CLOSED - **Resolution:** No memory cap or eviction strategy is needed for v0.1 or v0.2. Current analysis: ~14 KB per Active NPC KG (50 entities + 20 facts, D-041 budget). 80 Active NPCs = ~1.1 MB. 2,000 Background NPCs at 10 entries = ~5 MB. Total ~6 MB. With gossip propagation (1–3 facts per conversation): estimated ~12 MB peak at current NPC counts. **Re-evaluation trigger:** Active NPC count exceeds 200, OR profiling shows KG memory exceeding 50 MB. Neither condition expected before v0.3. - **Closed by:** Workshop — Tyre, Gestalt, Dudley confirmed (Paula did not address; non-objection noted) --- ### Q-026: Contradiction detection — when and how to detect - **Status:** CLOSED - **Resolution:** Event-driven detection at KG write time in `observe_entity()`, using `ContradictionClaim` struct. Location contradiction is automatic (Sprint 17). Attribute and fact contradiction are content-authored (Sprint 18). See D-083 for full specification. Entity grants ship in Sprint 17 (team lead decision, 2026-02-24); the contradiction chain fires via `EntityKnowledge.ToldBy` as designed. - **Closed by:** Workshop — unanimous on detection architecture. Unconditional. --- ## Implementation Scope ### Ticket #141 — Knowledge grant wiring (POI discovery) **Updated scope based on workshop:** - Grant mechanism: `KnowledgeEventType::KnowledgeGranted` (D-079) handles POI discovery via `FactId("poi.dock_7_restricted")` — no separate event type needed - When player enters POI trigger zone, push `KnowledgeGranted { fact_id: FactId("poi.*"), confidence, source: DirectObservation { tick } }` to queue - `filter_by_access` → `KnowledgeGated("poi.dock_7_restricted")` already gates content on this fact (graph.rs:299–302) - No new access control code; no new types; no new systems beyond the shared `KnowledgeGranted` event - **Depends on:** Dudley's Ticket A + B (grant infrastructure) - **Effort:** ~20 lines on top of Ticket B infrastructure; primarily content authoring for POI definitions ### Ticket #142 — NPC information boundaries (MVP) **Updated scope based on workshop (D-082):** Sprint 17 deliverable: 1. `derive_tell_state` adds `Option<&KnowledgeGraph>` to query; uses KG for OTHER-entity relationship reads; falls back to axis components for self-state and when KG is absent 2. `DisclosureCandidates` component + `derive_disclosure_candidates` system (see #172 spec) 3. `process_unprompted_disclosure` system stub (trigger gates operational; content-authored lines sparse for Sprint 17) **Explicitly out of Sprint 17 scope:** - Conversation partner KG-awareness check (Sprint 18) - Routine KG awareness (v0.2+) - Pathfinding KG integration (not planned) **Depends on:** Ticket B (KG populated with meaningful content); Ticket D (NPC KGs populated via gossip) --- ## Updated Specifications ### Ticket #172 — Unprompted Disclosure **Full specification (supersedes any prior spec):** **New components:** ```rust #[derive(Component, Debug, Default)] pub struct DisclosureCandidates { pub candidates: Vec, pub computed_tick: u64, } #[derive(Component, Debug, Default)] pub struct DisclosureCooldown { pub per_fact_history: BTreeSet, // facts disclosed this cooldown period pub npc_cooldown_until: u64, } ``` **`derive_disclosure_candidates` system:** - Runs every tick for Active NPCs; recomputes candidates when `time.tick - computed_tick > 30` - Filters NPC KG: Active state; confidence ≥ KnowsOf (trait-adjusted); not in `per_fact_history`; `disclosure_blocked != true`; trust rough-gate (categories requiring Real trust excluded at Surface) - Trait Stage 1 predicates: Cautious = requires KnowsDetails+; Gossipy = includes Suspects; Loyal = suppresses entity-linked facts where entity is trusted by NPC - Candidate list capped at 10; sorted by confidence descending, then `last_updated_tick` descending **`process_unprompted_disclosure` system:** - Trigger gates (all must pass): trust ≥ Surface; mood ≠ Hostile; contentment ≥ −10; candidates not empty; per-NPC cooldown clear; global rate limit (1/10 ticks, StableId-ordered); witness inhibition check; location privacy check - On success: push `KnowledgeGranted` event for player + push selected fact to `per_fact_history`; reset `npc_cooldown_until` **Trait Stage 2 (line pool):** Trait modifier tags in `IndexedDialogueLine.tags` bias Layer 4 selection weights for delivery style. **`disclosure_context` YAML field on disclosure candidates:** `private` / `semi_private` / `any`. Checked at Layer 4 line selection against current location zone type. **Estimated effort:** ~220 lines (Dudley Ticket G); requires Gestalt + Paula algorithm spec (now provided in round-2-notes.md). --- ### Ticket #173 — Trait filtering for unprompted disclosure **Full specification (supersedes any prior spec):** Traits operate as two-stage filters, both implemented: **Stage 1 — candidate pool filter (WHAT):** | Trait | Filter applied | |-------|---------------| | Cautious | Remove candidates with confidence < KnowsDetails; remove ToldBy-source facts (won't pass on rumors) | | Gossipy | Include Suspects-confidence candidates (normally excluded) | | Loyal | Remove facts linked to entities with trust_level ≥ Real in NPC Relationships | | Talkative | Override witness inhibition gate; include candidates at all confidence levels | Traits map to filter predicates via a content-authorable YAML configuration (not hard-coded enum dispatch). **Stage 2 — line pool scoring (HOW):** - Trait tags in `IndexedDialogueLine.tags` are matched by Layer 4 line selection - Tag examples: `"cautious_delivery"`, `"gossip_delivery"`, `"professional_delivery"` - Same underlying candidate fact can have multiple authored line variants with different trait delivery tags **`disclosure_eligible: bool` in authored NPC KG YAML:** - Set to `false` for facts that should never enter the candidate pool regardless of trust tier or trait - Implemented as `disclosure_blocked: bool` field on `FactKnowledge` (default `false`) - Used for Major secrets with survival stakes (e.g., Kael's ring membership) **Estimated effort:** ~40 lines for trait filter predicates + content configuration schema --- ## New Ticket Recommendations The following new tickets are recommended based on workshop outputs. These are recommendations for SI to formalize: --- ### Recommended Ticket A: KnowledgeGrant schema + ContentEntityRegistry **Scope:** `server/src/content/types.rs` — replace `KnowledgeGrant` struct with untagged enum (`Fact` + `Entity` variants); add `disclosure_blocked: bool` to `FactKnowledge`. New file `server/src/knowledge/content_registry.rs` — `ContentEntityRegistry` resource. NPC spawn sites: add `content_registry.register(content_id, sid)`. Content index validation: parse confidence strings, validate entity_refs. **Blocks:** Tickets B, C, D, E, F, G (all downstream work) **Effort estimate:** ~120 lines **Sprint:** 17 --- ### Recommended Ticket B: KnowledgeGranted event + `process_knowledge_events` handler **Scope:** `server/src/knowledge/events.rs` — add `KnowledgeEventType::KnowledgeGranted` variant with `ProcessedKnowledgeGrant` enum (`Fact` and `Entity` subtypes); add match arm handler. `server/src/knowledge/types.rs` — add `TryFrom<&str> for KnowledgeConfidence`. `server/src/simulation/dialogue.rs` — wire `knowledge_grant` field in `process_talk_interaction` after line selection. Runtime NPC KG guardrail: 3-line check in handler. **Blocks:** Tickets D, F, G; ticket #141 POI discovery wiring **Effort estimate:** ~150 lines **Sprint:** 17 --- ### Recommended Ticket C: ContradictionClaim struct + detection in `observe_entity` **Scope:** `server/src/knowledge/types.rs` — add `ContradictionClaim` struct; add `contradicted_claim: Option` to `EntityKnowledge`; add `CONTRADICTION_WINDOW_TICKS: u64 = 600` constant. `server/src/knowledge/graph.rs` — add pre-overwrite contradiction check in `observe_entity()`. `server/src/knowledge/events.rs` — add `ContradictionDetected` event variant; add processing (relationship shift for ToldBy source entity). Tests: location contradiction, time window boundary, no false positive for non-ToldBy sources. **Blocks:** Ticket F (requires ContradictionClaim + event for monologue integration) **Effort estimate:** ~120 lines + ~40 lines tests **Sprint:** 17 --- ### Recommended Ticket D: NPC-to-NPC knowledge transfer system **Scope:** New file `server/src/simulation/npc_knowledge_transfer.rs` — `transfer_npc_knowledge` system. Trust-tier mapping; confidence downgrade via `min(source_confidence, KnowsOf)`; `disclosure_blocked` check; ToldBy source construction; 1–3 random fact selection (most-recent-first); player overheard grant at Suspects confidence; system ordering: `after(run_npc_conversations)`. **Blocks:** Ticket F (ToldBy entries needed for contradiction to fire); Ticket G (NPC KGs need meaningful content before disclosure is useful) **Effort estimate:** ~160 lines **Sprint:** 17 --- ### Recommended Ticket E: `tell_state.rs` KG awareness (MVP boundary #142) **Scope:** `server/src/npc/tell_state.rs` — add `Option<&KnowledgeGraph>` to `derive_tell_state` query; replace Friendly-tell `relationships.entries` direct read with `kg.relationship_with(&entity_sid)` for OTHER-entity state; self-axes remain ground-truth reads. **Blocks:** Nothing (standalone improvement, existing tests continue passing) **Effort estimate:** ~35 lines **Sprint:** 17 --- ### Recommended Ticket F: Contradiction monologue + event chain completion **Scope:** `server/src/simulation/monologue.rs` — add `EntityRegistry` + `Query<&NpcName>` to system signature (or read pre-resolved names from event payload); add `resolve_name` helper; add match arm for `ContradictionDetected`. `server/src/knowledge/events.rs` — in `ContradictionDetected` processing, call relationship shift for ToldBy source entity; pre-resolve display names into event struct. Monologue pool (content — Mellanie): generic fallback template line + hand-authored FRIEND-specific lines for Sera/Kael contradiction phases. Integration test: full THE FRIEND arc sequence (Tick T1 grant creates ToldBy → Tick T2 observation triggers ContradictionClaim → monologue fires with correct names → Sera/Kael shift to PersonOfInterest → AnomalyMarker set). **Blocks:** Nothing (downstream consumers — anomaly.rs, D-033 pipeline — already built and tested) **Effort estimate:** ~90 lines + content (Mellanie) **Sprint:** 17 --- ### Recommended Ticket G: DisclosureCandidates + unprompted disclosure (#172 + #173) **Scope:** New file `server/src/npc/disclosure.rs` — `DisclosureCandidates` component; `DisclosureCooldown` component; `derive_disclosure_candidates` system; `process_unprompted_disclosure` system; trait filter predicates (Cautious, Gossipy, Loyal, Talkative); global rate limit resource. `server/src/simulation/dialogue.rs` — Layer 4 reads `DisclosureCandidates` for NPC-initiated line selection. System ordering: `derive_disclosure_candidates.after(process_knowledge_events)`, `process_unprompted_disclosure.after(derive_disclosure_candidates)`. **Blocks:** Nothing **Depends on:** Tickets B + D (KG needs meaningful NPC content before disclosure is useful) **Effort estimate:** ~220 lines **Sprint:** 17 (implement after Tickets B + D) --- ## THE FRIEND Arc Validation Sequence **Status:** All systems in steps 3–6 are already implemented and passing tests. Steps 1–2 are new work (Tickets A+B for grant; Ticket C for ContradictionClaim; Ticket D for ToldBy population in NPC KGs). **Canonical sequence (D-079/D-083 integration test, using entity grant path):** 1. **Tick T1 — Testimony:** Detective talks to Sera → `process_talk_interaction` selects line with `knowledge_grant: { entity_ref: "kael", attributes: { location: "dock-7", shift: "second" }, confidence: "knows_of" }` → `KnowledgeGranted` event pushed → `process_knowledge_events` creates `EntityKnowledge(kael_sid)` in player KG: `source: ToldBy { source_id: sera_sid, tick: T1 }`, `last_known_position: dock_7_pos`, `confidence: KnowsOf`, `state: Active`. 2. **Tick T2 — Observation:** Player LOS includes Kael in corridor B-7 → Perception fires `DirectObservation` → `observe_entity(kael_sid, b7_pos, T2)` runs → pre-overwrite contradiction check: existing entry has `ToldBy` source; `b7_pos ≠ dock_7_pos`; `|T2 - T1| < 600` → `entry.state = Contradicted`; `entry.contradicted_claim = Some(ContradictionClaim { source: ToldBy { sera_sid, T1 }, claimed_position: dock_7_pos, detected_at_tick: T2 })` → entry overwritten with `DirectObservation`. 3. **Tick T2 — ContradictionDetected event pushed:** `{ observer: detective_entity, entity_sid: kael_sid, source_display_name: Some("Sera"), subject_display_name: Some("Kael") }`. 4. **Tick T2/T3 — Downstream cascade (existing systems, already tested):** - `detect_anomalies` finds `Contradicted` state → `AnomalyMarker` on Kael - Relationship system: `EntityKnowledge(kael_sid).relationship → PersonOfInterest`; `EntityKnowledge(sera_sid).relationship → PersonOfInterest` - D-033 pipeline: both Kael and Sera → amber (#e8c547) - Monologue system: fires contradiction line: *"Sera said Kael was at the dock intake during second shift. I'm looking at him in corridor B-7 right now."* 5. **Next player Talk with Sera:** Confrontation option unlocked for Sera and Kael. Sera behaves normally (she does not know the detective has seen Kael in B-7 — her KG has no such entry). The detective holds the contradiction alone until confrontation. --- ## Sprint 17 vs Sprint 18 Scope Summary ### Sprint 17 (this workshop produces) | Component | Ticket | Lines est. | |-----------|--------|-----------| | `KnowledgeGrant` schema + `ContentEntityRegistry` | A | ~120 | | `KnowledgeGranted` event + handler + dialogue wire | B | ~150 | | `ContradictionClaim` struct + detection in `observe_entity` | C | ~160 | | NPC-to-NPC `transfer_npc_knowledge` system | D | ~160 | | `tell_state.rs` KG awareness | E | ~35 | | Contradiction monologue + event chain | F | ~90 | | `DisclosureCandidates` + unprompted disclosure | G | ~220 | | **Total** | | **~935 lines** | Q-024, Q-025, Q-026 closures in decisions/questions.md. ### Sprint 18 (committed scope from this workshop) - Full `EntityGrant { attributes: BTreeMap }` variant (currently only `entity_ref` + position in Sprint 17) - `Compound` grant variant (facts + entity in one line) - Conversation partner KG-awareness check (`kg.knows_entity(&partner_sid)` in pairing loop) - Attribute contradiction via content-authored YAML pairs (typed attribute key conventions) - Fact contradiction via `contradicts` field on KnowledgeGrant entries - Template monologue system for named contradictions (generic entity substitution for auto-generated NPCs) - Specific overheard fact transfer to player (currently: entity-level Suspects grant only) - Runtime NPC KG decay → disclosure candidate staleness handling ### Deferred (v0.2+) - Routine KG awareness - Pathfinding KG integration (not planned — no safe failure recovery) - "Ask around" emergent information-seeking behavior - NPC disclosing own KG-contradicted knowledge ("I could have sworn Kael was at the dock...") --- ## Team Lead Decisions | Decision | Date | Override / Note | |----------|------|----------------| | **Entity grants ship in Sprint 17** | 2026-02-24 | Team lead call. Overrides Paula's Round 2 acceptance of Dudley's FactId workaround. Adopts Tyre/Gestalt/Dudley position: compound `KnowledgeGrant` enum with `Entity` variant, `ContentEntityRegistry` at NPC spawn (~0.5 day infrastructure). Paula's conditions (ToldBy in event, Sprint 18 formal commitment) honoured where applicable. See round-2-notes.md §Tension A for full workshop record. | --- ## Open Questions Not Resolved by This Workshop | ID | Question | Required for | Decision owner | |----|----------|-------------|---------------| | — | `CONTRADICTION_WINDOW_TICKS` final value: 600 (majority) vs 1800 (Tyre) | Contradiction detection sensitivity | Can ship 600; playtest adjusts | --- *Document produced from workshop rounds 1–2. D-records pending registration in decisions/ domain files by Qatux. New tickets A–G pending SI formalization.*