Standardized YAML frontmatter on all 12 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Round 2 Notes — Knowledge Flow & NPC Information Boundaries Workshop | Qatux's Round 2 synthesis documenting tension resolutions, entity grant split outcome, and D-record readiness | workshop | archived | knowledge-flow-npc-boundaries | qatux | 2 | 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:
-
Resolver-compatible FactId naming.
FactId("entity.kael-davan.location.second-shift-dock")requires the contradiction detector to map the slug"kael-davan"→kael_sidat 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. -
ToldBy source flows through
ContradictionDetectedevent. The event must include the fullKnowledgeSource::ToldBy { source_id: sera_sid, tick }so the monologue system can name Sera. Dudley's Round 1 event sketch already includesentry_a_source: KnowledgeSource— if this is preserved, Condition 2 is satisfied. -
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
EntityKnowledgeentry 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<ContradictionClaim> 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):
pub struct ContradictionClaim {
pub source: KnowledgeSource, // Who made the contradicted claim (ToldBy source)
pub claimed_position: Option<TilePosition>,
pub detected_at_tick: u64,
}
// Added to EntityKnowledge:
pub contradicted_claim: Option<ContradictionClaim>, // 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_attributesis 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: boolonFactKnowledge(field on struct) - Paula:
disclosure_eligible: falsein 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 issecrettier (or NPC hasTalkativetrait 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<String>, // resolved at detection time
subject_display_name: Option<String>, // 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:
-
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::Confusionfor NPCs surfacing their own contradicted knowledge. -
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.
-
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:
-
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.
-
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<ContradictionClaim> 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 |