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

11 KiB
Raw Blame History

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.
  • IMasteryCatalogRegistry resolves tracks by skillId and perk metadata by perkId.
  • Cross-check: every catalog skillId exists in ISkillDefinitionRegistry.
  • Unit tests (AAA) for loader happy path + duplicate id / unknown skill deny.
  • Host-level test: valid repo catalog → WebApplicationFactory starts; invalid fixture → startup fails with actionable message.

Out of scope (from Linear):

  • Per-player perk state, unlock engine, HTTP (NEO-4749).
  • Client HUD.
  • Applying effectKind gameplay modifiers.

Acceptance criteria checklist

  • Fail-fast boot load for mastery catalog(s) under agreed content path.
  • IMasteryCatalogRegistry resolves tracks by skillId and enumerates perk definitions.
  • Cross-check: every catalog skillId exists in ISkillDefinitionRegistry.
  • Unit tests for loader happy path + duplicate id / unknown skill deny.

Technical approach

  1. Configuration: Extend ContentPathsOptions with MasteryDirectory and optional MasteryCatalogSchemaPath. When unset, discover content/mastery by walking ancestors of AppContext.BaseDirectory (same pattern as content/skills). Schema default: {parent of mastery dir}/schemas/mastery-catalog.schema.json.
  2. Path resolution: Add MasteryCatalogPathResolution under Game/Mastery/ (mirror SkillCatalogPathResolution).
  3. Loader (MasteryCatalogLoader): Enumerate *_mastery.json (top-level only, sorted). Per file:
    • Parse JSON; validate whole document against mastery-catalog.schema.json via JsonSchema.Net (already on server from NEO-34).
    • Post-schema gates aligned with validate_content.py _validate_mastery_catalogs + _prototype_slice4_gate:
      • perks map key == PerkDef.id
      • Global duplicate perkId across files
      • Global duplicate branch perkIds reference (one reference site per perk id catalog-wide)
      • Unknown perkIds references; unreferenced perks entries
      • Unknown skillId (against skill ids passed in from ISkillDefinitionRegistry / loaded skill catalog)
      • Per-tier: duplicate branchId; requiredLevel strictly increasing; tier N branchId set equals tier N1
      • Slice 4: exactly one track across all files; sole track skillId == salvage (PrototypeSlice4MasteryCatalogRules, constant cross-referenced to Python PROTOTYPE_SLICE4_SALVAGE_SKILL_ID)
    • Server-only (kickoff): per track, tierIndex values unique and exactly {1..N} with no gaps (schema allows gaps; prototype catalog is 1..2).
  4. Models: Immutable POCOs — MasteryCatalog, MasteryTrackRow, MasteryTierRow, MasteryBranchRow, PerkDefRow — enough for registry lookups and logging.
  5. Registry: IMasteryCatalogRegistry with TryGetTrack(string? skillId, out MasteryTrackRow?), TryGetPerk(string? perkId, out PerkDefRow?), GetTracksInSkillIdOrder(), GetPerksInIdOrder() (or equivalent; name flexible).
  6. DI: AddMasteryCatalog(IServiceCollection, IConfiguration) registers MasteryCatalog + IMasteryCatalogRegistry; loader receives skill ids from SkillDefinitionCatalog (resolve skill catalog first in factory delegate). Eager resolve in Program.cs after skill catalog (same as NEO-34).
  7. Logging: Information log with resolved mastery directory, file count, track count, perk count.
  8. Docs: server/README.mdContent:MasteryDirectory / schema override; note server tierIndex gate. 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 tierIndex gate to validate_content.py (not required for NEO-46).
  • Test discovery: dotnet test from server/ must find content/mastery via 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.