Files
settled-reach/docs/workshops/knowledge-flow-npc-boundaries/workshop-outcomes.md
T
jpmschweitzerandClaude Opus 4.6 60ffe2e02a docs(docs): add frontmatter to knowledge-flow-npc-boundaries workshop
Standardized YAML frontmatter on all 12 files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:40:44 +01:00

31 KiB
Raw Blame History

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Workshop Outcomes — Knowledge Flow & NPC Information Boundaries Final workshop outcomes with D-079 through D-083 decision records for grant architecture, NPC propagation, disclosure, boundaries, and contradiction detection workshop archived knowledge-flow-npc-boundaries qatux 2 2026-02-24

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<String, StableId>, populated at NPC spawn time) resolves entity_ref strings to StableIds 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<KnowledgeGrant> } (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 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: 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<ActiveSim> — Background-tier NPCs (D-026, 5002000) 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<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 — 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 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 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 (13 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_accessKnowledgeGated("poi.dock_7_restricted") already gates content on this fact (graph.rs:299302)
  • 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:

#[derive(Component, Debug, Default)]
pub struct DisclosureCandidates {
    pub candidates: Vec<FactId>,
    pub computed_tick: u64,
}

#[derive(Component, Debug, Default)]
pub struct DisclosureCooldown {
    pub per_fact_history: BTreeSet<FactId>,   // 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:


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


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


Scope: server/src/knowledge/types.rs — add ContradictionClaim struct; add contradicted_claim: Option<ContradictionClaim> 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


Scope: New file server/src/simulation/npc_knowledge_transfer.rstransfer_npc_knowledge system. Trust-tier mapping; confidence downgrade via min(source_confidence, KnowsOf); disclosure_blocked check; ToldBy source construction; 13 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


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


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


Scope: New file server/src/npc/disclosure.rsDisclosureCandidates 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 36 are already implemented and passing tests. Steps 12 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 DirectObservationobserve_entity(kael_sid, b7_pos, T2) runs → pre-overwrite contradiction check: existing entry has ToldBy source; b7_pos ≠ dock_7_pos; |T2 - T1| < 600entry.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 12. D-records pending registration in decisions/ domain files by Qatux. New tickets AG pending SI formalization.