12 KiB
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 · Epic 7 Slice 1 · backlog E7M1-02 |
| Branch | NEO-113-e7m1-server-quest-catalog-load-fail-fast |
| Precursor | NEO-112 — quest schemas, prototype_quests.json, CI gates (landed on main) |
| Pattern | NEO-101 — row JSON Schema + catalog envelope + in-process C# gates mirroring scripts/validate_content.py |
| Blocks | NEO-114 — injectable quest definition registry + DI |
| Client counterpart | None — server-only catalog load; player-visible work starts at NEO-122 / 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; 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 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):
- 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.mdsection 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
- Host fails startup on invalid quest JSON (mirror CI rules).
- Loader tests cover allowlist + cross-ref failures (and host boot per kickoff).
Implementation reconciliation (shipped)
- Loaders:
QuestDefinitionCatalogLoader+PrototypeE7M1QuestCatalogRulesunderGame/Quests/with CI-parity gates. - Catalog:
QuestDefinitionCatalogsingleton; eager resolve inProgram.csafter encounter catalogs. - DI:
AddQuestDefinitionCatalog— loads after item, recipe, and encounter catalogs for cross-refs. - Config:
ContentPathsOptionsquest path keys;InMemoryWebApplicationFactorypins repocontent/quests. - Tests: 18 AAA tests in
QuestDefinitionCatalogLoaderTests(loader + host boot). - Docs:
server/README.mdquest catalog section; alignment register E7.M1 row updated.
Technical approach
-
Game/Quests/load-time DTOs — nested rows mirroring catalog JSON:QuestObjectiveDefRow—Id,Kind, optionalItemId,Quantity,RecipeId,EncounterId(populated per kind).QuestStepDefRow—Id,DisplayName,Objectives.QuestDefRow—Id,DisplayName,PrerequisiteQuestIds,Steps.
-
QuestDefinitionCatalog— immutable snapshot: directory path,ById,DistinctQuestCount,CatalogJsonFileCount;TryGetQuest(id, out QuestDefRow?). -
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 == 1and top-levelquestsarray. - Register
quest-objective-def.schema.json,quest-step-def.schema.json, thenquest-def.schema.jsoninSchemaRegistry.Globalbefore row validation (recipe I/O row pattern). - Per row: JsonSchema.Net validation; track quest
id; reject duplicate questidacross files. - Post-schema: reject duplicate step
idwithin a quest; reject duplicate objectiveidwithin a quest (across all steps — same as CI). - Run
PrototypeE7M1QuestCatalogRulesgates (sync comment →PROTOTYPE_E7M1_*invalidate_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_itemcontract_handoff_token×1.
- Throw
InvalidOperationExceptionwith aggregated sorted messages prefixedQuest catalog validation failed:. - Information log: absolute quests directory, quest count, JSON file count.
- Enumerate
-
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.ExpectedEncounterIdswhere applicable (or pass-through from loaded catalogs — loader receives live id sets from DI).
-
Path resolution —
QuestCatalogPathResolution:TryDiscoverQuestsDirectory,ResolveQuestsDirectory,ResolveQuestDefSchemaPath,ResolveQuestStepDefSchemaPath,ResolveQuestObjectiveDefSchemaPath(ancestor walk fromAppContext.BaseDirectoryforcontent/quests; schema defaults under siblingcontent/schemas/). -
ContentPathsOptions— addQuestsDirectory,QuestDefSchemaPath,QuestStepDefSchemaPath,QuestObjectiveDefSchemaPath. -
QuestCatalogServiceCollectionExtensions.AddQuestDefinitionCatalog:- Factory for
QuestDefinitionCatalogdepends onItemDefinitionCatalog,RecipeDefinitionCatalog,EncounterDefinitionCatalog(known id sets from.ById.Keys). - No
IQuestDefinitionRegistryyet (NEO-114).
- Factory for
-
Program.cs— register extension after encounter catalogs; eager-resolveQuestDefinitionCatalogbeforeRun(). -
InMemoryWebApplicationFactory— pinContent:QuestsDirectoryto repo path so existing tests keep booting. -
appsettings.json— optional emptyContent:QuestsDirectory+ schema override keys for documentation. -
Tests — temp-dir fixtures copying repo schemas; valid JSON from 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. -
Docs —
server/README.mdquest catalog section (config keys, discovery, fail-fast, load order);documentation_and_implementation_alignment.mdE7.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 |