Files
settled-reach/docs/workshops/knowledge-flow-npc-boundaries/dudley-round1.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

30 KiB

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Workshop Round 1 — Dudley: Implementation Feasibility Analysis Dudley's Round 1 analysis of Rust implementation feasibility, ECS integration, and edge cases for knowledge flow systems workshop archived knowledge-flow-npc-boundaries dudley 1 2026-02-23

Workshop Round 1 — Dudley: Implementation Feasibility Analysis

Workshop: Knowledge Flow & NPC Information Boundaries Author: Dudley (Server Developer) Date: 2026-02-23 Focus: Rust implementation feasibility, integration with existing systems, edge cases


Framing

After reading all referenced files, I can confirm Tyre's audit is accurate and complete. The simulation guarantees state consistency for everything it already touches — the problem is the empty spaces where state transitions have no code to run. My job here is to determine the precise implementation shape for each gap, flag edge cases, and push back on anything that will create determinism problems.

One constraint runs through all five topics: every state mutation must flow through an event or a system, never through direct component mutation in a calling system. This is D-010 principle 4. Any design that requires "update the KG in the dialogue handler" instead of "push an event for the KG system to process" is inadmissible. I'll note where this creates architectural pressure.


Topic 1: Knowledge Flow — The Grant Mechanism

State of the code

server/src/content/types.rs lines 494-498:

pub struct KnowledgeGrant {
    pub fact_id: String,
    pub confidence: String,
}

server/src/content/line_pool.rs line 297:

pub knowledge_grant: Option<types::KnowledgeGrant>,

The field is deserialized and indexed, but nothing reads it at runtime. The grant never fires.

Where the grant fires: line selection, not client display

The grant must fire server-side at line selection — not when the client displays the text. Reasons:

  1. D-010 principle 4: deterministic simulation requires state changes to occur at a specific tick, tied to an input event (the Talk verb). Client display timing is non-deterministic.
  2. The process_talk_interaction system already has access to the selected IndexedDialogueLine. This is the natural injection point.
  3. Firing on client display would require a client→server acknowledgment round-trip, adding IPC complexity.

Implementation path: After the weighted line selection in process_talk_interaction, if selected_line.knowledge_grant.is_some(), push a KnowledgeEventType::KnowledgeGranted event into KnowledgeEventQueue. The existing process_knowledge_events system (events.rs line 85) handles it on the same tick.

New event type

server/src/knowledge/events.rs — add to KnowledgeEventType enum:

KnowledgeGranted {
    recipient: Entity,         // who receives the knowledge
    fact_id: FactId,
    confidence: KnowledgeConfidence,
    source_id: StableId,       // NPC who told them (ToldBy)
}

The recipient is the observer entity (player or NPC). The source_id is the NPC's StableId — available via registry.to_stable(npc_entity) at the point of line selection.

Confidence string parsing

KnowledgeGrant.confidence is a String in the YAML schema. We need a parsing step to convert "KnowsOf"KnowledgeConfidence::KnowsOf. I recommend this at content index time (when IndexedDialogueLine is built), not at runtime grant time. A TryFrom<&str> for KnowledgeConfidence implementation on the type is ~6 lines.

If parsing fails on an unknown confidence string, the content indexer should emit a validation error and reject the YAML file. Do not silently default — silent defaults hide content author errors.

Entity grants vs fact grants

The current KnowledgeGrant schema only covers facts (fact_id). The brief asks whether entity-knowledge grants are needed.

My position: defer entity grants to Sprint 18. The mechanics required for Sprint 17 (#141, #149 POI discovery) are fact-based. Entity grants introduce a harder problem: the content author must specify the target entity by fact_id-style reference (e.g., "entity.kael") which requires a separate entity name registry that doesn't exist yet.

For THE FRIEND arc (D-034), Sera says "Kael was at the dock during second shift." The grant here is: FactId("entity.kael.location.second_shift_dock") with confidence KnowsOf. This is a fact grant that encodes the entity + location + time context as a structured fact ID. The contradiction detector can then match it against the player's direct observation. This approach works without entity grants.

The EntityKnowledge.known_attributes field (BTreeMap<String, String> in types.rs line 171) can receive population via fact grants that encode attribute updates — but this is awkward. A proper entity grant schema is a Sprint 18 design task.

POI discovery

Option (a) — FactId("poi.dock_7_restricted") — is the correct choice. Reasons:

  1. POIs are locations/things, not entities. FactKnowledge (types.rs line 177) models exactly this.
  2. filter_by_access (KnowledgeGated variant, graph.rs line 299) already gates on FactId presence. POI visibility gating works immediately with no new access control code.
  3. The "poi.*" namespace is self-documenting in YAML.
  4. No new type is needed. KnowledgeGraph.facts already stores arbitrary FactId.

The POI discovery system (#149) pushes a KnowledgeGranted event with fact_id = FactId("poi.dock_7_restricted") when the player enters the POI's trigger zone. Same event type, same queue, same processing system. No special-casing.

Physical evidence discovery

Physical evidence (terminals, documents, manifests) should use the same KnowledgeGranted event, not a separate EvidenceDiscovered type. Adding a new event type when the existing mechanism handles it cleanly violates the simulation's guarantee of a minimal, composable event set.

The rendering distinction between "NPC told you" and "you found a document" is a client-side concern. Server-side, both are KnowledgeGranted with different KnowledgeSource variants: ToldBy vs DirectObservation. The source is already tracked per-entry (types.rs line 88-102).

Content author guardrails

At content index time (when LinePoolIndex is built), validate:

  1. confidence string parses to a valid KnowledgeConfidence variant
  2. fact_id conforms to the "category.topic" format

Runtime enforcement (NPC can only grant facts it knows) is Tier 3 difficulty and should not be Sprint 17 scope. It requires cross-entity KG queries at line selection time, which adds significant query complexity to an already-complex system.

For Sprint 17: authoring-time validation only. Document the constraint: "NPCs should only grant facts consistent with their character background and role." This is enforced by content review, not runtime checks.

Summary: Topic 1 implementation cost

  • New KnowledgeEventType::KnowledgeGranted variant: ~30 lines
  • process_knowledge_events handler for new variant: ~20 lines
  • TryFrom<&str> for KnowledgeConfidence parsing: ~10 lines
  • Hook in process_talk_interaction after line selection: ~15 lines
  • Content index validation: ~20 lines
  • Total: ~95 lines of new code, zero schema changes, zero breaking changes

Tyre's 2-day estimate is accurate for the fact-only path.


Topic 2: NPC-to-NPC Knowledge Propagation (Q-024)

The conversation system is the right hook

run_npc_conversations at server/src/simulation/conversation.rs line 278+ already provides:

  • Proximity pairing with deterministic StableId sort (line 321-324)
  • Cooldown management (line 314-319)
  • Conversation lifecycle (start tick, end tick, line timing)

This satisfies "queued at routine intersections." No separate system needed for the pairing logic.

The critical ECS constraint

run_npc_conversations currently queries NPCs with:

npc_query: Query<(Entity, &TilePosition, Option<&NpcName>, ...), (With<Npc>, With<ActiveSim>)>

There is no &mut KnowledgeGraph in this query. To perform knowledge transfer, we need mutable access to the KGs of both conversation participants. Bevy ECS does not allow two mutable borrows of the same component type from a single query in the same system. This is the primary structural constraint.

Solution: separate transfer_npc_knowledge system. This system runs immediately after run_npc_conversations completes, reads the NpcConversation components (which identify both participants), and performs the transfer. The system ordering guarantee: transfer_npc_knowledge.after(run_npc_conversations) ensures transfer happens on the same tick as conversation initiation.

The transfer system structure:

pub fn transfer_npc_knowledge(
    time: Res<SimulationTime>,
    conversations: Query<(Entity, &NpcConversation, Option<&StableEntityId>), With<Npc>>,
    mut kg_query: Query<&mut KnowledgeGraph>,
    relationship_query: Query<&Relationships, With<Npc>>,
    mut event_queue: ResMut<KnowledgeEventQueue>,
) { ... }

Using kg_query.get_many_mut([entity_a, entity_b]) — bevy_ecs supports this for distinct entities.

Trust-gated filtering and confidence downgrade

Trust tier mapping (D-028 to NPC-NPC transfer):

Trust level What A shares with B
surface (trust_level 0-2) KnowledgeState::Active facts with confidence >= KnowsOf only
real (trust_level 3-6) Also Suspects facts + entity knowledge at KnowsOf or above
secret (trust_level 7-10) Also entity knowledge at Suspects (rumors)

Confidence downgrade:

let transferred_confidence = source_entry.confidence.min(KnowledgeConfidence::KnowsOf);

This works because KnowledgeConfidence derives Ord with load-bearing ordering assertions at types.rs lines 50-55. Direct downgrades to KnowsOf. KnowsDetails downgrades to KnowsOf. Suspects stays Suspects. KnowsOf stays KnowsOf. The cap is clean and prevents knowledge amplification through gossip chains.

Rate limiting

1-3 facts per conversation, drawn via rng.rng.random_range(1..=3) at conversation start. The cap goes in NpcConversation:

pub struct NpcConversation {
    ...
    pub knowledge_transfers_remaining: u8,  // set at start, decremented per fact
}

Transfer happens once per conversation (at conversation start tick), not on each line emission. The conversation is already tracked; we transfer when NpcConversation is first attached.

ToldBy source construction

KnowledgeSource::ToldBy { source_id: StableId, tick: u64 } (types.rs line 97) is defined but never constructed. Construction point:

let source_sid = registry.to_stable(entity_a)
    .expect("NPC in conversation must be registered");
let source = KnowledgeSource::ToldBy {
    source_id: source_sid,
    tick: time.tick,
};

The StableId is available. The tick is in SimulationTime. This is a 2-line construction. The expect() is justified — any NPC in an active conversation must have been registered at spawn.

Overheard knowledge by player

The overheard grant should fire for the player entity when distance <= VOICE_RANGE_TILES AND the conversation had a knowledge transfer. The player's confidence should be Suspects unconditionally — the player doesn't know what was said, only that information was exchanged.

Implementation constraint: the player's overheard grant requires knowing which facts NPC A transferred to NPC B during the conversation. This means the transfer system must also push a KnowledgeGranted event for the player (or: record the transfer in a RecentConversationTransfer resource that the observer system reads).

For Sprint 17: overheard knowledge at Suspects for the NPC entity (not the specific facts). The player learns "Kael was being discussed" not "Kael was discussed in relation to dock schedules." The specific fact transfer to player can wait for Sprint 18 when content-authored conversation lines replace placeholder lines.

Summary: Topic 2 implementation cost

  • New transfer_npc_knowledge system: ~80 lines
  • NpcConversation struct addition (knowledge_transfers_remaining): ~5 lines
  • Trust-tier filter helper: ~30 lines
  • Overheard entity grant (Suspects confidence): ~20 lines
  • Total: ~135 lines, no breaking changes

Tier 1 for the core transfer. Tier 2 for overheard fact precision. Q-024 resolves with the core transfer.


Topic 3: Unprompted Disclosure Design (#172)

Architecture constraint: where does disclosure live?

The dialogue pipeline layers in D-028 are: access → situation → trust → topic+mood scoring. Layer 4 is "unprompted disclosure." Currently Layer 4 only fires when the player initiates (Talk verb creates TalkRequest).

For unprompted disclosure, the NPC initiates. This means:

  1. A new system derive_disclosure_candidates runs each tick on Active-tier NPCs with a KG
  2. It writes DisclosureCandidates component (new component, per-NPC)
  3. A new system process_unprompted_disclosure checks whether conditions are met, then pushes a dialogue line to DialogueResponseBuffer (or a new UnpromptedDisclosureBuffer)

DerivedTellState vs separate component

I recommend a separate DisclosureCandidates component rather than adding disclosure_candidates: Vec<FactId> to DerivedTellState. Reasons:

  1. DerivedTellState is read by the observer snapshot system — adding a Vec<FactId> to it would serialize unnecessary data over the bridge
  2. DisclosureCandidates may be empty (and often will be) — keeping it separate avoids allocating a Vec on every Active NPC every tick
#[derive(Component, Debug, Default)]
pub struct DisclosureCandidates {
    pub candidates: Vec<FactId>,
    pub last_derived_tick: u64,
}

Candidate selection algorithm

For each Active NPC with KG:
  candidates = npc_kg.known_facts_iter()
    .filter(|(_, fact)| fact.state == KnowledgeState::Active)
    .filter(|(_, fact)| fact.confidence >= KnowsOf)
    .filter(|(fact_id, _)| !matches trust gate against player relationship)
    .take(3)
    .collect()

The NPC does NOT check the player's KG (Option A). This is:

  1. Correct per D-010 principle 3 — no entity-special-casing
  2. Realistic — NPCs don't know what you know
  3. Architecturally clean — avoids cross-entity KG queries in a per-NPC system

Repeated disclosure of known facts is acceptable. The rate limiting (see below) prevents spam.

Trait filtering (#173)

From my implementation perspective: traits should filter WHAT is disclosed, not just HOW. The reason is architectural: if traits only affect delivery, they're a line-pool modifier and we already have that mechanism via tags. But if a cautious NPC systematically withholds certain fact categories, that requires filtering the DisclosureCandidates set — a KG-level operation.

Implementation: traits become a candidate_filter function applied to the candidates list. A Cautious trait drops facts with source == ToldBy (NPC won't pass on rumors). A Gossipy trait includes Suspects confidence facts. This is a trait-to-filter-predicate mapping, ~20 lines per trait.

Trigger conditions

My proposed conditions (as system checks in process_unprompted_disclosure):

  1. trust_toward_player >= SURFACE_THRESHOLD (read from NPC's Relationships component)
  2. mood_state != Hostile (hostile NPCs don't volunteer info)
  3. DisclosureCandidates.candidates.is_not_empty()
  4. Per-NPC disclosure cooldown not active (new DisclosureCooldown component)

The SURFACE_THRESHOLD should be configured per-NPC role (not global). Content-authored as a YAML field.

Rate limiting

The existing LINE_COOLDOWN_TICKS = 600 (dialogue.rs line 39) applies to individual lines. For unprompted disclosure, we also need a per-NPC cooldown to prevent one NPC bombarding the player:

#[derive(Component, Debug)]
pub struct DisclosureCooldown {
    pub until_tick: u64,
    pub last_disclosed_facts: BTreeSet<FactId>,  // prevent fact repetition
}

The last_disclosed_facts set caps per-fact disclosure rate. Once a fact has been disclosed, it's excluded from candidates until the cooldown expires.

Global rate limit (prevent disclosure spam from multiple NPCs simultaneously): a per-tick counter on SimulationTime or a resource DisclosureThisTick: u8. Cap at 1 unprompted disclosure per tick globally. Simple, deterministic.

Summary: Topic 3 implementation cost

  • DisclosureCandidates component: ~15 lines
  • derive_disclosure_candidates system: ~50 lines
  • DisclosureCooldown component: ~15 lines
  • process_unprompted_disclosure system: ~60 lines
  • Trait filter predicates (3 basic traits): ~40 lines
  • Total: ~180 lines, requires Paula + Gestalt to specify candidate selection criteria before implementation

Tier 2. The pipeline exists; the NPC-side candidate derivation is the new work. Cannot implement until this workshop produces: (a) the disclosure candidate selection algorithm and (b) the trait filter/delivery split decision.


Topic 4: NPC Information Boundaries (#142)

Priority order with implementation complexity

System Complexity Value
tell_state.rs (add KG awareness) Tier 1 High
conversation.rs (KG-aware partner selection) Tier 2 Medium
routine.rs (KG-aware routine execution) Tier 3 Low for v0.1
path_follow.rs (KG-aware pathfinding) Tier 3 Risky — see below

Minimum viable boundary: tell_state.rs

The derive_tell_state system at tell_state.rs line 129 queries six components. Adding &KnowledgeGraph to the query is a one-line addition:

pub fn derive_tell_state(
    mut npcs: Query<
        (
            &Secret,
            &ToleranceThreshold,
            &Contentment,
            &MoodState,
            Option<&Relationships>,
            Option<&RoutineDeviation>,
            Option<&KnowledgeGraph>,   // new
            &mut DerivedTellState,
        ),
        (With<Npc>, With<ActiveSim>),
    >,
) {

The KG awareness change: suppress Guarded and Nervous tells if the NPC has FactKnowledge for "meta.secret.safe" or similar fact indicating their secret hasn't been threatened. This is a KG-gate on tell derivation. Practically: if the NPC's own KG has no entry suggesting the secret is at risk, show fewer tells.

More immediately useful: an NPC that KNOWS the player suspects them (because it has a ToldBy entry from another NPC saying "the detective is asking about you") should have elevated stress contributing to tell derivation. This is a new compute path, not just a filter.

For Sprint 17 minimum viable: add Option<&KnowledgeGraph> to the query but only use it as a null check — NPCs without a KG retain current axis-based behavior, NPCs with a KG can have their tell overridden by specific KG facts. This preserves all existing tests while opening the KG pathway.

Conversation partner selection

Adding KG awareness to partner selection requires checking: "does NPC A know NPC B exists (i.e., has an EntityKnowledge entry for B's StableId)?"

The pairing loop at conversation.rs lines 332-371 currently pairs any two proximate eligible NPCs. With KG: pair only if npc_a_kg.knows_entity(&npc_b_sid).

Edge case: brand-new NPCs have empty KGs. If neither NPC knows the other, they never converse — which breaks the natural "first meeting" scenario. Resolution: NPCs can converse with Unknown entities (proximity-driven meeting), but knowledge transfer on first meeting is minimal (only public facts, regardless of trust tier). Trust tier defaults to surface when there is no relationship history.

The query constraint: run_npc_conversations at line 284 already has 8 fields in the query tuple. Adding Option<&KnowledgeGraph> makes 9. Bevy ECS supports this but query ergonomics degrade. The conversion to KG-aware pairing is Tier 2 because of the query complexity, not because the logic is hard.

Pathfinding: DO NOT retrofit for v0.1

path_follow.rs using KG for walkability creates a failure mode with no safe recovery path in v0.1: NPC forgets a path node is blocked → NPC navigates into a blocked tile → physics/movement system conflicts → undefined behavior. The fallback must be ground truth, and if the fallback is always ground truth, the KG check adds complexity without behavioral change.

Recommendation: no pathfinding KG retrofit until v0.2. Document as deferred decision.

D-026 tier interaction

Background-tier NPCs should receive NO KG-based boundary changes. The With<ActiveSim> filters in both derive_tell_state and run_npc_conversations already enforce this. The tier boundary is already correct in the existing code.

Summary: Topic 4 implementation cost (MVP only)

  • Add Option<&KnowledgeGraph> to derive_tell_state query + minimal KG gate: ~25 lines
  • KG-aware conversation partner matching (Sprint 17 scope decision needed): ~30 lines
  • Total MVP: ~25-55 lines depending on scope decision

Tier 1 for tell_state MVP. Tier 2 for conversation pairing. Pathfinding deferred.


Topic 5: Contradiction Detection Pipeline (Q-026)

Prerequisite dependency is real and hard

The detection algorithm requires ToldBy entries. Without Topics 1 and 2 being implemented first:

  • KnowledgeSource::ToldBy is never constructed (confirmed — it exists at types.rs line 97 but no code calls it)
  • There is nothing to contradict against DirectObservation entries
  • The detection system would scan KGs and find zero contradiction candidates on every tick

The simulation guarantees: if ToldBy sources don't exist, detect_contradictions is a no-op. It will not error. But it will also do nothing useful. Topics 1 and 2 are genuinely blocking.

Location contradiction algorithm

For each entity E in observer KG where last_known_position.is_some():

// Check all knowledge entries for E
// (currently one entry per entity — extension needed for multi-source tracking)

Critical architectural gap: EntityKnowledge (types.rs line 154) is a single struct per target entity. It holds ONE last_known_position, ONE source, ONE confidence. It cannot represent "NPC said X was at Dock" AND "I saw X at Corridor B-7" simultaneously — the second observation overwrites the first.

This is the core design problem for contradiction detection. To detect contradictions, we need to retain multiple conflicting observations. Options:

A. Add Vec<PositionClaim> to EntityKnowledge alongside the primary entry B. Extend the known_attributes map with structured claim keys (e.g., "claim.tick.1234.position" = "dock_7") C. Create a separate ContradictionCandidate component attached to entities that need cross-source comparison

My recommendation: Option B for Sprint 17 (lowest structural impact). Encode position claims as structured attribute strings. The contradiction detector parses them. Ugly but zero schema change. Option A is cleaner and should be the Sprint 18 refactor target.

Option B implementation:

key: "claim.{tick}.position"
value: "{x},{y},{z},{source_stable_id}"

Contradiction detection then iterates known_attributes keys matching "claim.*", parses the values, and checks for conflicting positions within a configurable tick window.

Detection timing

Event-driven is correct. Run detect_contradictions as part of process_knowledge_events handling — check for contradictions only when a new KnowledgeGranted or DirectObservation event is processed. Do not run as a separate per-tick system.

Implementation: after applying a KnowledgeGranted event to the KG, call check_for_contradictions(&mut kg, target_stable_id, tick) inline. This is ~40 lines of detection logic added to process_knowledge_events.

Event emission chain for THE FRIEND arc

Walk through the full sequence with the proposed design:

  1. Dialogue selection tick T1: Sera's line "Kael was at the dock during second shift" is selected. knowledge_grant on the line fires → KnowledgeGranted { fact_id: FactId("entity.kael.position.second_shift"), confidence: KnowsOf, source_id: sera_sid } pushed to queue.

  2. process_knowledge_events tick T1: Event processed → kg.facts.insert(FactId("entity.kael.position.second_shift"), FactKnowledge { confidence: KnowsOf, source: ToldBy { source_id: sera_sid, tick: T1 }, ... }). Also: add structured attribute claim to Kael's EntityKnowledge: "claim.T1.position" = "dock_7,T1,sera_sid".

  3. detect_contradictions check tick T1: No DirectObservation of Kael at a different position yet. No contradiction.

  4. DirectObservation tick T2: Player sees Kael in corridor B-7. DirectObservation event → observe_entity(kael_sid, corridor_B7, T2). Also adds structured attribute claim: "claim.T2.position" = "corridor_b7,T2,direct".

  5. detect_contradictions check tick T2: Parses claims for Kael. "claim.T1.position" = "dock_7" vs "claim.T2.position" = "corridor_b7". T2 - T1 < contradiction_window_ticks. Positions differ → CONTRADICTION DETECTED. Both KG entries receive KnowledgeState::Contradicted. ContradictionDetected event pushed (new event type, or reuse KnowledgeEventQueue with a new variant).

  6. detect_anomalies tick T3: Iterates player KG, finds Kael with state == Contradicted → attaches AnomalyMarker to Kael entity. (anomaly.rs line 64 — this fires on Contradicted already.)

  7. Monologue system tick T3+: ContradictionDetected event triggers selection of contradiction monologue line: "Sera said Kael was at the dock. I just saw him in B-7." (Content team writes this line; the trigger tag fires here.)

  8. Relationship update: set_relationship(&kael_sid, RelationshipState::PersonOfInterest) → D-033 amber color transition.

The simulation guarantees this chain fires correctly given the proposed event additions. The existing downstream consumers (anomaly.rs, monologue.rs) already handle Contradicted state — tested and passing.

Attribute contradiction

known_attributes: BTreeMap<String, String> is untyped (types.rs line 171). Typed attribute keys are a Sprint 18 concern. For Sprint 17:

  • Location contradictions: algorithmic (position claim parsing as described above)
  • Attribute contradictions: content-authored only — a YAML file defines explicit contradiction pairs. The detector checks a lookup table: if known_attributes.get("role") == Some("legitimate") && kg.knows_fact(&FactId("entity.kael.smuggler")) → CONTRADICTION.

This is a simple O(N) check per KG update, where N is the authored contradiction pair count.

New types needed

// In events.rs
KnowledgeEventType::ContradictionDetected {
    observer: Entity,
    entity_a: StableId,
    entry_a_source: KnowledgeSource,
    entity_b: StableId,        // same as entity_a for location contradictions
    entry_b_source: KnowledgeSource,
    contradiction_type: ContradictionType,
}

pub enum ContradictionType {
    Location,
    Attribute { key: String },
    FactConflict { fact_id_a: FactId, fact_id_b: FactId },
}

The monologue system subscribes to ContradictionDetected events and selects the appropriate content line based on the ContradictionType.

Summary: Topic 5 implementation cost

  • EntityKnowledge structured claim addition (Option B): ~15 lines type + ~20 lines claim recording
  • detect_contradictions function (location only): ~60 lines
  • ContradictionDetected event variant + type: ~20 lines
  • Monologue subscription to contradiction events: ~30 lines
  • Content-authored attribute contradiction table: ~25 lines + YAML schema
  • Total: ~170 lines, plus content schema

Tyre's 3-day estimate for location detection + event chain is correct. The critical gap is the EntityKnowledge single-entry model — this must be addressed (Option B minimally, Option A properly) before detection can work.


Cross-Topic Dependencies and Implementation Order

The simulation guarantees a specific ordering constraint:

Topic 1 (KnowledgeGranted event + dialogue wire)
    ↓ enables
Topic 2 (ToldBy source construction + NPC-to-NPC transfer)
    ↓ enables
Topic 5 (contradiction detection — needs ToldBy entries to exist)

Topics 3 and 4 are partially independent:

  • Topic 4 MVP (tell_state.rs KG awareness) can be implemented in parallel with Topic 1
  • Topic 3 (DisclosureCandidates system) requires Topic 2 to have populated NPC KGs with meaningful content
  • Topic 3 requires Paula + Gestalt decisions on candidate selection and trait behavior before implementation

Recommended implementation order for Sprint 17:

  1. Topic 1 fact grant mechanism (unblocks everything)
  2. Topic 4 MVP (tell_state KG awareness — isolated, low risk)
  3. Topic 2 NPC-to-NPC transfer (unblocks Topic 5)
  4. Topic 5 location contradiction detection (immediately useful for THE FRIEND arc)
  5. Topic 3 unprompted disclosure (requires decisions from this workshop + populated NPC KGs)

Open Positions for Round 2 Discussion

  1. Entity grant schema: I've proposed deferring to Sprint 18 and encoding entity-knowledge grants as structured FactIds. Tyre's opinion needed on whether the single-entry EntityKnowledge model needs an architectural fix before Sprint 17 or can carry the Option B workaround.

  2. EntityKnowledge multi-source model: The contradiction detection gap (single entry per entity) is the most structurally significant problem in this workshop. My Option B (structured attributes) is a workaround. What's the correct path to Option A (proper multi-claim model)?

  3. Overheard knowledge precision: I've proposed Suspects confidence for overheard NPC-NPC conversations regardless of occlusion fidelity. Is this the right tradeoff, or does the game need per-fact overheard grants for a mechanically satisfying experience?

  4. Trait filtering (#173): My position is traits should filter WHAT is disclosed AND HOW it is delivered. Paula needs to confirm whether the narrative design requires both, or if HOW-only is sufficient for the first NPC characters.

  5. Q-025 formal closure: Tyre's 30KB memory analysis is correct at current NPC counts. I support formal closure. At 500+ Active NPCs, the question should be reopened. This can be a one-line decision.