Standardized YAML frontmatter on all 12 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
248 lines
16 KiB
Markdown
248 lines
16 KiB
Markdown
---
|
|
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<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):**
|
|
|
|
```rust
|
|
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 |
|