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

12 KiB
Raw Blame History

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.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

  • 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 + 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:

    • QuestObjectiveDefRowId, Kind, optional ItemId, Quantity, RecipeId, EncounterId (populated per kind).
    • QuestStepDefRowId, DisplayName, Objectives.
    • QuestDefRowId, 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 resolutionQuestCatalogPathResolution: 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; 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. Docsserver/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