neon-sprawl/docs/plans/NEO-113-implementation-plan.md

145 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEO-113 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-113 |
| **Title** | E7M1-02: Server quest catalog load (fail-fast) |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-113/e7m1-02-server-quest-catalog-load-fail-fast |
| **Module** | [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-02** |
| **Branch** | `NEO-113-e7m1-server-quest-catalog-load-fail-fast` |
| **Precursor** | [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) — quest schemas, `prototype_quests.json`, CI gates (**landed on `main`**) |
| **Pattern** | [NEO-101](NEO-101-implementation-plan.md) — row JSON Schema + catalog envelope + in-process C# gates mirroring `scripts/validate_content.py` |
| **Blocks** | [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) — injectable quest definition registry + DI |
| **Client counterpart** | None — server-only catalog load; player-visible work starts at [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) / [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) |
## Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Test scope** | Loader unit tests only, or also `WebApplicationFactory` host boot failure / DI resolve tests? | **Both** — mirror [NEO-101](NEO-101-implementation-plan.md); Linear AC “host fails on invalid quest JSON” is stronger with an integration boot test. | **User:** loader + host boot tests. |
| **NEO-113 vs NEO-114 split** | Ship `IQuestDefinitionRegistry` on this story? | **Strict split** — E7M1-02: loader + catalog singleton + eager fail-fast boot; [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) adds injectable registry (E7M1-03). Same pattern as NEO-101/102. | **Adopted** — backlog out-of-scope; no separate question needed. |
| **Runtime validation** | In-process C# vs subprocess `validate_content.py`? | **In-process C#** mirroring `scripts/validate_content.py` E7M1 gates (NEO-51/101 precedent; no Python on server images). | **Adopted** |
| **Cross-ref at load** | Validate objective `itemId` / `recipeId` / `encounterId` against loaded catalogs? | **Yes** — quest loader takes `knownItemIds`, `knownRecipeIds`, `knownEncounterIds` from resolved singletons; load **after** item, recipe, and encounter catalogs (same order as CI `main()`). | **Adopted** |
## Goal, scope, and out-of-scope
**Goal:** Disk → host: startup load of `content/quests/*_quests.json` with CI-parity validation; process refuses to listen when catalogs are missing, malformed, or fail prototype E7M1 gates.
**In scope (from Linear + [E7M1-02](E7M1-prototype-backlog.md#e7m1-02--server-quest-catalog-load-fail-fast)):**
- Loader + catalog types under `server/NeonSprawl.Server/Game/Quests/`.
- Fail-fast on malformed files, duplicate ids (quest / step / objective), allowlist mismatch with CI, broken cross-refs (items, recipes, encounters), acyclic prerequisite graph, chain terminal token gate.
- Unit tests (AAA) for loader happy path and failure modes; host boot tests (AAA).
- `server/README.md` section for quest catalog.
**Out of scope (from Linear + backlog):**
- Injectable registry interface (**NEO-114** / E7M1-03).
- HTTP projection, player quest state, runtime state machine (E7M1-04+).
- Godot client.
## Acceptance criteria checklist
- [x] Host fails startup on invalid quest JSON (mirror CI rules).
- [x] Loader tests cover allowlist + cross-ref failures (and host boot per kickoff).
## Implementation reconciliation (shipped)
- **Loaders:** `QuestDefinitionCatalogLoader` + `PrototypeE7M1QuestCatalogRules` under `Game/Quests/` with CI-parity gates.
- **Catalog:** `QuestDefinitionCatalog` singleton; eager resolve in `Program.cs` after encounter catalogs.
- **DI:** `AddQuestDefinitionCatalog` — loads after item, recipe, and encounter catalogs for cross-refs.
- **Config:** `ContentPathsOptions` quest path keys; `InMemoryWebApplicationFactory` pins repo `content/quests`.
- **Tests:** 18 AAA tests in `QuestDefinitionCatalogLoaderTests` (loader + host boot).
- **Docs:** `server/README.md` quest catalog section; alignment register E7.M1 row updated.
## Technical approach
1. **`Game/Quests/` load-time DTOs** — nested rows mirroring catalog JSON:
- `QuestObjectiveDefRow``Id`, `Kind`, optional `ItemId`, `Quantity`, `RecipeId`, `EncounterId` (populated per kind).
- `QuestStepDefRow``Id`, `DisplayName`, `Objectives`.
- `QuestDefRow``Id`, `DisplayName`, `PrerequisiteQuestIds`, `Steps`.
2. **`QuestDefinitionCatalog`** — immutable snapshot: directory path, `ById`, `DistinctQuestCount`, `CatalogJsonFileCount`; `TryGetQuest(id, out QuestDefRow?)`.
3. **`QuestDefinitionCatalogLoader.Load`** — parameters: `questsDirectory`, `questDefSchemaPath`, `questStepDefSchemaPath`, `questObjectiveDefSchemaPath`, `knownItemIds`, `knownRecipeIds`, `knownEncounterIds`, `logger`:
- Enumerate `*_quests.json` (top-level only), ordered by path.
- Per file: parse JSON; require `schemaVersion == 1` and top-level `quests` array.
- Register `quest-objective-def.schema.json`, `quest-step-def.schema.json`, then `quest-def.schema.json` in `SchemaRegistry.Global` before row validation (recipe I/O row pattern).
- Per row: JsonSchema.Net validation; track quest `id`; reject duplicate quest `id` across files.
- Post-schema: reject duplicate step `id` within a quest; reject duplicate objective `id` within a quest (across all steps — same as CI).
- Run **`PrototypeE7M1QuestCatalogRules`** gates (sync comment → `PROTOTYPE_E7M1_*` in `validate_content.py`):
- Four-id allowlist gate.
- Prerequisite known-id + acyclic DFS gate.
- Objective cross-ref gate (`gather_item` / `inventory_has_item` → item ids; `craft_recipe` → recipe ids; `encounter_complete` → encounter ids).
- Operator chain terminal step: sole objective `inventory_has_item` **`contract_handoff_token`** ×1.
- Throw **`InvalidOperationException`** with aggregated sorted messages prefixed `Quest catalog validation failed:`.
- Information log: absolute quests directory, quest count, JSON file count.
4. **`PrototypeE7M1QuestCatalogRules`** — frozen constants + gate helpers:
- `ExpectedQuestIds` — sync → `PROTOTYPE_E7M1_QUEST_IDS`.
- `ChainQuestId`, `ChainTerminalItemId` — sync → `PROTOTYPE_E7M1_CHAIN_QUEST_ID`, `PROTOTYPE_E7M1_CHAIN_TERMINAL_ITEM_ID`.
- Cross-ref sets reuse `PrototypeSlice1ItemCatalogRules.ExpectedItemIds`, `PrototypeSlice3RecipeCatalogRules.ExpectedRecipeIds`, `PrototypeE5M3EncounterCatalogRules.ExpectedEncounterIds` where applicable (or pass-through from loaded catalogs — loader receives live id sets from DI).
5. **Path resolution**`QuestCatalogPathResolution`: `TryDiscoverQuestsDirectory`, `ResolveQuestsDirectory`, `ResolveQuestDefSchemaPath`, `ResolveQuestStepDefSchemaPath`, `ResolveQuestObjectiveDefSchemaPath` (ancestor walk from `AppContext.BaseDirectory` for `content/quests`; schema defaults under sibling `content/schemas/`).
6. **`ContentPathsOptions`** — add `QuestsDirectory`, `QuestDefSchemaPath`, `QuestStepDefSchemaPath`, `QuestObjectiveDefSchemaPath`.
7. **`QuestCatalogServiceCollectionExtensions.AddQuestDefinitionCatalog`**:
- Factory for `QuestDefinitionCatalog` depends on `ItemDefinitionCatalog`, `RecipeDefinitionCatalog`, `EncounterDefinitionCatalog` (known id sets from `.ById.Keys`).
- **No** `IQuestDefinitionRegistry` yet (NEO-114).
8. **`Program.cs`** — register extension after encounter catalogs; eager-resolve `QuestDefinitionCatalog` before `Run()`.
9. **`InMemoryWebApplicationFactory`** — pin `Content:QuestsDirectory` to repo path so existing tests keep booting.
10. **`appsettings.json`** — optional empty `Content:QuestsDirectory` + schema override keys for documentation.
11. **Tests** — temp-dir fixtures copying repo schemas; valid JSON from [prototype_quests.json](../../content/quests/prototype_quests.json); negative cases for schema violation, duplicate quest/step/objective id, wrong allowlist id, unknown prerequisite, cyclic prerequisites, unknown item/recipe/encounter id, wrong chain terminal objective, `schemaVersion` ≠ 1, missing schema file; host resolve via DI + `/health`; host fail with empty quests dir.
12. **Docs**`server/README.md` quest catalog section (config keys, discovery, fail-fast, load order); `documentation_and_implementation_alignment.md` E7.M1 row note server load landed when complete.
## Files to add
| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Quests/QuestObjectiveDefRow.cs` | Load-time DTO for a single quest objective row. |
| `server/NeonSprawl.Server/Game/Quests/QuestStepDefRow.cs` | Load-time DTO for a quest step (objectives array). |
| `server/NeonSprawl.Server/Game/Quests/QuestDefRow.cs` | Load-time DTO for a single `QuestDef` row. |
| `server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalog.cs` | In-memory quest catalog + `TryGetQuest`. |
| `server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs` | Disk I/O, schema validation, duplicate id gates, E7M1 rules, logging. |
| `server/NeonSprawl.Server/Game/Quests/QuestCatalogPathResolution.cs` | Directory/schema discovery for quests. |
| `server/NeonSprawl.Server/Game/Quests/PrototypeE7M1QuestCatalogRules.cs` | Frozen quest ids + prerequisite / cross-ref / chain terminal gates (sync → Python). |
| `server/NeonSprawl.Server/Game/Quests/QuestCatalogServiceCollectionExtensions.cs` | `AddQuestDefinitionCatalog` DI registration. |
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestCatalogTestPaths.cs` | Repo `content/quests` + schema discovery for tests. |
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoaderTests.cs` | AAA loader + host boot tests for quest catalog. |
| `docs/plans/NEO-113-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs` | Add quest directory and schema path options under `Content` section. |
| `server/NeonSprawl.Server/Program.cs` | Register quest catalog; eager-resolve after build. |
| `server/NeonSprawl.Server/appsettings.json` | Optional empty `Content:QuestsDirectory` / schema override keys for documentation. |
| `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | Pin quest content path so test host boots with repo catalog. |
| `server/README.md` | New quest catalog section (config, discovery, fail-fast, load order). |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M1 row — note NEO-113 server load when complete. |
## Tests
| Test file | What it covers |
|-----------|----------------|
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoaderTests.cs` | **Unit:** valid prototype quest catalog loads (4 quests); `quests` not array; schema violation; duplicate quest `id`; duplicate step `id` within quest; duplicate objective `id` within quest; wrong/missing E7M1 quest id; unknown prerequisite id; cyclic `prerequisiteQuestIds`; unknown `itemId` / `recipeId` / `encounterId` in objective; wrong chain terminal objective (kind / item / quantity); `schemaVersion` ≠ 1; missing schema file. **Host:** `InMemoryWebApplicationFactory` + `/health` + DI resolve `QuestDefinitionCatalog` (quest `prototype_quest_gather_intro`); `WebApplicationFactory` with empty quests dir fails startup with actionable message. |
## Open questions / risks
| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Constants drift vs Python** | Comment on `PrototypeE7M1QuestCatalogRules` pointing at `scripts/validate_content.py` `PROTOTYPE_E7M1_*` constants and gate functions. | **adopted** |
| **Load order in DI** | Register quest catalog factory after item, recipe, and encounter singletons; pass `.ById.Keys` as known id sets. | **adopted** |
| **Test cwd / CI** | `InMemoryWebApplicationFactory` must set `Content:QuestsDirectory` explicitly. | **adopted** |
| **Three-schema $ref registration** | Register objective → step → def schemas in `SchemaRegistry.Global` before evaluating quest rows (same as CI `_quest_def_validator`). | **adopted** |
| **Objective id uniqueness scope** | Duplicate objective ids rejected **within quest** (all steps), matching NEO-112 CI gate — not per-step. | **adopted** |