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

12 KiB

NEO-77 — Implementation plan

Story reference

Field Value
Key NEO-77
Title E5M1-02: Server ability catalog load (fail-fast)
Linear https://linear.app/neon-sprawl/issue/NEO-77/e5m1-02-server-ability-catalog-load-fail-fast
Module E5.M1 — CombatRulesEngine · Epic 5 Slice 1 · backlog E5M1-02
Branch NEO-77-e5m1-server-ability-catalog-load
Precursor NEO-76 — frozen four-ability catalog + CI gates (Done on main)
Pattern NEO-51 / NEO-66 — fail-fast catalog loader + ContentPathsOptions + eager Program.cs resolve; strict split before registry ticket
Blocks NEO-79IAbilityDefinitionRegistry + DI (E5M1-03)

Kickoff clarifications

Topic Question Agent recommendation Answer
NEO-77 vs NEO-79 (DI scope) Ship IAbilityDefinitionRegistry on this story, or only load + AbilityDefinitionCatalog? Strict split — NEO-77: loader + AbilityDefinitionCatalog + eager fail-fast boot; NEO-79 adds injectable IAbilityDefinitionRegistry (E5M1-03). Rationale: E5M1 backlog separates E5M1-02/03; mirrors NEO-66/NEO-67. User: AbilityDefinitionCatalog singleton only (kickoff).
Startup failure tests Loader unit tests only, or also WebApplicationFactory host boot failure? Both — mirror ItemDefinitionCatalogLoaderTests / RecipeDefinitionCatalogLoaderTests. User: loader AAA + host boot tests (kickoff).
PrototypeAbilityRegistry Migrate hotbar/cast allowlist reads this story? Leave unchanged — E5M1-03 explicitly migrates call sites; NEO-77 is load-only. User: unchanged (kickoff).
Runtime validation In-process C# vs subprocess validate_content.py? In-process C# mirroring scripts/validate_content.py (Draft 2020-12 + E5M1 four-id gate). Rationale: NEO-51 / NEO-66 precedent; no cross-catalog refs in Slice 1. Adopted — NEO-51 precedent; no separate question.
Lookup API on NEO-77 How does “catalog resolves AbilityDef by id” land without NEO-79? AbilityDefinitionCatalog.ById (read-only map) + TryGetAbility(string id, out AbilityDefRow row); row carries damage + cooldown fields for NEO-79 consumers. Adopted — NEO-66 RecipeDefinitionCatalog precedent.

Goal, scope, and out-of-scope

Goal: On host startup, load all content/abilities/*_abilities.json catalogs (validated in CI per NEO-76), build an in-memory AbilityDefinitionCatalog, and refuse to listen when any file is missing, malformed, has duplicate id, or fails the prototype E5M1 four-id gate.

In scope (from Linear + E5M1-02):

  • Loader + catalog types under server/NeonSprawl.Server/Game/Combat/.
  • Configurable abilities directory and schema path (ContentPathsOptions extension).
  • CI-parity validation: schemaVersion, row schema (ability-def), duplicate id, E5M1 four-id allowlist gate.
  • DI registration of AbilityDefinitionCatalog singleton; eager resolve in Program.cs.
  • Unit + host startup tests (AAA).
  • server/README.md section.

Out of scope (from Linear):

  • IAbilityDefinitionRegistry / hotbar-cast migration from PrototypeAbilityRegistry (NEO-79, E5M1-03).
  • HTTP projection (NEO-78, E5M1-04), combat resolution (NEO-81+).

Acceptance criteria checklist

  • Server refuses boot when ability catalog invalid (missing dir, no *_abilities.json, bad JSON, schemaVersion ≠ 1, schema violation, duplicate id, E5M1 gate failure).
  • AbilityDefinitionCatalog resolves prototype rows by ability id (e.g. prototype_pulse, prototype_burst) with baseDamage, cooldownSeconds, optional abilityKind.
  • Unit tests (AAA): loader happy path + failure modes; host resolves catalog on success; host fails startup on invalid catalog directory.
  • Success log includes ability count, catalog directory, and file count (Information).

Technical approach

  1. Game/Combat/ row DTOAbilityDefRow (Id, DisplayName, BaseDamage, CooldownSeconds, optional AbilityKind enum or string).

  2. AbilityDefinitionCatalogLoader.Load(abilitiesDirectory, schemaPath, logger) — single-pass load mirroring validate_content.py _validate_ability_catalogs + _prototype_e5m1_ability_gate:

    • Enumerate *_abilities.json (top-level only), ordered by path.
    • Build JsonSchema.Net evaluator from ability-def.schema.json.
    • Per file: parse JSON; require schemaVersion == 1 and top-level abilities array.
    • Per row: validate against schema; on clean rows track id; build AbilityDefRow; reject duplicate id across files.
    • Run PrototypeE5M1AbilityCatalogRules.TryGetE5M1GateError (mirror PROTOTYPE_E5M1_ABILITY_IDS / _prototype_e5m1_ability_gate in scripts/validate_content.py).
    • Throw InvalidOperationException with aggregated, sorted messages prefixed Ability catalog validation failed:.
  3. AbilityDefinitionCatalog — immutable snapshot: AbilitiesDirectory, ById, DistinctAbilityCount, CatalogJsonFileCount; TryGetAbility(id, out AbilityDefRow?) for lookups.

  4. AbilityCatalogPathResolutionTryDiscoverAbilitiesDirectory, ResolveAbilitiesDirectory, ResolveAbilityDefSchemaPath (parent content/schemas/ layout when unset).

  5. PrototypeE5M1AbilityCatalogRules — frozen four ability ids; sync comment to scripts/validate_content.py PROTOTYPE_E5M1_ABILITY_IDS / _prototype_e5m1_ability_gate. Ids: prototype_pulse, prototype_guard, prototype_dash, prototype_burst (same set as PrototypeAbilityRegistry — unchanged this story).

  6. AbilityCatalogServiceCollectionExtensions.AddAbilityDefinitionCatalog — bind ContentPathsOptions; register AbilityDefinitionCatalog singleton factory that resolves paths and calls loader. No dependency on item/skill catalogs (Slice 1 abilities have no cross-ref fields).

  7. Program.csAddAbilityDefinitionCatalog(configuration) alongside existing catalog registrations; after Build(), GetRequiredService<AbilityDefinitionCatalog>() with other eager catalog resolves.

  8. Tests — temp-dir fixtures copying repo ability-def.schema.json; valid four-ability JSON matching prototype_abilities.json; negative cases (duplicate id, wrong/missing E5M1 roster, bad baseDamage / cooldownSeconds, empty displayName, schemaVersion ≠ 1, missing dir/schema, no *_abilities.json, invalid JSON); InMemoryWebApplicationFactory host test + WebApplicationFactory with empty abilities dir fails startup.

  9. Test factories — pin Content:AbilitiesDirectory in InMemoryWebApplicationFactory and other WebApplicationFactory subclasses that already pin recipes/items (same pattern as NEO-66).

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Combat/AbilityDefRow.cs Load-time AbilityDef row DTO.
server/NeonSprawl.Server/Game/Combat/AbilityDefinitionCatalog.cs In-memory catalog + TryGetAbility.
server/NeonSprawl.Server/Game/Combat/AbilityDefinitionCatalogLoader.cs Disk I/O, schema validation, E5M1 gate.
server/NeonSprawl.Server/Game/Combat/AbilityCatalogPathResolution.cs Directory/schema discovery and config resolution.
server/NeonSprawl.Server/Game/Combat/PrototypeE5M1AbilityCatalogRules.cs Frozen four ids gate (sync comment to Python).
server/NeonSprawl.Server/Game/Combat/AbilityCatalogServiceCollectionExtensions.cs AddAbilityDefinitionCatalog DI registration.
server/NeonSprawl.Server.Tests/Game/Combat/AbilityCatalogTestPaths.cs Repo content/abilities + schema discovery for tests.
server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs AAA loader + host startup tests.
docs/plans/NEO-77-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs Add AbilitiesDirectory, AbilityDefSchemaPath under Content section.
server/NeonSprawl.Server/Program.cs Register and eagerly resolve AbilityDefinitionCatalog at startup.
server/NeonSprawl.Server/appsettings.json Optional empty Content:AbilitiesDirectory / schema path keys for override documentation (mirror other catalogs).
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs Pin Content:AbilitiesDirectory to discovered repo path so existing tests keep booting.
server/NeonSprawl.Server.Tests/Game/PositionState/PostgresWebApplicationFactory.cs Pin abilities directory (same as recipes/items).
server/NeonSprawl.Server.Tests/Game/Interaction/SalvageActivityDeniedRegistryWebApplicationFactory.cs Pin abilities directory.
server/NeonSprawl.Server.Tests/Game/Skills/RefineActivityDeniedRegistryWebApplicationFactory.cs Pin abilities directory.
server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardDeniedRegistryWebApplicationFactory.cs Pin abilities directory.
server/README.md New Ability catalog (content/abilities, NEO-77) section (config keys, discovery, fail-fast, E5M1 parity with CI).
docs/decomposition/modules/documentation_and_implementation_alignment.md E5.M1 row — note NEO-77 server load when implementation completes.
docs/decomposition/modules/E5_M1_CombatRulesEngine.md Status line — E5M1-02 server load landed when complete.
docs/plans/E5M1-prototype-backlog.md E5M1-02 checkboxes when implementation completes.

Tests

Test file What it covers
server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionCatalogLoaderTests.cs Unit: valid four-ability prototype catalog loads; abilities not array; schema violation (negative baseDamage, zero cooldownSeconds, empty displayName); duplicate id across files; wrong/missing E5M1 ids; schemaVersion ≠ 1; missing dir/schema; no *_abilities.json files; invalid JSON. Host: InMemoryWebApplicationFactory + /health + DI AbilityDefinitionCatalog count 4; WebApplicationFactory with empty abilities dir fails startup with actionable message.

Manual QA: Skipped — no player-visible HTTP; acceptance is fully covered by AAA loader/host tests above.

Open questions / risks

Question / risk Agent recommendation Status
Constants drift vs Python Comment on PrototypeE5M1AbilityCatalogRules pointing at scripts/validate_content.py PROTOTYPE_E5M1_ABILITY_IDS / _prototype_e5m1_ability_gate. adopted
Test cwd InMemoryWebApplicationFactory must set Content:AbilitiesDirectory explicitly (do not rely on discovery alone in CI). adopted
Registry consumers Hotbar/cast/combat use IAbilityDefinitionRegistry in NEO-79+; this story only exposes AbilityDefinitionCatalog for boot + direct test lookup. PrototypeAbilityRegistry unchanged. adopted
Global cooldown still active Expected — catalog lands before E5M1-07 wires per-ability cooldownSeconds; load does not change runtime cast behavior. adopted

Decisions (kickoff)

  • AbilityDefinitionCatalog singleton only — defer IAbilityDefinitionRegistry and PrototypeAbilityRegistry migration to NEO-79 (user confirmed kickoff).
  • In-process validation + loader and host boot tests per NEO-51 / NEO-66 precedent.
  • No cross-catalog checks — ability rows have no item/skill refs in Slice 1; loader has no catalog dependencies.