feat(content): D-084 dual-namespace line ID scheme — resolve Q-028 (#544)

Role namespace (shared pool) stays unchanged; instance namespace
(role-slug + seeded counter) added as opt-in for per-instance
authored content on auto-generated NPCs. No schema migration needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 02:27:33 +01:00
co-authored by Claude Opus 4.6
parent 2eca8e961b
commit 9cec3df68c
4 changed files with 128 additions and 6 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ Cross-domain decisions live in one file with cross-reference notes in related fi
|------|--------|-----------|
| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073 |
| [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072, D-076, D-077, D-078 |
| [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074 |
| [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074, D-075, D-084 |
| [scope.md](scope.md) | Game concept, prototype | D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065 |
| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040 |
| [questions.md](questions.md) | Open questions | Q-001 through Q-026 |
+23 -1
View File
@@ -182,6 +182,28 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Raised by:** Tyre (technical analysis, architecture synthesis)
- **Dissent:** Gestalt endorses D-075 (reviewed 2026-02-19). The emergent archetype distinction is sufficient: access tier tags already encode "authority figure lines" vs "insider lines" in content; starting knowledge differentials produce different dialogue gate timings per character; adding an archetype filter would create per-character content maintenance burden and dilute the "two keyholes on the same world" experience (D-027). Knowledge vocabulary doc (#368) confirms this works in practice — same fact IDs, different starting confidence levels, different gate-open timing per character. *Nigel's input still pending.*
### D-084: Dual-namespace line ID scheme — role pool + instance override
- **Date:** 2026-02-25
- **Decision:** Line IDs for auto-generated NPC content use a dual-namespace approach that eliminates the Q-028 collision problem without altering the existing ID format.
- **Role namespace (unchanged, primary):** `{role-slug}_{d|m|e}_{###}` — e.g., `dock-worker_d_001`. These are shared lines delivered by any instance of the role. One file, one ID sequence per role-at-location. No per-instance authoring, no collision possible. All existing authored content is unaffected.
- **Instance namespace (new, opt-in):** `{role-slug}-{zero-padded counter}_{d|m|e}_{###}` — e.g., `dock-worker-07_d_001`. Counter is the generation-order rank within the role group for that district, starting at 01, assigned deterministically from the world seed. Used only when a specific generated NPC needs authored content that differs from the role pool.
- **Key design choices:**
- **Why the collision problem is mostly already solved:** The Q-028 collision framing assumed NPC-scoped IDs require per-instance ID sequences. They don't. D-028 tagged line pools are role-scoped: `dock-worker_d_001` is content that any dock worker can deliver. Forty dock workers all drawing from `dock-worker_d_###` is correct behavior, not a collision. A collision would only occur if two *distinct authored lines* shared the same ID — which the role namespace prevents by definition (one file, one sequence).
- **Instance namespace scope:** Opt-in only. Tier 3 (flat wallpaper) and Tier 2 (mundane triangles) auto-generated NPCs use the role pool exclusively. Instance pools are authored only when a specific generated NPC needs content variation the role pool cannot supply (e.g., a generated NPC flagged as a triangle member with unique tell lines).
- **History log disambiguation:** The speaker of a line is identified by `StableId`, not by line ID. Line ID identifies content; `StableId` identifies the speaker. `(StableId: 42, line_id: "dock-worker_d_001")` and `(StableId: 43, line_id: "dock-worker_d_001")` are two different log entries for the same content line — no collision in the log.
- **Counter stability:** Generation order within a role group is seeded from the world seed. Same seed → same order → same counter assignments. Counter is recorded in the district's NPC roster at world-gen time. The counter survives save/load because it is part of the generated NPC's profile, not recomputed at runtime.
- **Schema compatibility:** The existing ID regex `^[a-z][a-z0-9-]*_[dme]_\d{3}$` already accepts `dock-worker-07_d_001`. No regex change required. No content migration required.
- **Rejected alternatives:**
- **StableId prefix (`npc-00042_d_001`):** StableId is assigned at load time from sorted canonical IDs. Authors cannot know it before writing files. A generate-then-bake pipeline would break the content/generation separation principle. Rejected.
- **UUID suffix (`dock-worker-a3f2_d_001`):** UUIDs are stable per seed but change across seeds, orphaning any authored instance content on replay. Rejected.
- **Slug registry with collision resolution (`dock-worker`, `dock-worker-2`, ...):** First instance gets a privileged non-suffixed slug while all others get a counter, creating asymmetry with no upside. Rejected.
- **Implementation requirement:** The content registry (`server/src/knowledge/registry.rs` or a new `server/src/content/npc_slug.rs`) tracks a `RoleCounter: BTreeMap<String, u32>` per district. Incremented when a generated NPC claims an instance namespace slot. Stored in the district manifest. Provides `generate_instance_slug(role_slug) -> String` returning `{role-slug}-{counter:02}`.
- **Hand-authored NPCs:** Unchanged. `kael-davan`, `sera-venn`, and all named authored NPCs keep their current slugs and ID sequences. No migration.
- **Resolves:** Q-028
- **Cross-reference:** Line ID scheme ([D-035](#d-035-converged-tag-taxonomy-for-dialogue-and-monologue-line-pools)), population model ([D-029](#d-029-population-entanglement-ratio--305020)), NPC generation ([D-024](#d-024-npc-generation-model--10-axes--combat-component))
- **Raised by:** Gestalt (Sprint 18, #544). Endorsed by Tyre pending implementation review.
- **Dissent:** None.
---
*16 decisions. Last updated: 2026-02-19 (D-075 dissent updated)*
*17 decisions. Last updated: 2026-02-25 (D-084 added — Q-028 resolution)*
+4 -4
View File
@@ -161,13 +161,13 @@ Tracked questions awaiting discussion or resolution.
- **Source:** Sprint 10 PR review discussion (2026-02-19)
### Q-028: Collision-resistant line IDs for auto-generated NPCs
- **Status:** Open
- **Question:** The D-035 NPC-scoped line ID scheme uses NPC slugs as prefix (`kael-davan_d_001`). Hand-authored NPCs have unique slugs, but auto-generated populations (D-029: hundreds of NPCs) will produce collisions when the generator creates multiple NPCs with the same role slug (e.g., two `dock-worker` NPCs). What collision-resistance mechanism should be used? Options: (1) Short UUID/hash suffix on auto-gen slugs (`dock-worker-a7f3_d_001`), (2) Entity UUID as prefix, (3) Slug registry that guarantees uniqueness at generation time, (4) Composite key (entity ID + sequence) in server, human-readable slug only for authored content.
- **Constraints:** Line IDs must be globally unique across entire save file lifetime (history log readiness). Must stay human-readable for hand-authored content. Server treats IDs as opaque strings — solution lives in content/generation layer. Must be compatible with D-035 NPC-scoped namespace.
- **Status:** Resolved → [D-084](content.md#d-084-dual-namespace-line-id-scheme--role-pool--instance-override)
- **Resolution:** The collision problem is mostly already solved by the role-pool architecture: `dock-worker_d_###` lines are shared content for all instances of the role, not per-instance IDs. A true collision (two distinct authored lines sharing the same ID) cannot occur with one file per role. For the edge case of authored instance-specific content, a role-slug + zero-padded generation counter suffix produces `dock-worker-07_d_001`. Counter is seeded-deterministic. No schema change, no migration. Hand-authored NPCs unchanged.
- **Closed by:** Gestalt (Sprint 18, #544). 2026-02-25.
- **Ticket:** #544
- **Assigned to:** Gestalt, Tyre
- **Source:** Sprint 16 PR #59 review discussion (2026-02-23)
---
*28 questions (6 resolved, 1 partially resolved, 21 open). Last updated: 2026-02-24*
*28 questions (7 resolved, 1 partially resolved, 20 open). Last updated: 2026-02-25 (Q-028 resolved → D-084)*
+100
View File
@@ -0,0 +1,100 @@
# Line ID Authoring Guide
**Decision:** D-084 (dual-namespace line ID scheme)
**Resolves:** Q-028 (collision-resistant IDs for auto-generated NPCs)
**Ticket:** #544
---
## The Short Version
- **Role pool lines:** Use `{role-slug}_d_{###}` — e.g., `dock-worker_d_001`. These lines are shared by all NPCs with that role. This is the default for all auto-generated NPC content.
- **Named NPC lines:** Use `{npc-slug}_d_{###}` — e.g., `kael-davan_d_001`. Unchanged from current practice.
- **Instance-specific lines (rare):** Use `{role-slug}-{counter}_d_{###}` — e.g., `dock-worker-07_d_001`. Only needed when a specific generated NPC needs content different from the role pool.
---
## How Line IDs Work
A line ID identifies **content**, not speaker. The speaker is identified by their `StableId` in the history log. So `dock-worker_d_001` being said by 40 different dock workers is correct: the log records `(StableId: 12, dock-worker_d_001)`, `(StableId: 37, dock-worker_d_001)`, etc. No collision.
This means the role pool approach already handles most cases — the "collision problem" is mainly a concern for the rare case where you want a specific generated NPC to say something *different* from others of the same role.
---
## Namespace Reference
### Named NPC lines (Tier 1 and Tier 2 authored NPCs)
```
Format: {npc-slug}_{content-type}_{###}
Example: kael-davan_d_001 (Kael's dialogue line 1)
sera-venn_d_015 (Sera's dialogue line 15)
pc-smuggler_m_s_001 (Smuggler monologue line 1)
```
File location: One file per NPC (e.g., `dialogue/maintenance-corridors/kael-davan.yaml`)
Numbering: Sequential within the file. Gaps are acceptable (deleted lines leave permanent gaps). Never reuse a number.
---
### Role pool lines (auto-generated NPCs, Tier 3 flat, Tier 2 mundane)
```
Format: {role-slug}_{content-type}_{###}
Example: dock-worker_d_001 (any dock worker, dialogue line 1)
bar-regular_d_008 (any bar regular, dialogue line 8)
transit-worker_d_003 (any transit worker, dialogue line 3)
```
File location: One file per role-at-location (e.g., `dialogue/the-terminal/dock-worker.yaml`)
These lines are shared by **all instances** of the role. Write them to suit any dock worker, not a specific one.
---
### Instance-specific lines (opt-in, rare)
Use only when the generation system has flagged a specific NPC as needing content that differs from the role pool. Examples: a generated dock worker who is also a triangle member with a specific tell; a generated bar regular who witnessed a specific event.
```
Format: {role-slug}-{zero-padded counter}_{content-type}_{###}
Example: dock-worker-07_d_001 (instance 7 of dock-worker role, line 1)
bar-regular-02_d_005 (instance 2 of bar-regular role, line 5)
```
The counter (01, 02, ... N) is assigned by the generation system in world-seed-deterministic order. The NPC's generated profile file will tell you which counter to use.
File location: Same directory as the role pool file, separate file with instance slug as name (e.g., `dialogue/the-terminal/dock-worker-07.yaml`)
---
## Quick Decision Guide
| Situation | ID format to use |
|-----------|------------------|
| Named authored NPC (Kael, Sera, Voss...) | `{npc-slug}_d_{###}` |
| Lines any dock worker can say | `dock-worker_d_{###}` |
| Lines any bar regular can say | `bar-regular_d_{###}` |
| Generated NPC with specific triangle role | `{role-slug}-{counter}_d_{###}` |
| Generated NPC who's just background | `{role-slug}_d_{###}` — no instance ID needed |
---
## Schema Compatibility
The existing ID regex `^[a-z][a-z0-9-]*_[dme]_\d{3}$` accepts all three formats. No schema change is required. The content validator (`make validate-content`) checks for duplicate IDs across all files in a district.
---
## Numbering Rules
1. Start at `001`, increment by 1 for each new line.
2. Never reuse a number, even if a line is deleted. Gaps are fine.
3. Lines within a single file have a contiguous prefix — `dock-worker_d_001` through `dock-worker_d_042`, etc.
4. Cross-file: `kael-davan.yaml` at the terminal and `kael-davan.yaml` at maintenance corridors both use the `kael-davan_d_###` namespace. Continue numbering from where the other file left off (check the existing files first, use a fresh sequence if the NPC is new to a location).
---
*D-084 — authored by Gestalt, Sprint 18*