From 9cec3df68c54a0e905a34cca17530c070981e60b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 25 Feb 2026 02:27:33 +0100 Subject: [PATCH] =?UTF-8?q?feat(content):=20D-084=20dual-namespace=20line?= =?UTF-8?q?=20ID=20scheme=20=E2=80=94=20resolve=20Q-028=20(#544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- decisions/README.md | 2 +- decisions/content.md | 24 +++++- decisions/questions.md | 8 +- docs/design/line-id-authoring-guide.md | 100 +++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 docs/design/line-id-authoring-guide.md diff --git a/decisions/README.md b/decisions/README.md index 98ca46d9a..eed582653 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -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 | diff --git a/decisions/content.md b/decisions/content.md index 1870407a0..21a0b9e17 100644 --- a/decisions/content.md +++ b/decisions/content.md @@ -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` 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)* diff --git a/decisions/questions.md b/decisions/questions.md index d2dbf7d79..b6f50ea63 100644 --- a/decisions/questions.md +++ b/decisions/questions.md @@ -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)* diff --git a/docs/design/line-id-authoring-guide.md b/docs/design/line-id-authoring-guide.md new file mode 100644 index 000000000..7373df6fd --- /dev/null +++ b/docs/design/line-id-authoring-guide.md @@ -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*