--- title: "Round 2 Notes — Knowledge Flow & NPC Information Boundaries Workshop" description: "Qatux's Round 2 synthesis documenting tension resolutions, entity grant split outcome, and D-record readiness" type: workshop status: archived workshop: knowledge-flow-npc-boundaries agent: "qatux" round: 2 created: 2026-02-24 --- # Round 2 Notes — Knowledge Flow & NPC Information Boundaries Workshop **Documented by:** Qatux **Date:** 2026-02-24 **Sources:** tyre-round2.md, gestalt-round2.md, dudley-round2.md, paula-round2.md --- ## Summary Both principal tensions are resolved. Tension B (EntityKnowledge structural fix) resolved unanimously to the typed struct. Tension A (entity grants Sprint 17 vs 18) resolved **unexpectedly** — three engineers converged on entity grants in Sprint 17 via `ContentEntityRegistry`, while Paula (who originally called entity grants "Critical") accepted Dudley's FactId workaround with three binding conditions. All secondary disputes are resolved or conceded. The workshop is ready for formal D-record production. --- ## Tension A Resolution: Entity Grants — Split Outcome **Result: 3-1 split (not a simple resolution).** | Participant | Round 2 Position | Changed from Round 1? | |-------------|-----------------|----------------------| | **Tyre** | Entity grants Sprint 17 via `ContentNameRegistry` (BTreeMap, ~30 lines). FactId workaround breaks contradiction chain — the two data maps never intersect. | No change | | **Gestalt** | Entity grants Sprint 17, narrow scope: `EntityPositionGrant` only (full `EntityGrant { attributes }` defers to Sprint 18). Static lookup against existing `entity-attributes.yaml`. | Refined (scoped narrower) | | **Dudley** | **Changed to entity grants Sprint 17.** `ContentEntityRegistry` resource, spawn-time registration (~60 lines total). Confirmed FactId workaround breaks because `observe_entity` contradiction check is in the `entities` map — FactKnowledge (in `facts` map) is never compared against it. | **Major change** | | **Paula** | **Accepted FactId workaround with 3 conditions.** The conditions are binding; if any fail, entity grants are required. | Reversed from Round 1 | **The unexpected inversion:** Paula (Round 1: entity grants Critical, non-negotiable) accepted the workaround. The engineers (Round 1: split on feasibility) converged on entity grants as the cleaner path. ### Paula's 3 conditions on the FactId workaround These conditions are binding. If any are not met, the workaround fails and entity grants are required for Sprint 17: 1. **Resolver-compatible FactId naming.** `FactId("entity.kael-davan.location.second-shift-dock")` requires the contradiction detector to map the slug `"kael-davan"` → `kael_sid` at runtime. NpcName must carry a canonical slug field, or a separate slug-to-StableId lookup must exist. This path must be confirmed before implementation begins. 2. **ToldBy source flows through `ContradictionDetected` event.** The event must include the full `KnowledgeSource::ToldBy { source_id: sera_sid, tick }` so the monologue system can name Sera. Dudley's Round 1 event sketch already includes `entry_a_source: KnowledgeSource` — if this is preserved, Condition 2 is satisfied. 3. **Sprint 18 entity grants are a formal commitment, not an aspiration.** The D-record for this workshop must record Sprint 18 entity grants as committed scope. The FactId workaround leaves the player without an `EntityKnowledge` entry for Kael from hearsay alone (they only gain one on direct observation), which limits future systems. **For the record:** Tyre argues that even if Paula's 3 conditions are met, the cross-structure lookup (FactKnowledge checked against EntityKnowledge entries) is more complex than simply writing the entity grant correctly. The workaround requires the contradiction detector to scan the `facts` map for `"entity.*"` prefixed entries and cross-reference against `entities` — a dependency the typed entity grant avoids entirely. Gestalt registers a standing dissent: "If the team decides to defer entity grants, the decision should record that THE FRIEND arc contradiction sequence using `EntityKnowledge.ToldBy` cannot fire in v0.1 — not that we found a workaround." **Workshop leaves this split for the team lead to resolve.** The implementation path is either: - (A) Entity grants + ContentEntityRegistry (Tyre/Gestalt/Dudley, ~60 lines extra Sprint 17 infra) - (B) FactId workaround (Paula accepts, contingent on 3 conditions) + entity grants committed to Sprint 18 --- ## Tension B Resolution: `ContradictionClaim` Struct — Unanimous **Result: Full consensus. Dudley conceded Option B.** | Participant | Position | |-------------|---------| | **Tyre** | `contradicted_claim: Option` on `EntityKnowledge`. Naming: `ContradictionClaim`. `claimed_position` field. | | **Gestalt** | Same architecture. Defers to Tyre's naming: `ContradictionClaim`. | | **Dudley** | **Concedes Option B.** String parsing is fragile, pollutes `known_attributes`, and requires Sprint 18 refactor. Adopts `ContradictionClaim` struct. | | **Paula** | Supports typed struct; her preference for field name `prior_claim` noted but not held firm. | **Agreed struct (Tyre/Dudley final spec):** ```rust pub struct ContradictionClaim { pub source: KnowledgeSource, // Who made the contradicted claim (ToldBy source) pub claimed_position: Option, pub detected_at_tick: u64, } // Added to EntityKnowledge: pub contradicted_claim: Option, // None until contradiction fires ``` **Why Dudley conceded Option B:** - String parsing in contradiction detection hot path is fragile (malformed string = silent failure) - Monologue system needs typed `ToldBy { source_id }` for source attribution — a parsed string adds a second fragility point - `known_attributes` is semantic NPC metadata; filling it with internal bookkeeping strings mixes concerns - One optional field with serde default is not a meaningful schema change (all existing state deserializes with `None`) --- ## Secondary Dispute Resolutions ### Global disclosure rate limit **Round 1:** 3-for (Tyre, Dudley, Paula), 1-against (Gestalt). **Round 2 movement:** - Paula **withdraws** her support: "The trigger conditions being strict enough makes simultaneous disclosure rare by design rather than requiring a hard cap." Agrees with Gestalt. - Gestalt **partially concedes**: "I accept the global rate limit as a degenerate-case safeguard, with one condition: the winning NPC must be selected by deterministic StableId ordering (D-010 principle 4), not random." - Dudley **maintains**: 1 per tick, StableId-ordered. Addresses Gestalt's invisible-competition concern (player watches one NPC; doesn't observe others not-speaking). - Tyre **maintains**: per-game-minute (1 per 10 ticks), not per-tick. **Resolution:** Include global rate limit. Deterministic selection by ascending StableId. Per-game-minute granularity (1 per 10 ticks, per Tyre; more conservative than per-tick). This means in practice the limit only fires when 2+ NPCs have simultaneous first-time disclosures in the same game-minute — a degenerate edge case. Normal play is governed by per-NPC cooldowns (300 ticks). ### Contradiction window (`CONTRADICTION_WINDOW_TICKS`) | Participant | Proposal | |-------------|---------| | Gestalt | 600 ticks (1 game-hour) | | Tyre | 1800 ticks (3 game-hours) | | Dudley | 600 ticks (1 game-hour) | | Paula | 600 default, configurable per contradiction type | **Resolution:** 600-tick default. Configurable per contradiction type via content authoring. A schedule-based claim can carry a longer window than a "just saw them" claim. `const CONTRADICTION_WINDOW_TICKS: u64 = 600` as the default constant. Tyre's 1800-tick argument (shift coverage + travel time) noted; the playtest will determine whether 600 is too tight. ### Trust-weighted transfer count (gossip) **Paula's proposal** (Surface: 0-1, Real: 1-2, Secret: 2-3) accepted by Dudley; others not opposed. **Paula withdraws** in Round 2: flat `rand.random_range(1..=3)` is sufficient. Trust tier already gates which facts are eligible; the count cap is a ceiling on an already-filtered pool. **Resolution:** Flat `rng.random_range(1..=3)` per conversation, confirmed by all. ### `disclosure_blocked` / `disclosure_eligible` flag All four participants agree a per-entry flag is needed for Major secrets (Kael's ring membership). Naming differs: - Dudley/Tyre: `disclosure_blocked: bool` on `FactKnowledge` (field on struct) - Paula: `disclosure_eligible: false` in authored YAML **Resolution:** `disclosure_blocked: bool` on `FactKnowledge` (struct field, default `false`). Content authors set `disclosure_blocked: true` on authored KG entries for Major secrets in NPC profile YAML. The field name inverted to `_blocked` for cleaner boolean logic in the filter (`if fact.disclosure_blocked { continue }`). ### Witness inhibition + location privacy gates Both confirmed for v0.1 trigger gate. Additive, not competing. - **Witness inhibition** (Gestalt): count Active NPCs within 5 tiles; if > 2, suppress `real`/`secret`-tier disclosures unless NPC-player trust is `secret` tier (or NPC has `Talkative` trait override). NPC's trust toward nearby NPCs (not toward player) determines "witness" — Sera won't confide in front of coworkers she doesn't trust, regardless of her trust for the detective. - **Location privacy** (Paula): `disclosure_context: [private, semi_private, any]` tag on authored disclosure candidates. Check fires at Layer 4 line selection, not at candidate derivation. Private-tagged candidates do not fire at public zones (e.g., The Terminal). ### Runtime NPC KG guardrail - Tyre Round 1: runtime check Sprint 17 (3 lines) - Dudley Round 1: Tier 3, defer - Dudley Round 2: **recalibrates to include** — "once entity grant architecture is in place, the 3-line check is straightforward" - Gestalt: runtime + content-load both Sprint 17 - Paula: authoring-time mandatory **Resolution:** Both content-load validation (parse confidence strings, validate entity_refs) AND runtime guardrail (3-line check: if granting NPC's KG doesn't contain the fact, skip + warn) in Sprint 17. ### Monologue StableId → NpcName resolution **Dudley confirms:** `EntityRegistry + Query<&NpcName>` is accessible from the monologue system with a ~5-line signature addition. The `resolve_name(source_id, registry, name_query)` helper is ~10 lines. **Paula's additional proposal:** Pre-resolve display names into the `ContradictionDetected` event payload at detection time. This makes the monologue system a pure string consumer with no ECS query needed: ``` ContradictionDetected { ..., source_display_name: Option, // resolved at detection time subject_display_name: Option, // resolved at detection time } ``` **Resolution:** Pre-resolve names into event payload (Paula's approach). Simpler monologue system, single resolution point. Fallback: `"Unknown({})"` if entity not in registry. ### DisclosureCandidates compute trigger **Dudley confirms:** No "player entered dialogue range" event exists. Range-query per tick in Active NPC processing loop — O(N_active) position comparisons (~60 at max). Negligible. `DisclosureCandidates` component as cache, freshness checked by `computed_tick` field (expires after 30 ticks / 3 game-minutes). ### Contradiction monologue authoring **Paula's specification (final):** **Option 3 adopted:** Generic fallback template + hand-authored override for FRIEND-pattern NPCs. - FRIEND NPCs (Sera, Kael): hand-authored lines per relationship phase. Phase 2 primary: *"Sera said Kael was at the dock intake during second shift. I'm looking at him in corridor B-7 right now."* Secondary beat: *"One of them is wrong. Sera, or what I'm seeing. Or I'm missing something I don't have yet."* - Auto-generated NPC generic fallback: *"{source_name} placed {subject_name} at {claimed_location}. I just saw them at {actual_location}."* - Authoring principle: cognitive dissonance, not accusation. The engine marks uncertainty; the detective reflects uncertainty. The player makes accusations. This is a specification for Mellanie. It is unblocked once Dudley confirms the name resolution path (now confirmed above). ### Disclosure candidate selection algorithm **Gestalt provides complete specification** (pseudocode in gestalt-round2.md §"Disclosure Candidate Selection Algorithm"). Paula asked to confirm narrative assumptions: 1. **Contradicted facts excluded from candidates** — Gestalt's proposal. Paula's Round 2 is silent on this specific item (confirming by non-objection). Excluded in v0.1; future sprint may add `DisclosureMode::Confusion` for NPCs surfacing their own contradicted knowledge. 2. **Sera's Phase 2 disclosures are FactId entries** — the compound grant mechanism creates EntityKnowledge as a side effect. Paula's canonical sequence uses FactId grants, consistent with Gestalt's algorithm. 3. **Witness inhibition "trusted" means NPC's trust toward nearby NPC** — not player's trust toward NPC. Gestalt and Paula agree on this framing. --- ## Positions That Did NOT Change This Round For the record: the following Round 1 consensus positions were confirmed without challenge in Round 2: - Option A (NPC checks own KG only for disclosure) — reaffirmed by all - Event-driven contradiction detection at KG write time — reaffirmed - Both entries marked `Contradicted` (epistemic neutrality) — reaffirmed - POI discovery via `FactId("poi.*")` — reaffirmed - Confidence cap `min(source_confidence, KnowsOf)` on gossip — reaffirmed - Background-tier NPCs get no KG-driven behavior — reaffirmed - No pathfinding KG retrofit — reaffirmed - Q-025 close (memory not a constraint) — reaffirmed --- ## Standing Dissent Register Items where a participant registered formal dissent in Round 2 that was not resolved: 1. **Gestalt (Tension A):** "If the team decides to defer entity grants, the decision should be recorded as 'THE FRIEND arc contradiction sequence cannot fire using EntityKnowledge.ToldBy in v0.1' — not that we found a workaround." This dissent stands regardless of which path is chosen. If (B) is selected, the D-record must accurately describe the scope limitation. 2. **Tyre (Tension A):** FactId workaround requires more code and is more fragile than entity grants + ContentNameRegistry. The workaround produces a cross-structure dependency that does not exist today. Registry cost is genuinely ~0.5 days. --- ## Open Items Still Requiring Decision | Item | Status | Who decides | |------|--------|-------------| | Entity grants Sprint 17 vs FactId workaround | **Split: 3 engineers vs Paula's conditions** | Team lead | | Paula's Condition 1 (slug → StableId resolution path) | Requires technical confirmation if workaround chosen | Dudley to confirm before implementation | | `CONTRADICTION_WINDOW_TICKS` final value (600 vs 1800) | 3-to-1 for 600; playtest will calibrate | Can ship with 600, adjust | | `Compound` grant variant (facts + entities in one line) | Deferred to Sprint 18 by all except Tyre who included it in schema | Minor — Sprint 18 either way | --- ## Summary Table: All Round 2 Decisions | Topic | Decision | Agreed by | |-------|----------|-----------| | Tension B: struct | `contradicted_claim: Option` on `EntityKnowledge` | All 4 | | `ContradictionClaim` naming | `ContradictionClaim` (Tyre's name) | Tyre, Gestalt, Dudley; Paula prefers `prior_claim` but concedes | | `claimed_position` vs `position` field | `claimed_position` | Tyre, Dudley | | `detected_at_tick` field | Include | Tyre, Dudley | | Tension A: entity grants vs workaround | Split — see above | N/A | | Gossip rate per conversation | Flat `rng.random_range(1..=3)` | All 4 | | Contradiction window default | 600 ticks (1 game-hour), configurable | Gestalt, Dudley, Paula (Tyre prefers 1800) | | Global disclosure rate limit | Include, deterministic StableId-ordered, 1/10 ticks | Tyre, Dudley, Gestalt (conditional); Paula withdrew | | Witness inhibition gate | Include in v0.1 trigger conditions | All 4 | | Location privacy gate | Include in v0.1 trigger conditions | All 4 | | `disclosure_blocked: bool` on `FactKnowledge` | Include | All 4 | | Runtime NPC KG guardrail | Include Sprint 17 | Dudley recalibrated; Gestalt; Paula | | Monologue name resolution | Pre-resolve into `ContradictionDetected` event payload | Paula (proposed); Dudley (confirmed feasible) | | Contradiction monologue authoring | Option 3: generic template + hand-authored FRIEND override | All 4 | | Q-024 formal closure | Close | All 4 | | Q-025 formal closure | Close | All 4 | | Q-026 formal closure | Close (contingent on Tension A path) | All 4 |