11 KiB
11 KiB
NEO-46 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-46 |
| Title | E2.M3: Server loads mastery catalog at startup (fail-fast) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-46/e2m3-server-loads-mastery-catalog-at-startup-fail-fast |
| Module | E2.M3 — MasteryAndPerkUnlocks · Epic 2 Slice 4 |
| Branch | NEO-46-mastery-catalog-startup-load |
| Blocked by | NEO-45 (content + CI) — Done on main |
Kickoff clarifications
| Topic | Question | Answer |
|---|---|---|
| Code location | Game/Skills/ vs Game/Mastery/? |
Game/Mastery/ — E2.M3 boundary; NEO-47+ (unlock engine, PerkState, HTTP) stays cohesive. |
tierIndex gate |
Enforce uniqueness + sequential 1..N at server boot? | Yes — stricter than CI today; protects NEO-47 PerkState keys. |
| Validation strategy | C# in-process vs subprocess? | In-process C# mirroring scripts/validate_content.py (NEO-34 precedent). |
| NEO-45 gate parity | Which Python constants/rules? | Mirror PROTOTYPE_SLICE4_SALVAGE_SKILL_ID, Slice 4 single-track gate, unknown skillId, duplicate perkId, branch-set equality, unreferenced perks, branch/perk reference integrity — see NEO-45 plan and E2.M3 NEO-46 handoff. |
Goal, scope, and out-of-scope
Goal: On host startup, load all content/mastery/*_mastery.json files validated in CI, build an in-memory mastery catalog, cross-check every track skillId against ISkillDefinitionRegistry, and refuse to listen if any file is missing, malformed, or violates prototype gates. Expose read-only IMasteryCatalogRegistry for NEO-47+.
In scope (from Linear):
- Fail-fast boot load for mastery catalog(s) under agreed content path.
IMasteryCatalogRegistryresolves tracks byskillIdand perk metadata byperkId.- Cross-check: every catalog
skillIdexists inISkillDefinitionRegistry. - Unit tests (AAA) for loader happy path + duplicate id / unknown skill deny.
- Host-level test: valid repo catalog →
WebApplicationFactorystarts; invalid fixture → startup fails with actionable message.
Out of scope (from Linear):
- Per-player perk state, unlock engine, HTTP (NEO-47–49).
- Client HUD.
- Applying
effectKindgameplay modifiers.
Acceptance criteria checklist
- Fail-fast boot load for mastery catalog(s) under agreed content path.
IMasteryCatalogRegistryresolves tracks byskillIdand enumerates perk definitions.- Cross-check: every catalog
skillIdexists inISkillDefinitionRegistry. - Unit tests for loader happy path + duplicate id / unknown skill deny.
Technical approach
- Configuration: Extend
ContentPathsOptionswithMasteryDirectoryand optionalMasteryCatalogSchemaPath. When unset, discovercontent/masteryby walking ancestors ofAppContext.BaseDirectory(same pattern ascontent/skills). Schema default:{parent of mastery dir}/schemas/mastery-catalog.schema.json. - Path resolution: Add
MasteryCatalogPathResolutionunderGame/Mastery/(mirrorSkillCatalogPathResolution). - Loader (
MasteryCatalogLoader): Enumerate*_mastery.json(top-level only, sorted). Per file:- Parse JSON; validate whole document against
mastery-catalog.schema.jsonvia JsonSchema.Net (already on server from NEO-34). - Post-schema gates aligned with
validate_content.py_validate_mastery_catalogs+_prototype_slice4_gate:perksmap key ==PerkDef.id- Global duplicate
perkIdacross files - Global duplicate branch
perkIdsreference (one reference site per perk id catalog-wide) - Unknown
perkIdsreferences; unreferencedperksentries - Unknown
skillId(against skill ids passed in fromISkillDefinitionRegistry/ loaded skill catalog) - Per-tier: duplicate
branchId;requiredLevelstrictly increasing; tier NbranchIdset equals tier N−1 - Slice 4: exactly one track across all files; sole track
skillId==salvage(PrototypeSlice4MasteryCatalogRules, constant cross-referenced to PythonPROTOTYPE_SLICE4_SALVAGE_SKILL_ID)
- Server-only (kickoff): per track,
tierIndexvalues unique and exactly{1..N}with no gaps (schema allows gaps; prototype catalog is 1..2).
- Parse JSON; validate whole document against
- Models: Immutable POCOs —
MasteryCatalog,MasteryTrackRow,MasteryTierRow,MasteryBranchRow,PerkDefRow— enough for registry lookups and logging. - Registry:
IMasteryCatalogRegistrywithTryGetTrack(string? skillId, out MasteryTrackRow?),TryGetPerk(string? perkId, out PerkDefRow?),GetTracksInSkillIdOrder(),GetPerksInIdOrder()(or equivalent; name flexible). - DI:
AddMasteryCatalog(IServiceCollection, IConfiguration)registersMasteryCatalog+IMasteryCatalogRegistry; loader receives skill ids fromSkillDefinitionCatalog(resolve skill catalog first in factory delegate). Eager resolve inProgram.csafter skill catalog (same as NEO-34). - Logging: Information log with resolved mastery directory, file count, track count, perk count.
- Docs:
server/README.md—Content:MasteryDirectory/ schema override; note servertierIndexgate. Optional one-line pointer in E2.M3 module if implementation touches handoff section (only if needed for accuracy).
Gate constant parity (C# ↔ Python)
| Rule | Python | C# (planned) |
|---|---|---|
| Slice 4 sole track skill | PROTOTYPE_SLICE4_SALVAGE_SKILL_ID |
PrototypeSlice4MasteryCatalogRules.SalvageSkillId |
| Slice 4 track count | _prototype_slice4_gate |
TryGetSlice4GateError(trackSkillIds) |
Comment in C# constants: // Keep in sync with scripts/validate_content.py.
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Mastery/ContentPathsMasteryExtensions.cs |
Optional: mastery path helpers if not folded into shared resolution type. |
server/NeonSprawl.Server/Game/Mastery/MasteryCatalogPathResolution.cs |
Discover / resolve content/mastery and schema paths. |
server/NeonSprawl.Server/Game/Mastery/MasteryCatalogLoader.cs |
File I/O, schema validation, post-schema gates, skill cross-check. |
server/NeonSprawl.Server/Game/Mastery/MasteryCatalog.cs |
Immutable load result (tracks by skillId, perks by id). |
server/NeonSprawl.Server/Game/Mastery/MasteryTrackRow.cs |
DTO for one track after validation. |
server/NeonSprawl.Server/Game/Mastery/MasteryTierRow.cs |
Tier row (tierIndex, requiredLevel, branches). |
server/NeonSprawl.Server/Game/Mastery/MasteryBranchRow.cs |
Branch row (branchId, displayName, perkIds). |
server/NeonSprawl.Server/Game/Mastery/PerkDefRow.cs |
Perk metadata row. |
server/NeonSprawl.Server/Game/Mastery/PrototypeSlice4MasteryCatalogRules.cs |
Slice 4 gate (single salvage track). |
server/NeonSprawl.Server/Game/Mastery/IMasteryCatalogRegistry.cs |
Read-only lookup contract for game code. |
server/NeonSprawl.Server/Game/Mastery/MasteryCatalogRegistry.cs |
Adapter over MasteryCatalog. |
server/NeonSprawl.Server/Game/Mastery/MasteryCatalogServiceCollectionExtensions.cs |
AddMasteryCatalog DI registration. |
server/NeonSprawl.Server.Tests/Game/Mastery/MasteryCatalogTestPaths.cs |
Repo schema / fixture path discovery for tests. |
server/NeonSprawl.Server.Tests/Game/Mastery/MasteryCatalogLoaderTests.cs |
Loader unit tests (happy + failure modes). |
server/NeonSprawl.Server.Tests/Game/Mastery/MasteryCatalogRegistryTests.cs |
Registry lookup tests + prototype fixture parity. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs |
Add MasteryDirectory, MasteryCatalogSchemaPath. |
server/NeonSprawl.Server/Program.cs |
Register mastery catalog; eager-resolve after skill catalog. |
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs |
Set Content:MasteryDirectory (discover repo content/mastery) so host tests load real prototype catalog. |
server/NeonSprawl.Server.Tests/Game/PositionState/PostgresWebApplicationFactory.cs |
Same mastery path override as in-memory factory (if present). |
server/README.md |
Document mastery content paths and fail-fast behavior. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E2.M3 row: note NEO-46 server load when landed (during implementation). |
Tests
| Test file | Coverage |
|---|---|
MasteryCatalogLoaderTests.cs |
Happy: temp dir with copy of repo schema + minimal valid catalog matching prototype salvage shape → load succeeds, track/perk counts. Deny: malformed JSON; schema violation; unknown skillId (skill catalog stub with only refine); duplicate perkId across files; duplicate global perkIds reference; orphan perks entry; Slice 4 wrong track count / wrong skillId; tierIndex gap or duplicate. Assert InvalidOperationException messages include file path. |
MasteryCatalogRegistryTests.cs |
TryGetTrack("salvage"), TryGetPerk("salvage_scrap_efficiency_1"); unknown ids return false; ordered enumeration. |
MasteryCatalogLoaderTests.cs (host) |
InMemoryWebApplicationFactory + /health + DI IMasteryCatalogRegistry resolves salvage track from repo content. |
MasteryCatalogLoaderTests.cs (host negative) |
Invalid mastery directory override → host fails startup (mirror SkillDefinitionCatalogLoaderTests host case). |
All new [Fact] methods use AAA with // Arrange / // Act / // Assert labels per csharp-style.
Open questions / risks
- C# vs Python drift: Mitigate with shared constant names documented in both places; optional follow-up to add
tierIndexgate tovalidate_content.py(not required for NEO-46). - Test discovery:
dotnet testfromserver/must findcontent/masteryvia same ancestor walk as skills; verify in CI. - Load order: Mastery loader must run after skill catalog is available in DI (factory dependency on
SkillDefinitionCatalog).
None blocking after kickoff clarifications.
Decisions
| Decision | Rationale |
|---|---|
Game/Mastery/ folder |
User-approved kickoff; separates E2.M3 from E2.M1 skill catalog. |
tierIndex 1..N gate at server boot |
User-approved kickoff; protects NEO-47 persistence keys. |
| In-process C# validation | NEO-34 precedent; no Python on server images. |
Cross-check skill ids from loaded SkillDefinitionCatalog |
Linear AC; single source of truth with E2.M1. |