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>
32 KiB
Gestalt Round 1 — Systems Design Analysis
Knowledge Flow & NPC Information Boundaries Workshop
Author: Gestalt Date: 2026-02-23 Focus: Mechanical interactions, "is this fun?", knowledge as gameplay lever
Framing: The Master Mechanic
Before diving topic-by-topic, let me crack my knuckles and establish the frame I'm evaluating everything through.
Asymmetric information is the game. Not a feature, not a pillar — the entire experience of The Settled Reach is "you know different things than the world does." Every system in this workshop is a mechanism for managing that asymmetry. The question I'm asking for each topic is: does this design produce interesting player decisions about what to do with information?
The current state is: the KG data model is a perfect asymmetric information engine (D-041), but it's running in idle. Knowledge enters it via perception. Nothing comes out of it into NPC behavior. This workshop is about turning the engine on.
Let me break down what that actually means mechanically for each topic.
Topic 1: Knowledge Flow — The Grant Mechanism
Mechanical position summary
This is the INPUT side of the knowledge engine. The key design question is: what is the canonical moment a fact "enters" the player's mental model as a game state change?
1.1 When does grant fire?
Position: Fire on line selection, server-side, before snapshot emission.
Not on display. Not on client acknowledgment. Line selection is the authoritative moment.
Why this matters mechanically: if the grant fires at display, we have a timing problem when the player walks away mid-conversation (D-064 walk-away mechanic). The NPC started a sentence — did the player "hear" it? By firing on selection, the server authoritatively records "this information was transmitted at tick T." The walk-away event, if it fires, records an incomplete interaction but does NOT retract the knowledge already granted. This is diegetically correct: if someone starts telling you something, you heard the beginning.
The pipeline should be:
- Layer 4 topic+mood scoring selects line
- Server fires
KnowledgeGrantedevent intoKnowledgeEventQueue - Knowledge update system processes it that tick
- Snapshot includes updated KG state
This puts knowledge grant in the same event pipeline as DirectObservation and LeftLOS (events.rs) — correct architectural fit.
1.2 Grant payload schema — the entity/fact split
Position: KnowledgeGrant must support three payloads.
Current KnowledgeGrant { fact_id: String, confidence: String } only handles fact-level grants. The "Kael handles cargo at Dock 7" example from the brief is NOT a fact grant — it's an entity grant: creates/updates an EntityKnowledge entry for Kael with source ToldBy { source_id: sera_sid, tick }.
Proposed schema (extend content/types.rs):
enum KnowledgeGrantPayload:
FactGrant { fact_id: FactId, confidence: KnowledgeConfidence }
EntityGrant { entity_id: String, attributes: BTreeMap<String,String>, confidence: KnowledgeConfidence }
Compound { grants: Vec<KnowledgeGrantPayload> } // "Kael is at Dock 7 AND contraband.ring_exists"
The EntityGrant maps to known_attributes on the EntityKnowledge entry. The source is always ToldBy { source_id: speaking_npc_sid, tick }. This is the missing piece that makes KnowledgeSource::ToldBy constructible.
Is this fun? Yes. It means a single dialogue line can tell you TWO things simultaneously — a new fact AND update your understanding of a person. These are the "information dense" moments players remember.
1.3 POI discovery
Position: FactId("poi.*") namespace is correct. No new type needed.
FactId("poi.dock_7_restricted") integrates cleanly with:
filter_by_access→KnowledgeGated("poi.dock_7_restricted")— the access rule already reads fromknows_fact()in graph.rs line 301- Monologue prerequisites →
fact_at_least()in graph.rs line 71 already works on any FactId - Dialogue gating → same
The alternative (extend EntityKnowledge to cover locations) fragments the query model. POIs are facts, not entities. Extending EntityKnowledge for locations would mean adding a third BTreeMap to KnowledgeGraph and duplicating query methods. Reject.
1.4 Physical evidence
Position: Same grant mechanism, different source variant.
Evidence discovered via terminal/document/cargo manifest fires a FactGrant or EntityGrant with source: DirectObservation { tick } rather than ToldBy. This distinction matters for contradiction detection (Topic 5): a DirectObservation source carries higher credibility than ToldBy. It also means physical evidence cannot be contradicted by the NPC denial path — you saw it with your own eyes.
A separate KnowledgeEventType::EvidenceDiscovered variant is NOT needed. The payload type distinguishes it from regular perception updates; the source type (DirectObservation) distinguishes it from NPC-told knowledge. One new event type (KnowledgeGranted) handles all grant paths.
1.5 Author guardrails
Position: Runtime enforcement with content-load validation.
Rule: an NPC dialogue line can only grant knowledge that is consistent with the NPC's KG or their authored background facts.
Implementation: content validator (already exists in the build pipeline) checks that for any EntityGrant { entity_id }, the entity_id references a known entity in the NPC's profile or the station manifest. Runtime enforcement: the dialogue system, when processing knowledge_grant, first checks that the granting NPC has the relevant knowledge at the claimed confidence level. If the NPC's KG doesn't support the grant, the line fires but the grant is silently dropped with a tracing::warn!.
Why not compile-time only? NPCs' KGs are dynamic (they change at runtime via gossip, observation). A content-authored grant might be valid at session start and invalid after a KG decay cycle. Runtime enforcement handles this gracefully.
Topic 2: NPC-to-NPC Knowledge Propagation (Q-024)
Mechanical position summary
This is the CIRCULATION side of the knowledge engine. Knowledge isn't useful if it just accumulates in one NPC and decays. The gossip system is what makes knowledge a renewable resource — facts move through the world and reach the player through multiple channels.
2.1 Conversation system as the hook
Position: Confirmed. The conversation system IS the routine intersection hook. No separate system needed.
run_npc_conversations (conversation.rs line 278+) already has:
- Proximity detection within
CONVERSATION_PROXIMITYtiles - Cooldown management (
ConversationCooldown) - Deterministic pairing via StableId sort (D-010 compliant)
- Duration management (start tick → end tick)
Adding a knowledge transfer phase between conversation start and conversation end is architecturally clean. It fits between Phase 1 (start conversation) and the existing emission of SoundEvent/ConversationEvent. The transfer happens at conversation start — both NPCs exchange knowledge at the moment contact is made.
This satisfies the "queued at routine intersections" intent from Q-024. Routine-driven NPCs will naturally cross paths during their schedules, triggering conversations, triggering transfers.
2.2 Trust-gated filtering
Position: Map trust tiers to KnowledgeConfidence caps.
Proposed mapping (aligns with D-028 trust tiers):
| Trust tier | D-028 label | Facts eligible to transfer | Confidence cap on transfer |
|---|---|---|---|
| 0 (None) | — | No transfer | — |
| 1 (Surface) | public |
Facts with confidence ≥ KnowsOf, state: Active only | KnowsOf |
| 2 (Real) | real |
Facts with confidence ≥ KnowsOf, any Active state | KnowsOf (downgraded from KnowsDetails) |
| 3 (Secret) | secret |
All Active facts including Suspects-level | KnowsOf (downgraded from KnowsDetails) |
The confidence cap is the critical design choice: transferred confidence = min(source_confidence, KnowsOf).
Why KnowsOf as the cap, not KnowsDetails? Because gossip degrades information. NPC A telling NPC B something produces second-hand knowledge, never first-hand detail. This prevents gossip chains from propagating KnowsDetails — if a fact reaches someone via three intermediaries, it's still only KnowsOf. This is also why the player gaining KnowsDetails from an NPC requires DIRECT dialogue, not gossip relay.
Suspects stays Suspects across all tiers — a rumor is a rumor.
2.3 Rate limiting
Position: 1-3 facts per conversation, randomly selected from eligible pool.
Why random selection rather than "all eligible"? Knowledge explosion prevention. If NPC A has 30 eligible facts and meets NPC B every 10 game-minutes, uncapped transfer means every NPC converges to the same knowledge state quickly. This destroys the asymmetry that makes the game work.
Random selection also creates variance: each NPC-NPC meeting produces a different outcome. An NPC you talk to might not know the one thing they had a chance to overhear from someone else yesterday — they just weren't selected to share it. This is emergent and creates replay variance (nodding to Nigel).
Suggested cap: rand.random_range(1..=3) facts per conversation. Small enough to prevent explosion; large enough that relationships with high-trust NPCs provide real information value.
2.4 Observable by player — overheard knowledge
Position: Overheard knowledge is always Suspects regardless of occlusion fidelity.
The workshop raises this as a question: should the grant for overheard conversations follow word-level occlusion (D-078)? My answer: no. Here's why.
The two systems answer different questions:
- D-078 word occlusion: what can the player READ/HEAR — audio fidelity, narrative text
- Knowledge grant: what can the player KNOW — mechanical state
These should be decoupled. A player who overhears "...Kael...dock...second shift..." (heavily occluded) still walked away with a SUSPICION, not knowledge. The occluded words give narrative flavor; the fixed-confidence grant gives mechanical state.
Fixed Suspects for ALL overheard NPC-NPC conversations means:
- Player always gets SOMETHING from eavesdropping (reward for the behavior)
- Player never gets CERTAINTY from eavesdropping (incentive to follow up with direct dialogue)
- System is simple (no occlusion-weighted confidence calculation)
Is this fun? Yes. Eavesdropping becomes a preliminary investigation tool. You hear something suspicious, you go find the person involved, you talk to them directly, you escalate to KnowsOf or KnowsDetails. The eavesdrop is a lead, not a solution.
2.5 ToldBy source construction
The source_id for ToldBy is available at conversation.rs line 321: sid.map(|s| s.0.0).unwrap_or(u64::MAX). This is the StableId of the source NPC. The tick is time.tick from the SimulationTime resource. ToldBy construction is a one-liner once the architecture confirms Topic 2.
Topic 3: Unprompted Disclosure Design (#172)
Mechanical position summary
This is the most design-dense topic. Unprompted disclosure is not just "NPC volunteers info" — it's the mechanic that makes NPCs feel like they have an independent relationship with information. Done correctly, it creates the "a stranger just told you something important you didn't know you needed" moments that define good immersive sims.
3.1 "Do I know something you don't?" — Option A is correct
Position: NPC checks only own KG (Option A). Strongly opposed to cross-entity KG query (Option B).
This is not primarily a technical decision — it's a design decision about NPC cognition. NPCs do not know what the player knows. This is both diegetically correct AND mechanically interesting.
Why is Option A MORE interesting, not less interesting?
Consider what happens with Option B: every time the player already knows something, no NPC will repeat it. The player's information state becomes a filter on the entire NPC disclosure system. This means players who investigate thoroughly get LESS disclosure from NPCs over time. That's punishing success.
With Option A: NPCs might tell you things you already know. That is information too. "Why is this NPC telling me this? Do they not know that I know? Are they testing me? Are they covering for someone by offering an explanation I already disproved?" This is the paranoid detective headspace the game is going for.
The only UX downside is redundant disclosure feeling noisy. Solved by per-fact cooldowns (see 3.5) — the player won't hear the same thing twice from the same NPC in rapid succession.
3.2 Connection to NPC KG — architecture
Position: New DisclosureCandidateList component, NOT extending DerivedTellState.
DerivedTellState is a behavioral signal (how the NPC APPEARS). Disclosure candidates are content selection state (what the NPC might SAY). These are different concerns.
Proposed: a separate DisclosureCandidateList component, populated by a system that runs after the KG update pass:
struct DisclosureCandidateList {
// Facts from this NPC's KG that pass the disclosure filter
candidates: Vec<FactId>,
// Entity knowledge entries that pass the filter
entity_candidates: Vec<(StableId, String)>, // (entity, key_attribute)
// Last computed at this tick
computed_tick: u64,
}
This component is computed lazily (only for Active-tier NPCs in dialogue range) and expires after a few ticks. Layer 4 of the dialogue pipeline reads it when selecting unprompted disclosure lines.
Why not make DerivedTellState hold candidates? Because tell derivation runs every tick for ALL Active NPCs (tell_state.rs line 129). Adding KG iteration to that loop for NPCs not in dialogue range wastes compute. DisclosureCandidateList only computes for NPCs the player is actually engaging with.
3.3 Trait filtering (#173) — BOTH, sequenced
Position: Traits filter WHAT (candidate pool) then modify HOW (line selection). Two-stage.
Stage 1 — candidate filtering (which facts are eligible):
Cautioustrait: removes facts with confidence >Suspectsfrom candidate pool (shares only rumors, never certainties)Loyaltrait: removes facts about entities withrelationship: Friendly/PersonOfInterest(protects people they care about)Talkativetrait: expands pool to include facts at all confidence levels
Stage 2 — delivery modification (which line from the pool wins):
- Trait tags bias the line pool selection weights in Layer 4
- A
CautiousNPC who does disclose something delivers it obliquely ("I heard something... probably nothing") - A
TalkativeNPC delivers it directly and with elaboration
This is the correct architecture because content authors can create trait-appropriate LINE variants without needing to touch the candidate selection logic. The filtering and the delivery are independently authorable.
Is this fun? Yes. Personality shapes WHAT the NPC is willing to reveal AND how it reads when they reveal it. Two NPCs with the same knowledge but different traits create different investigative experiences.
3.4 Trigger conditions
Position: Four-gate system, all must pass.
| Gate | Condition | Rationale |
|---|---|---|
| Trust | Trust tier ≥ surface toward player |
You don't volunteer info to strangers |
| Mood | MoodState ≠ Hostile | Angry NPCs don't help |
| Contentment | Contentment ≥ -10 | Miserable NPCs are self-absorbed |
| Witness inhibition | No NPCs in radius OR trust override | NPCs are less forthcoming with an audience |
The witness inhibition gate is the interesting one. An NPC who is Friendly to the player might still not disclose sensitive information if their colleague is standing nearby. This creates the "can we talk privately?" dynamic — a moment of social positioning that feels diegetically real.
Trust tier ≥ surface maps to D-028 access tier: the NPC will speak to you at all (they haven't gone silent). Real disclosure of substantive facts should probably gate at real trust. Layer 4 line pool already handles trust tier filtering — unprompted disclosure can piggyback on this.
3.5 Rate limiting
Position: Two-layer rate limiting.
Layer 1 — per-fact per-NPC per-player: once an NPC has disclosed a fact to the player, that fact is marked disclosed_to_player in the DisclosureCandidateList and never selected again. Facts the player already knows (via other channels) are NOT filtered — see 3.1 above.
Layer 2 — per-NPC disclosure rate: 1 disclosure per LINE_COOLDOWN_TICKS (600 ticks = 1 game-hour, dialogue.rs line 39). This prevents disclosure spam from high-contentment NPCs.
No global rate limit across all NPCs. Global limiting would create invisible competition between NPCs for "disclosure slots" that the player can't see or understand. Keep complexity in the individual NPC state.
Topic 4: NPC Information Boundaries (#142)
Mechanical position summary
Let me be precise about what "NPC using their own KG for decisions" actually means at each level, because different systems have different stakes.
4.1 Priority order with justification
| Priority | System | What changes | Gameplay impact | Risk |
|---|---|---|---|---|
| 1 | tell_state.rs |
Tell derivation reads NPC's relationship knowledge from KG | NPC tells reflect what they know about OTHERS, not just their internal state | Low — KG reflects observed state accurately |
| 2 | Unprompted disclosure (#172) | Disclosure candidates come from KG | NPCs only volunteer what they know | Low — this is the whole point of #172 |
| 3 | Conversation partner selection | NPC A checks KG before approaching NPC B | NPCs don't chat with "strangers" (entities not in their KG) | Medium — adds KG query to pairing loop |
| 4 | Routine execution | Routine decisions read KG state | NPCs take different routes based on what they know | High — requires careful fallback |
| 5 | Pathfinding | Path uses known (not ground-truth) walkability | NPCs might get lost after KG decay | Very high — don't touch in v0.1 |
4.2 On tell_state.rs specifically
There is a subtle point here worth calling out. The workshop brief says tell_state.rs uses axes directly and should use KG. I want to be careful: the Secret, Contentment, Tolerance axes ARE the NPC's own internal state — the NPC always knows their own secret severity. Using ground truth for self-knowledge is CORRECT.
What changes with KG integration is not reading self-knowledge differently, but incorporating relationship knowledge into tell derivation. Currently, TellCategory::Friendly checks relationships.entries directly (tell_state.rs lines 105-112). This should instead check the KG's RelationshipState for the entities in range — because an NPC's relationship state can be affected by observed behavior that's recorded in their KG.
More impactfully: an NPC who has a Contradicted entry in their KG (they know something doesn't add up about someone they thought they trusted) should show a modified tell. This is a new tell category or a modifier on existing tells, not yet designed. Worth flagging for a future sprint.
4.3 Minimum viable boundary
Position: tell_state.rs relationship reads from KG + unprompted disclosure from KG = MVP.
These two changes produce maximum gameplay-visible difference for minimum implementation risk. An NPC that:
- Shows tells based on their actual observed relationship state (not just the raw relationship axis)
- Only voluntarily discloses things they actually know
...is a meaningfully bounded NPC even if its pathfinding runs on ground truth.
4.4 Simulation tier
Position: Background-tier NPCs (D-026, 500-2000 range) get NO KG-based behavior.
Background NPCs run state machines. They don't have dialogue. They don't initiate conversations. Their KG is either empty or minimal. Adding KG queries to background-tier processing would blow the performance budget.
Active-tier NPCs (30-80 per D-026) are the only ones who can engage in dialogue and unprompted disclosure. KG-based behavior is gated on With<ActiveSim>, matching the existing derive_tell_state scope (tell_state.rs line 140).
4.5 Fallback behavior
Position: Ground truth fallback with structured logging. No "ask around" behavior in v0.1.
If an Active-tier NPC's KG has no relevant entry for a needed decision:
- Fall through to ground truth
- Log at
tracing::debug!level:"NPC {sid} falling back to ground truth for {decision_type}"
The "ask around" emergent behavior (option C from the brief) is the CORRECT long-term design but it's a v0.2+ feature. It requires an "information-seeking" behavioral state, a system to resolve it, and content to support it. Not v0.1 scope.
Topic 5: Contradiction Detection Pipeline (Q-026)
Mechanical position summary
This is the payoff. Everything above feeds into this: ToldBy sources exist (from Topic 2 grants and Topic 1 dialogue grants), the player has observed entity positions directly, and now the system must detect when told information and observed reality diverge. This is THE FRIEND arc mechanic at its purest.
5.1 Critical structural issue I must raise
There is a problem with the current EntityKnowledge data model for contradiction detection.
KnowledgeGraph.entities is BTreeMap<StableId, EntityKnowledge> — one entry per known entity. When the player observes Kael at corridor B-7 via observe_entity(), the last_known_position is overwritten to B-7 and source is overwritten to DirectObservation. The original ToldBy source that said "dock during second shift" is gone.
Contradiction detection cannot fire on writes if the information being contradicted has already been overwritten.
Position: Add contradiction_basis: Option<ContradictionBasis> to EntityKnowledge.
pub struct ContradictionBasis {
/// The previous source, preserved when a contradiction is detected.
pub conflicting_source: KnowledgeSource,
/// The previous position at the time of the conflicting observation.
pub conflicting_position: Option<TilePosition>,
/// The tick when the contradiction was detected.
pub detected_at_tick: u64,
}
Contradiction detection fires in the observe_entity() write path BEFORE overwriting:
- Read existing entry
- If existing source is
ToldBy { source_id, tick: told_tick }AND new position ≠ existing position AND time window overlaps (|current_tick - told_tick| < CONTRADICTION_WINDOW) - Preserve old source/position in
contradiction_basis - Set entry state to
Contradicted - Write new observation
- Push
ContradictionDetectedevent
This preserves both pieces of information for monologue text ("Sera said X was at the dock — I just saw them in corridor B-7").
5.2 Detection algorithm
Position: Event-driven detection at KG write time. Never per-tick.
Two detection categories for v0.1:
Location contradiction (automatic):
IF new_observation.position ≠ existing_entry.last_known_position
AND existing_entry.source == ToldBy { tick: told_tick }
AND |current_tick - told_tick| < CONTRADICTION_WINDOW_TICKS
THEN contradiction detected
CONTRADICTION_WINDOW_TICKS = 600 (1 game-hour) is a reasonable default. Facts more than a game-hour old aren't "this moment" contradictions — they're just outdated info, which is handled by decay/stale.
Attribute contradiction (semi-automatic):
For known_attributes: BTreeMap<String, String>, contradiction fires when a new attribute write for an existing key produces a different value AND both have different source types (ToldBy vs DirectObservation or different ToldBy sources).
The known_attributes key convention needs to establish a typing scheme to make this meaningful: "role:*" keys are comparable (same role key, different values), "event:*" keys are not directly comparable. This can be a content convention rather than a type system change — lightweight for v0.1.
Fact contradiction (content-authored):
Content authors specify contradiction pairs in YAML: contradicts: ["contraband.smuggling_denied", "contraband.ring_exists"]. Detection fires when both facts exist in the same KG at Active state.
5.3 Event chain
KG write (observe_entity / grant processing)
→ ContradictionCheck (inline, before write completes)
→ ContradictionDetected event { observer, entity, type, conflicting_source, new_source }
→ MonologueSystem: "Wait, that doesn't add up" line selection
→ Prerequisite: both KG entries Contradicted
→ RelationshipSystem: target entity → PersonOfInterest
→ AnomalySystem: AnomalyMarker set on target
→ D-033: color transition → amber #e8c547
The monologue system reads from EntityKnowledge.contradiction_basis.conflicting_source to construct the text: "[Name] said [entity] was at [conflicting_position]. I just saw them at [current_position]." This requires the ToldBy source_id to be resolvable back to a name — which requires the EntityRegistry and the resolved NPC's NpcName component. Dudley should confirm this lookup is available from the monologue system context.
5.4 THE FRIEND arc — full mechanical sequence
This is the canonical integration test. Walking through every system:
Setup: Sera's dialogue pool includes a line with EntityGrant { entity_id: "kael", attributes: { "location": "dock-7-second-shift" }, confidence: KnowsOf } and knowledge_grant: Some(...).
Sequence:
-
Tick 100: Player talks to Sera → dialogue pipeline selects her "Kael handles cargo at Dock 7" line →
KnowledgeGrantedevent pushed → knowledge update system processes it → player's KG:entities[kael_sid] = EntityKnowledge { last_known_position: dock_7_coords, source: ToldBy { source_id: sera_sid, tick: 100 }, confidence: KnowsOf, state: Active }. -
Tick 150: Player moves toward Dock 7, Kael is not there. Player continues through station. Passes through corridor B-7. Kael walks into LOS.
-
Tick 150: Perception system fires
KnowledgeEventType::DirectObservation { target: kael_entity, position: b7_coords }→ knowledge update system callsobserve_entity(kael_sid, b7_coords, 150). -
Inside
observe_entity(): Existing entry hassource: ToldBy { source_id: sera_sid, tick: 100 },last_known_position: dock_7_coords. New positionb7_coords ≠ dock_7_coords. Time window:|150 - 100| = 50 < 600. Contradiction detected. Entry updated:contradiction_basis: Some(ContradictionBasis { conflicting_source: ToldBy { sera_sid, 100 }, conflicting_position: dock_7_coords, detected_at: 150 }),state: Contradicted,last_known_position: b7_coords,source: DirectObservation { tick: 150 }. -
Tick 150 (same frame):
ContradictionDetectedevent emitted. Monologue system fires: "Sera said Kael was at the dock. I just saw him in B-7." (line from monologue pool, prerequisite:contradiction.kael_dock_b7or whatever the fact tag is — needs content authoring). Relationship state forSera→PersonOfInterest.AnomalyMarkerset on Sera entity. D-033 color transition: Sera → amber. -
Player reaction: Sera now appears amber on screen. Player is prompted to re-engage with Sera. Next conversation with Sera uses "Contradicted" entry in player's KG as a trust modifier in Layer 1-3 filtering → Sera's trust-tier dialogue unlocks confrontation lines.
What breaks this sequence: If observe_entity() does not check for contradiction before overwriting, step 4 fails silently. This is why the structural issue in 5.1 is blocking. Everything downstream (monologue, relationship shift, color change) is already built and tested — the detection algorithm is the missing link.
Cross-Topic Interactions (the emergent machine)
Let me map the feedback loops, because this is where it gets interesting:
Topic 2 (NPC gossip) → produces ToldBy entries in NPC KGs
Topic 1 (grant mechanism) → NPC ToldBy entries can be transferred to player via dialogue
Topic 5 (contradiction) → player observation contradicts ToldBy → Contradicted state
Topic 3 (unprompted disclosure) → NPC with Contradicted knowledge discloses their confusion?
That last arrow is a bonus emergent loop: an NPC who has a Contradicted entry in their own KG (because they also observe Kael somewhere unexpected) could volunteer that confusion via unprompted disclosure. "I could have sworn Kael was supposed to be at the dock today, but..." This is not Topic 5, it's Topic 3 using Topic 5 outputs as candidate selection criteria. Worth noting for future sprint.
Positions Summary Table
| Topic | Position | Rationale |
|---|---|---|
| 1.1 Grant timing | Fire on line selection | Server-authoritative, handles walk-away cleanly |
| 1.2 Grant payload | Three-type enum: Fact/Entity/Compound | Entity grants enable ToldBy construction |
| 1.3 POI | FactId("poi.*") namespace |
No new type; integrates with existing KnowledgeGated |
| 1.4 Evidence | Same grant mechanism, DirectObservation source | Source type distinguishes from NPC-told |
| 1.5 Guardrails | Runtime enforcement + content-load validation | KGs are dynamic; compile-time-only is insufficient |
| 2.1 Gossip hook | Conversation system confirmed | Already has pairing, lifecycle, determinism |
| 2.2 Trust-gate | min(source_confidence, KnowsOf) cap |
Prevents KnowsDetails propagation via gossip |
| 2.3 Rate limit | 1-3 random facts per conversation | Prevents knowledge convergence |
| 2.4 Overheard | Fixed Suspects regardless of occlusion |
Eavesdrop = lead, not certainty |
| 3.1 "Do I know?" | Option A: NPC checks own KG only | Diegetically correct AND mechanically richer |
| 3.2 Architecture | New DisclosureCandidateList component |
Separation from tell derivation |
| 3.3 Traits | Both: filter candidates AND modify delivery | Independently authorable |
| 3.4 Trigger | Four-gate: trust + mood + contentment + witnesses | Witness gate creates social positioning |
| 3.5 Rate limit | Per-fact per-NPC + per-NPC rate limit | No global limit (unintelligible to player) |
| 4.1 Priority | tell_state (KG relationships) → disclosure → conversation pairing | Most impact, lowest risk first |
| 4.2 tell_state | Read relationship state from KG, not raw axis | Self-knowledge stays on axes (correct) |
| 4.3 MVP | tell_state relationship reads + disclosure from KG | Visible difference, low risk |
| 4.4 Tier | Active-tier only (With<ActiveSim>) |
Consistent with D-026 |
| 4.5 Fallback | Ground truth + logging, no "ask around" in v0.1 | "Ask around" is v0.2+ |
| 5.1 Structural | Add contradiction_basis: Option<ContradictionBasis> to EntityKnowledge |
Overwrite problem — blocking |
| 5.2 Algorithm | Event-driven at KG write time | Off per-tick hot path |
| 5.2a Location | Automatic: position + time window | Tier 1 implementation |
| 5.2b Attribute | Semi-automatic: same key, different value, different source | Works with String BTreeMap + key conventions |
| 5.2c Fact | Content-authored contradiction pairs | Not all fact contradictions are algorithmic |
| 5.3 Event chain | KG write → ContradictionDetected → monologue → relationship → anomaly → color | All downstream consumers confirmed built |
| 5.4 FRIEND arc | Full sequence walks clean with proposed design | Pending structural fix from 5.1 |
Open Questions I'm Flagging for Round 2
-
Contradiction window (
CONTRADICTION_WINDOW_TICKS). I proposed 600 ticks (1 game-hour). Paula should weigh in: what timeframe makes narrative sense for "that's a contradiction, not just stale info"? -
Entity grant
entity_idresolution.EntityGrant { entity_id: String }needs to resolve to aStableIdat grant processing time. Who owns the "name → StableId" registry?EntityRegistrydoesStableId ↔ Entity(Bevy Entity), but is there a "display name → StableId" path? Tyre should confirm. -
Monologue text generation from
contradiction_basis. The monologue pool currently has authored lines. For THE FRIEND arc, the line needs to reference specific entities by name ("Sera said Kael was at the dock"). Does this require parameterized monologue lines (dynamic text insertion)? Or do we just require dedicated authored lines per contradiction scenario? Paula should decide. -
DisclosureCandidateListcompute trigger. I said it computes lazily for NPCs in dialogue range. Tyre should confirm: is there a "player entered dialogue range" event or does this need a range-query every N ticks? -
Q-025 formal closure. I agree with Tyre: ~30KB total at current NPC count, eviction not needed for v0.1/v0.2. Qatux, please record formal closure of Q-025 when this workshop produces its D-record.