13 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-79 — IAbilityDefinitionRegistry + 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 (
ContentPathsOptionsextension). - CI-parity validation:
schemaVersion, row schema (ability-def), duplicateid, E5M1 four-id allowlist gate. - DI registration of
AbilityDefinitionCatalogsingleton; eager resolve inProgram.cs. - Unit + host startup tests (AAA).
server/README.mdsection.
Out of scope (from Linear):
IAbilityDefinitionRegistry/ hotbar-cast migration fromPrototypeAbilityRegistry(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, duplicateid, E5M1 gate failure). AbilityDefinitionCatalogresolves prototype rows by abilityid(e.g.prototype_pulse,prototype_burst) withbaseDamage,cooldownSeconds, optionalabilityKind.- 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).
Implementation reconciliation (shipped)
- Loader:
AbilityDefinitionCatalogLoader— CI-parity validation via JsonSchema.Net + E5M1 gate. - Catalog:
AbilityDefinitionCatalog+AbilityDefRow—TryGetAbilitylookup by id. - Gate rules:
PrototypeE5M1AbilityCatalogRules— sync comment toscripts/validate_content.pyPROTOTYPE_E5M1_ABILITY_IDS. - DI / startup:
AbilityCatalogServiceCollectionExtensions+ eager resolve inProgram.cs. - Tests:
AbilityDefinitionCatalogLoaderTests— 14 AAA cases (loader + host). - Docs:
server/README.mdability catalog section; E5.M1 module doc + alignment register + E5M1 backlog E5M1-02 checkboxes. - Unchanged:
PrototypeAbilityRegistry— migration deferred to NEO-79.
Technical approach
-
Game/Combat/row DTO —AbilityDefRow(Id,DisplayName,BaseDamage,CooldownSeconds, optionalAbilityKindenum or string). -
AbilityDefinitionCatalogLoader.Load(abilitiesDirectory, schemaPath, logger)— single-pass load mirroringvalidate_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 == 1and top-levelabilitiesarray. - Per row: validate against schema; on clean rows track
id; buildAbilityDefRow; reject duplicateidacross files. - Run
PrototypeE5M1AbilityCatalogRules.TryGetE5M1GateError(mirrorPROTOTYPE_E5M1_ABILITY_IDS/_prototype_e5m1_ability_gateinscripts/validate_content.py). - Throw
InvalidOperationExceptionwith aggregated, sorted messages prefixedAbility catalog validation failed:.
- Enumerate
-
AbilityDefinitionCatalog— immutable snapshot:AbilitiesDirectory,ById,DistinctAbilityCount,CatalogJsonFileCount;TryGetAbility(id, out AbilityDefRow?)for lookups. -
AbilityCatalogPathResolution—TryDiscoverAbilitiesDirectory,ResolveAbilitiesDirectory,ResolveAbilityDefSchemaPath(parentcontent/schemas/layout when unset). -
PrototypeE5M1AbilityCatalogRules— frozen four ability ids; sync comment toscripts/validate_content.pyPROTOTYPE_E5M1_ABILITY_IDS/_prototype_e5m1_ability_gate. Ids:prototype_pulse,prototype_guard,prototype_dash,prototype_burst(same set asPrototypeAbilityRegistry— unchanged this story). -
AbilityCatalogServiceCollectionExtensions.AddAbilityDefinitionCatalog— bindContentPathsOptions; registerAbilityDefinitionCatalogsingleton factory that resolves paths and calls loader. No dependency on item/skill catalogs (Slice 1 abilities have no cross-ref fields). -
Program.cs—AddAbilityDefinitionCatalog(configuration)alongside existing catalog registrations; afterBuild(),GetRequiredService<AbilityDefinitionCatalog>()with other eager catalog resolves. -
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, badbaseDamage/cooldownSeconds, emptydisplayName,schemaVersion≠ 1, missing dir/schema, no*_abilities.json, invalid JSON);InMemoryWebApplicationFactoryhost test +WebApplicationFactorywith empty abilities dir fails startup. -
Test factories — pin
Content:AbilitiesDirectoryinInMemoryWebApplicationFactoryand otherWebApplicationFactorysubclasses 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)
AbilityDefinitionCatalogsingleton only — deferIAbilityDefinitionRegistryandPrototypeAbilityRegistrymigration 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.