Files
settled-reach/docs/workshops/knowledge-flow-npc-boundaries/gestalt-round2.md
T
jpmschweitzerandClaude Sonnet 4.6 f685cb7324 chore(meta): register D-079–D-083, close Q-024/Q-025/Q-026, ticket Sprint 17 knowledge flow work
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>
2026-02-24 01:59:58 +01:00

28 KiB

Gestalt Round 2 — Cross-Review, Synthesis, and D-Record Contributions

Knowledge Flow & NPC Information Boundaries Workshop

Author: Gestalt Date: 2026-02-23 Sources reviewed: tyre-round1.md, dudley-round1.md, paula-round1.md, round-1-notes.md


Opening: What Round 1 Actually Settled

The consensus table in Qatux's notes is largely correct and the fundamentals are solid. Before the tensions: I want to note that the four-way agreement on Option A (NPC checks own KG only) and on event-driven contradiction detection is meaningful — these were the design questions with the most surface area for disagreement, and we landed in the same place independently. That's a signal worth acknowledging before we argue about the edges.

Now: the two tensions. These are the critical path. Everything else in this document is secondary.


Tension A: Entity Grants — Sprint 17 vs Sprint 18

My position: Sprint 17, but scoped narrowly.

Let me map the debate precisely, because Dudley's objection is valid but not blocking.

Dudley's concern: "entity_ref → StableId" name registry doesn't exist, Sprint 17 shouldn't take on that infrastructure.

Tyre's counter: It's a BTreeMap<String, StableId> populated at content load. 0.5 days.

Both are right, but they're talking past each other. Dudley is imagining a general entity-name-to-StableId registry that works for ANY authored entity reference in ANY dialogue line. That is Sprint 18+ scope. Tyre is imagining a narrow content-load-time map for the specific entities involved in narrative-critical dialogue. That is Sprint 17 scope.

The actual minimum for Sprint 17:

The contradiction detection chain for THE FRIEND arc requires exactly one thing that entity grants provide: EntityKnowledge for Kael in the player's KG with source: ToldBy { source_id: sera_sid }. This is not a general capability — it's a specific authoring requirement for one dialogue line in one NPC's pool.

Dudley's workaround (structured FactId "entity.kael.position.second_shift_dock") does NOT cleanly substitute for this. Paula identified the exact failure mode:

  1. Sera's dialogue grants FactId("entity.kael.position.second_shift_dock") — creates FactKnowledge entry with source: ToldBy { sera_sid }
  2. Player observes Kael in B-7 — calls observe_entity(kael_sid, B7_pos, tick) — updates EntityKnowledge
  3. Contradiction check in observe_entity: existing entry has source: DirectObservation (the player's PREVIOUS direct observation of Kael, not ToldBy!) — no contradiction fires

The workaround breaks because the player's EntityKnowledge for Kael is populated by direct observation BEFORE any testimony. The first time the player sees Kael, observe_entity creates a DirectObservation entry. When Sera then tells them "Kael was at the dock," if this is only a FactGrant, the EntityKnowledge entry still shows DirectObservation. The contradiction between Sera's testimony and the player's later observation has no anchor in EntityKnowledge.

Dudley's Option B (structured attribute strings "claim.T1.position" = "dock_7,T1,sera_sid") does address this — it adds the ToldBy position claim INTO the existing EntityKnowledge entry's known_attributes. But it introduces a new problem: brittle string parsing in a hot code path, plus the contradiction detector needs a string parser instead of typed reads.

My verdict on Tension A: Sprint 17, narrow entity grant scope.

Deliverable for Sprint 17:

  • KnowledgeGranted event supports TWO payload types: FactGrant and EntityPositionGrant
  • EntityPositionGrant { entity_ref: String, position_claim: String, confidence: KnowledgeConfidence } — resolved to StableId at content load via a static BTreeMap<String, StableId> seeded from entity-attributes.yaml (which already defines all authored entities)
  • This creates/updates EntityKnowledge for the referenced entity with source: ToldBy { source_id: speaker_sid, tick } and stores the position claim in contradiction_basis (see Tension B resolution below)
  • Scope restriction: entity_ref must reference an entity in entity-attributes.yaml — no dynamic resolution

The full EntityGrant { attributes: BTreeMap<String,String> } capability (for arbitrary attribute grants) defers to Sprint 18 as Dudley prefers.

This gets THE FRIEND arc working. It does NOT require a general entity name registry — just a static lookup against the existing entity manifest.


Tension B: EntityKnowledge Structural Fix

My position: Tyre's approach. Unify on ContradictionClaim. Reject Dudley's Option B.

Tyre and I independently proposed the same architecture with different field names:

  • Tyre: contradicted_claim: Option<ContradictionClaim>
  • Gestalt: contradiction_basis: Option<ContradictionBasis>

These are structurally identical. I defer to Tyre's naming: contradicted_claim: Option<ContradictionClaim>. The struct fields:

pub struct ContradictionClaim {
    pub source: KnowledgeSource,             // Who made the conflicting claim
    pub position: Option<TilePosition>,      // Where they claimed the entity was
    pub tick: u64,                           // When the claim was made
}

The detection fires inside observe_entity before overwriting, sets entry.state = Contradicted, populates contradicted_claim with the prior source data, then overwrites normally.

Why not Dudley's Option B?

Dudley's structured attribute approach ("claim.T1.position" = "dock_7,T1,sera_sid") has three concrete problems:

  1. String parsing in a hot path. detect_contradictions would iterate known_attributes keys matching "claim.*" and parse comma-delimited values on every KG write. This is not expensive in absolute terms (Tyre's budget analysis puts it well within budget), but it is brittle. A malformed string crashes the detector. A content author misspelling a claim key silently skips detection.

  2. known_attributes is the wrong data structure for this. It's typed BTreeMap<String, String> for entity metadata — role, faction, relationships. Using it for contradiction tracking mixes concerns. When Dudley suggests a Sprint 18 refactor, they're acknowledging this is wrong — but the refactor creates breaking changes to any contradiction content authored in Sprint 17. Technical debt with a deadline is not "pragmatic workaround," it's committed future work.

  3. Downstream consumers read typed fields. The monologue system needs contradicted_claim.source to identify Sera by name. With Option B, it needs to parse "dock_7,T1,sera_sid_as_string" and reassemble a ToldBy source. This creates a second parsing site and doubles the failure modes.

The counterargument: "zero schema change" is attractive. My response: adding one optional field to EntityKnowledge is not a meaningful schema change. The struct has 8 fields already. Adding one optional field with Default: None is backward-compatible and requires no migration of existing state.

Resolution: Tension B resolves to Tyre's approach. One optional struct field. Zero breaking changes. Typed reads throughout.


Secondary Disagreements

Global disclosure rate limit — concession with modification

Round 1 had a 3-vs-1 split: Tyre/Dudley/Paula for global limit, Gestalt against.

Let me be precise about what I objected to and where I was wrong.

What I said: "Global limiting creates invisible competition between NPCs that the player can't see or understand."

Where I was wrong: In the specific scenario where a global limit fires — multiple NPCs simultaneously ready to disclose in the same player proximity — the player is looking at ONE NPC who is speaking. They are not watching the other NPCs not-speak. The "invisible competition" framing assumed the player was tracking all NPCs in parallel, which is not how occlusion-based single-character perspective works.

Where I was still right: Dudley proposed "1 disclosure per tick globally." At 10 ticks/game-minute, this is essentially no limit in normal play (per-NPC cooldowns of 300 ticks prevent individual NPCs from firing every tick). The global limit only matters when several NPCs have their cooldowns expire simultaneously AND are all within player range AND pass all their trigger conditions. That's a degenerate case, and limiting it to 1-per-tick is correct.

My concession: I accept the global rate limit as a degenerate-case safeguard, with one condition: the NPC that "wins" the slot must be selected by deterministic StableId ordering (D-010 principle 4), not random. A per-tick random selection would make the global limit a source of non-determinism. Lowest StableId in the ready-to-disclose set wins the tick.

Full rate limiting model (synthesis):

  1. Per-fact, per-NPC: once disclosed, fact is in last_disclosed_facts set; never repeated to same player until intentionally cleared
  2. Per-NPC cooldown: 300 ticks minimum between any disclosures from one NPC
  3. Global: 1 disclosure per tick maximum; when multiple candidates, select by lowest StableId (deterministic)

Paula's disclosure_threshold_override on KG entries

This is not a tension — it's a new design element that doesn't conflict with anything. I support it.

A per-entry flag (never_disclose: bool on FactKnowledge or per-entry override in DisclosureCandidates filtering) that prevents specific facts from entering the candidate pool regardless of trust tier. This handles the "Major secret that would never be shared even with closest confidant" scenario (Kael's ring membership, Voss's blackmail knowledge).

Implementation: Add an optional field disclosure_blocked: bool to FactKnowledge. Content authors set this on the authored initial KG for NPCs who hold facts they would NEVER share. This is a content-level attribute, not a systemic change.

Paula's location privacy gate

I proposed witness inhibition; Paula proposed location privacy. These are additive gates, not competing ones. Both should be in the trigger conditions for process_unprompted_disclosure.

The combined trigger condition set (final proposal, see Disclosure Algorithm section below):

Gate Condition
Trust NPC-player relationship >= Surface tier
Mood DerivedTellState.category not Angry; mood not Hostile
Contentment Contentment.level > -10
Candidates DisclosureCandidates.candidates.is_not_empty()
NPC cooldown DisclosureCooldown not active for this NPC
Global limit At most 1 disclosure this tick (StableId ordering)
Location privacy Candidate's location_privacy tag compatible with current location type
Witness inhibition No non-trusted NPCs in radius (≤3 tiles), OR NPC-player trust >= Real

The witness inhibition gate: I defined it as "no NPCs nearby OR trust high enough to override." Paula's location privacy gate: "current location must match candidate's privacy requirement." These are independent checks that both apply.

Implementation note for Dudley: The location privacy check applies at LINE SELECTION (Layer 4), not at candidate derivation. The candidate pool surfaces eligible facts. When Layer 4 selects a line to deliver a candidate fact, the line may have a location_privacy tag that gates against current location. This keeps candidate derivation clean (KG query only) and puts environmental context check at the correct pipeline layer.

Contradiction monologue authoring — Paula's three options

Paula's Option 3 (generic fallback + authored override) is correct and I endorse it with one addition.

For v0.1, the only FRIEND-pattern NPCs are Kael and Sera. Their contradiction monologue lines should be hand-authored with explicit entity names: "Sera said Kael was at the dock. I'm looking at him in B-7." This is possible because the monologue system has access to contradicted_claim.source (a ToldBy { source_id: sera_sid }) and can resolve sera_sid → NpcName via EntityRegistry.

The resolution lookup must be confirmed by Dudley: Is EntityRegistry + NpcName available in the monologue system context? This is the technical prerequisite for named contradiction monologue lines. If yes: hand-author the FRIEND arc lines now, design the template system for Sprint 18+. If no: the template system becomes a Sprint 17 blocker.

For auto-generated NPC contradictions (future scope): generic fallback "Something doesn't add up about [entity_name]'s whereabouts" is sufficient.


The Disclosure Candidate Selection Algorithm

This is the co-production Dudley requested. Paula and I need to specify this before implementation can proceed. Here is my full design; Paula should confirm the narrative assumptions inline or amend in her Round 2 document.

Algorithm: derive_disclosure_candidates

System name: derive_disclosure_candidates Runs: Once per game-minute (every 10 ticks) for Active-tier NPCs within player dialogue range Input components: KnowledgeGraph, DerivedTellState, Relationships, DisclosureCooldown Output component: DisclosureCandidates { candidates: Vec<FactId>, computed_tick: u64 }

The algorithm (pseudocode):

fn derive_disclosure_candidates(
    npc_kg: &KnowledgeGraph,
    npc_traits: &TraitProfile,           // from NPC's authored profile
    player_trust_tier: TrustTier,        // derived from NPC Relationships
    disclosed_facts: &BTreeSet<FactId>,  // from DisclosureCooldown
) -> Vec<FactId>:

    // Step 1: Determine minimum confidence threshold based on trait
    min_confidence = match npc_traits.primary_trait:
        Cautious      => KnowsDetails   // only shares things they're certain of
        Gossipy       => Suspects        // shares everything including rumors
        _             => KnowsOf         // default: established knowledge only

    // Step 2: Scan KG facts
    candidates = []
    for (fact_id, fact) in npc_kg.known_facts_iter():

        // Exclude: non-Active state (Stale = unreliable, Contradicted = NPC is confused)
        if fact.state != Active: continue

        // Exclude: below trait-modulated confidence threshold
        if fact.confidence < min_confidence: continue

        // Exclude: already disclosed to this player
        if fact_id in disclosed_facts: continue

        // Exclude: entry has disclosure_blocked = true (Major secret / never-share)
        if fact.disclosure_blocked: continue

        // Exclude: trust gate — some fact categories require higher trust
        // IMPORTANT: This is a ROUGH gate only. Layer 4 trust filtering (D-028 Layers 1-3)
        // does the precise trust check. This prevents obviously sensitive facts from
        // reaching Layer 4 at all for low-trust players.
        if player_trust_tier < Surface: continue
        if fact_id.category == "secret.*" && player_trust_tier < Real: continue

        // Trait: Loyal — don't disclose facts about entities the NPC trusts/protects
        // (entity-linked facts use "entity.{sid}.*" namespace convention)
        if npc_traits.has(Loyal):
            if fact is linked to a protected entity: continue

        candidates.push(FactId, fact.confidence, fact.last_updated_tick)

    // Step 3: Order candidates
    // Primary: higher confidence first (NPC leads with what they're most sure of)
    // Secondary: more recently updated first (current-game-state relevance)
    candidates.sort_by(|a, b|
        b.confidence.cmp(a.confidence)
            .then(b.last_updated_tick.cmp(a.last_updated_tick))
    )

    // Step 4: Cap the candidate list
    // Layer 4 line selection narrows further based on location, witnesses, etc.
    // We pre-filter to avoid unnecessary iteration downstream.
    candidates.truncate(10)

    return candidates.map(|(id, _, _)| id)

Notes on the algorithm

Why no entity candidates in v0.1? Entity-specific disclosure ("Kael does X") flows through the LINE CONTENT and the knowledge_grant on the line, not through the candidate derivation. The candidate is the FactId that TRIGGERS line selection (e.g., FactId("kael.cargo_intake_role") is in Sera's KG, triggers selection of her "Kael runs a tight intake process" line, which then grants both the FactKnowledge and the EntityKnowledge via compound grant). Entity candidate derivation as a separate category is Sprint 18.

The "Loyal" trait implementation note for Dudley: Requires a convention: FactIds about specific entities use the namespace "entity.{stable_id_as_hex}.*". The Loyal check then queries the NPC's Relationships component to see if the linked entity has trust_level > threshold. This is a convention, not a type change.

Why Contradicted facts are excluded: An NPC who knows something is contradicted (they received conflicting information themselves) shouldn't volunteer the contradicted entry as if it's fact. However: this creates an interesting future case where an NPC might disclose their OWN confusion ("I heard Kael was at the dock but I could have sworn I saw him elsewhere"). This is emergent from system collision and is exactly the kind of thing to design into Sprint 18+. For v0.1: exclude Contradicted entries from candidates.

The fact_id.category trust gate: The check if fact_id.category == "secret.*" && trust < Real is a rough filter using the existing FactId namespace convention ("category.topic"). Facts in the secret.* namespace require Real trust to even appear as candidates. Facts in poi.*, event.*, cargo.*, etc. require only Surface. This is content-convention enforcement at the algorithm level, not a schema change.

What Paula needs to confirm

  1. Is "Contradicted facts excluded from candidates" correct? Or should there be a separate DisclosureMode::Confusion that surfaces contradicted facts as uncertain disclosure ("I'm not sure about this, but...")?

  2. For THE FRIEND arc specifically: Sera's Phase 2 disclosures about Kael — are these FactId("kael.*") entries in her initial KG, or are they EntityKnowledge attributes? My algorithm treats them as facts (the trigger mechanism). The compound grant on the line creates the EntityKnowledge as a side effect. Does this match your narrative design for Phase 2?

  3. Witness inhibition threshold: I said "no non-trusted NPCs within 3 tiles OR trust >= Real overrides." Does "trusted" mean trust_level >= Real toward the NPC, or toward the player? (It should be: no NPCs with low trust toward the disclosing NPC, not toward the player — Sera won't confide to the detective in front of a coworker she doesn't trust, regardless of how much she trusts the detective.)


D-Record Draft Sections (Mechanical Interactions and Fun Factor)

The following are my contributions to the workshop's D-record. The full D-record synthesis will be Qatux's job; I'm writing my sections for inclusion.

Section: Mechanical Interactions

The knowledge engine: how the five topics form a closed loop

Topics 1-5 are not independent features — they form a directed graph of mechanical interactions. Understanding this graph is essential for implementation prioritization and for assessing whether the design is "interesting" (vs merely functional).

[NPC Gossip (Topic 2)] ──ToldBy sources──► [EntityKnowledge populated with ToldBy]
         │                                              │
         ▼                                              ▼
[Player Dialogue (Topic 1)] ──grants──► [Player KG: ToldBy + DirectObservation]
                                                        │
                                           Position/attribute mismatch?
                                                        │
                                                        ▼
                                        [Contradiction Detection (Topic 5)]
                                                        │
                               ┌───────────────────────┼────────────────────┐
                               ▼                        ▼                    ▼
                        [Monologue fires]    [Relationship → POI]    [D-033 amber]

[NPC KG-aware Behavior (Topic 4)]  ─────────────────────────────────────────►
  tell_state reads relationships, not axes; disclosure uses own KG

[Unprompted Disclosure (Topic 3)] ◄── DisclosureCandidates ── NPC's own KG
         │
         └── Knowledge grants to player (via line knowledge_grant)
               └── feeds back to player KG → possible future contradictions

The critical path through this graph for THE FRIEND arc:

  • Topic 1 (entity grant) → ToldBy EntityKnowledge exists → Topic 5 can fire → monologue lands

The enrichment path (makes the world feel inhabited):

  • Topic 2 (gossip) → NPC KGs populate → Topic 3 (disclosure) → player learns via NPCs → Topic 1 (grant mechanism) converts disclosure to KG state

Every system interaction should produce asymmetric information. If a mechanical interaction produces only symmetric effects (player and world learn the same thing at the same time), it's not serving the core design. Rate-check for each topic:

System Asymmetry produced
Topic 1 grant Player's KG gets ToldBy source; NPC doesn't know what player now knows
Topic 2 gossip NPC A and B share knowledge; player may or may not observe the conversation
Topic 3 disclosure NPC volunteers info from own KG; doesn't know what player already has
Topic 4 boundaries NPC's behavior reflects what it knows, not ground truth; player sees the difference
Topic 5 detection Player knows there's a contradiction; NPC doesn't know player noticed

Every topic in this workshop produces or amplifies asymmetric information. This is the confirmation that the design is correct at its foundations.

Section: Fun Factor — "Is This System Interesting?"

The core question for each mechanic: does it require the player to make a decision about information?

Topic 1 (Grant mechanism) — Fun factor: HIGH

The grant mechanism creates the moment where "talking to people produces game state changes, not just text." The player realizes: "I should have talked to Sera before going to the dock — she knew where Kael was." Or the reverse: "I went to the dock first, now Sera's testimony is suspicious." The order you gather information changes what contradictions you can detect. This produces player choice about investigation sequence.

Topic 2 (NPC gossip) — Fun factor: HIGH (with variance as the key)

The random 1-3 fact selection per conversation means gossip is probabilistic. The player can KNOW that two NPCs regularly interact (observable from conversation system, D-078 eavesdropping) and try to exploit that channel — but what they overhear is never complete or reliable. This creates the "I need to check this against a direct source" loop. The trust-cap (min(source_confidence, KnowsOf)) means gossip gives leads, never certainties. Correct design.

Topic 3 (Unprompted disclosure) — Fun factor: VERY HIGH — the emotional engine

This is the system that makes NPCs feel like they have an independent relationship with information. The player notices: "Sera keeps telling me about cargo processes. She seems to trust me." This builds the expectation of Sera as a friendly source. When the contradiction lands, the betrayal has weight because the prior gift-giving happened. Without disclosure, Sera is just a dialogue tree. WITH disclosure, she's a character who chose to share things with you.

The decision the player makes with disclosure information is whether to trust it. "Sera just told me Kael's been great — but I found those discrepancies. Is she lying? Does she not know? Is she covering for him?" This is the investigative headspace the game is built around.

Topic 4 (NPC boundaries) — Fun factor: MEDIUM (enablement layer)

NPC information boundaries don't create direct player decisions — they prevent the system from cheating. An NPC who tells you about cargo discrepancies because the DATA says there are discrepancies (not because their KG says they know about discrepancies) is an NPC player can intuit as hollow. Boundaries make NPCs feel real; real NPCs make player decisions feel meaningful. Topic 4 is infrastructure for fun, not fun itself.

Exception: the tell_state KG integration (NPC shows more stressed tells when they know they're being watched) creates a detection mechanic. Player observes elevated tell. Player infers: "This NPC knows I'm investigating." Player decision: confront now or gather more evidence first. That's a direct fun output from the boundary change.

Topic 5 (Contradiction detection) — Fun factor: CRITICAL MOMENT

Contradiction detection produces the defining moment of the game's design: the moment asymmetric information systems collide. The player has been building a mental model. The system detects the model is wrong. The monologue fires. The world shifts.

The key design success criterion: this should feel like a DISCOVERY, not a notification. The player shouldn't feel like the system told them "contradiction detected." They should feel like THEY noticed something wrong. The monologue text makes this work: "Sera said Kael was at the dock. I just saw him in B-7." This is the player-character's thought, not a system alert.

This is why Paula's requirement (named-source contradiction monologue) is non-negotiable for fun. Generic "that doesn't add up" is a notification. "Sera said..." is a thought.


Implementation Scope Recommendations for Sprint 17

Based on the full Round 2 analysis, here is my recommendation for what Sprint 17 should and should not attempt:

Sprint 17 MUST include:

  1. KnowledgeEventType::KnowledgeGranted (FactGrant + EntityPositionGrant payloads)
  2. ContradictionClaim struct field on EntityKnowledge
  3. Location contradiction detection in observe_entity (Tyre's Approach B)
  4. ContradictionDetected event + downstream chain (already built, just needs the trigger)
  5. Wire knowledge_grant field in process_talk_interaction
  6. NPC-to-NPC transfer via transfer_npc_knowledge system
  7. DisclosureCandidates component + derive_disclosure_candidates system
  8. process_unprompted_disclosure stub (trigger gates + candidate selection; content-authored lines can be sparse for Sprint 17)
  9. tell_state.rs add Option<&KnowledgeGraph> to query (MVP boundary)
  10. Formal Q-024, Q-025, Q-026 closure

Sprint 17 should NOT include:

  • Full EntityGrant { attributes: BTreeMap } variant (Sprint 18)
  • Conversation partner KG-awareness check (Sprint 18)
  • Attribute contradiction via YAML-authored pairs (Sprint 18 — requires typed attribute keys)
  • Template monologue system for named contradictions (Sprint 18; Sprint 17 hand-authors the FRIEND NPCs)
  • "Ask around" emergent information-seeking behavior (v0.2)
  • Pathfinding KG integration (v0.2 or never)

Formal Q-Closure Positions

Question My position Rationale
Q-024 (Gossip timing) CLOSE — resolves to conversation system hook Workshop consensus; transfer_npc_knowledge system is the implementation
Q-025 (KG memory pressure) CLOSE — not needed at current scale Tyre's 6MB total confirmed; re-evaluate at 500+ Active NPCs
Q-026 (Contradiction detection) CLOSE — resolves to event-driven write-time detection with ContradictionClaim Workshop consensus on architecture; Entity grant scope (Tension A) resolves to Sprint 17 narrow scope

Dissent Register

Items where I'm NOT conceding despite consensus pressure:

1. Dudley's Option B workaround for EntityKnowledge: I am not withdrawing my objection. String parsing in contradiction detection is wrong. Tyre's struct approach is correct. The only reason to choose Option B is "Sprint 17 time pressure" — and I dispute that adding one optional struct field is meaningfully more time-consuming than implementing a string encoding scheme plus parser. The workaround costs more total time when Sprint 18 refactor is included.

2. The EntityPositionGrant must be Sprint 17: Paula is correct that without this, THE FRIEND arc cannot fire its canonical contradiction sequence. Dudley's structured FactId workaround does not faithfully substitute. This is the FRIEND arc's critical path and it must ship in Sprint 17. If the team decides to defer, the decision should be recorded as "THE FRIEND arc contradiction sequence cannot fire in v0.1" — not "we found a workaround."