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>
522 lines
30 KiB
Markdown
522 lines
30 KiB
Markdown
# 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:
|
|
```rust
|
|
pub struct KnowledgeGrant {
|
|
pub fact_id: String,
|
|
pub confidence: String,
|
|
}
|
|
```
|
|
|
|
`server/src/content/line_pool.rs` line 297:
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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:
|
|
```rust
|
|
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:
|
|
```rust
|
|
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:
|
|
```rust
|
|
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`:
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
#[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:
|
|
|
|
```rust
|
|
#[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:
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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.
|