Files
settled-reach/docs/workshops/knowledge-flow-npc-boundaries/round-2-notes.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

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:

  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<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_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<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:

  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<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