9.0 KiB
9.0 KiB
NEO-45 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-45 |
| Title | E2.M3: Lock prototype salvage mastery catalog + schemas + CI |
| Linear | https://linear.app/neon-sprawl/issue/NEO-45/e2m3-lock-prototype-salvage-mastery-catalog-schemas-ci |
| Module | E2.M3 — MasteryAndPerkUnlocks · Epic 2 Slice 4 |
| Branch | NEO-45-salvage-mastery-catalog-schemas-ci |
Kickoff clarifications
| Topic | Question | Answer |
|---|---|---|
| PerkDef layout | Top-level perks map vs inline on branches? |
Top-level perks map — tracks reference perkId strings (dedupes metadata; mirrors skill catalog style). |
| Tier 1 unlocks | Perks on branch pick at level 2? | Branch pick only — tier-1 branches have empty perkIds; first perks unlock at tier 2 (requiredLevel 4). |
Frozen perkId strings |
Who names tier-2 perks? | Agent proposal (below); change displayName only after ship. |
| WIP decomposition docs | Include uncommitted E2.M3 doc edits on this branch? | Yes — commit with kickoff/plan work. |
| NEO-33 dependency | Blocker status? | Satisfied — salvage / refine / intrusion frozen in content/skills/prototype_skills.json (NEO-33 Done). |
Frozen prototype ids (Slice 4 — salvage flagship)
| Kind | Value | Notes |
|---|---|---|
skillId |
salvage |
Only skill with a MasteryTrack in prototype catalog. |
Tier 1 branchId |
scrap_efficiency, bulk_haul |
Mutually exclusive pick @ requiredLevel 2; no perks yet. |
Tier 2 branchId |
same ids | Continuation paths @ requiredLevel 4; one perk each. |
Tier 2 perkId |
salvage_scrap_efficiency_1, salvage_bulk_haul_1 |
Immutable after ship; tune player text via displayName. |
Goal, scope, and out-of-scope
Goal: Lock the salvage flagship mastery catalog under content/mastery/ with JSON Schema and CI validation (scripts/validate_content.py) so tiers, branches, and perk ids are stable before server load (NEO-46).
In scope (from Linear):
content/schemas/mastery-catalog.schema.jsonvalidates the prototype catalog shape.content/mastery/prototype_salvage_mastery.json— oneMasteryTrackforsalvage(tier 1 @ level 2 branches; tier 2 @ level 4 perks).scripts/validate_content.py— unknownskillId, duplicateperkId, branch/perk reference integrity.- Designer note: mutually exclusive branch picks per tier; no respec in v1 (E2.M3 module +
content/README.md). - Include WIP E2.M3 decomposition / backlog doc updates on this branch.
Out of scope (from Linear):
- Server boot load (NEO-46).
- Perk unlock engine, HTTP, client HUD (NEO-47+).
- Separate mastery XP bar; gig perks; respec.
refine/intrusionmastery rows (CI must not require them).
Acceptance criteria checklist
content/schemas/mastery-catalog.schema.json(or equivalent) validates prototype catalog.content/mastery/prototype_salvage_mastery.jsonships one MasteryTrack forsalvageonly (tier 1 @ level 2:scrap_efficiencyvsbulk_haul; tier 2 @ level 4 with one perk per branch path).- CI fails on unknown
skillId, duplicate perk ids, or invalid branch references. - Designer note in module doc: mutually exclusive branch picks per tier; no respec in v1.
refine/intrusionhave no mastery rows yet (CI does not require them).
Technical approach
- Schema (
mastery-catalog.schema.json): Draft 2020-12 object withschemaVersion: 1, top-levelperksmap (keys = perkid), andtracksarray. Each track:skillId, orderedtiers(tierIndex,requiredLevel,branches). Each branch:branchId, optionaldisplayName,perkIds(array, may be empty). Each perk:id,displayName, optionaleffectKindstub string. Enforcesnake_casepatterns for ids;additionalProperties: falseon objects. - Prototype catalog: Single file
content/mastery/prototype_salvage_mastery.jsonwith exactly one track (salvage), two tiers, two branches per tier (samebranchIdvalues tier-to-tier), two perks inperksmap referenced only from tier 2. StubeffectKindvalues (e.g.gather_yield_modifier) for future E3 wiring — no gameplay apply. validate_content.py: After existing skill + level-curve validation, load skill ids from validated*_skills.jsonfiles, then validatecontent/mastery/*_mastery.jsonagainst mastery schema. Post-schema gates:- Unknown
skillId: trackskillId∉ loaded skill ids. - Duplicate
perkId: same id inperksmap or referenced twice across branches. - Branch integrity: every
perkIds[]entry exists inperks; tier 2branchIdset equals tier 1branchIdset;requiredLevelstrictly increases by tier; uniquebranchIdper tier. - Slice 4 gate: exactly one track; its
skillIdissalvageonly (norefine/intrusiontracks).
- Unknown
- Docs: Extend
content/README.mdwithmastery/paths and freeze reminder. Ensure E2.M3 module freeze box lists concreteperkIdvalues. Update CT.M1 CI prototype paragraph. Refreshdocumentation_and_implementation_alignment.mdE2.M3 row. Commit included WIP decomposition/backlog edits. - No server/C#/client changes in this story.
Files to add
| Path | Purpose |
|---|---|
content/schemas/mastery-catalog.schema.json |
JSON Schema for mastery catalog files (perks + tracks). |
content/mastery/prototype_salvage_mastery.json |
Prototype salvage MasteryTrack, tier/branch tree, and perk definitions. |
Files to modify
| Path | Rationale |
|---|---|
scripts/validate_content.py |
Validate mastery catalogs; cross-check skillId; duplicate perkId; branch/perk integrity; Slice 4 salvage-only gate. |
content/README.md |
Document content/mastery/, schema path, Slice 4 freeze, local validate command. |
docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md |
Confirm designer note + freeze table with concrete perkId values (WIP edits). |
docs/decomposition/modules/CT_M1_ContentValidationPipeline.md |
PR gate paragraph: mastery catalog validation. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E2.M3 / NEO-45 row after catalog lands. |
docs/decomposition/epics/epic_02_skills_and_progression.md |
Slice 4 pointer consistency (WIP edits). |
docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md |
E2.M3 cross-link (WIP edits). |
docs/decomposition/modules/module_dependency_register.md |
E2.M3 note (WIP edits). |
docs/plans/E2M3-pre-production-backlog.md |
Pre-production backlog (untracked → tracked). |
Tests
| Item | Coverage |
|---|---|
| CI / PR gate | .github/workflows/pr-gate.yml already runs python scripts/validate_content.py — extend script; no workflow change required unless gate message needs updating. |
| Manual negative cases | Temporarily break catalog (unknown skillId, duplicate perkId, orphan perkIds reference, extra refine track) and confirm non-zero exit + actionable stderr. |
| Manual happy path | pip install -r scripts/requirements-content.txt && python scripts/validate_content.py reports mastery + existing catalogs OK. |
| Dedicated pytest | Not required for NEO-45 (same as NEO-33 / NEO-39 content-only pattern); optional follow-up if we want fixture-based script tests later. |
| Server tests | None — server load is NEO-46. |
Open questions / risks
- Tier index convention: Use explicit
tierIndex(1-based) in JSON for stable server persistence keys in NEO-47; document in schema description. - Catalog growth: Slice 4 gate mirrors NEO-33 — relaxing “salvage only” requires a new Linear issue to change
validate_content.pyconstants. - Drift vs NEO-46: Server loader must mirror Python rules; NEO-46 plan should reference this plan’s gate constants.
- None blocking at kickoff after clarifications above.
Decisions
| Decision | Rationale |
|---|---|
Top-level perks map |
User kickoff choice; avoids duplicating perk metadata on every branch reference. |
Empty tier-1 perkIds |
User kickoff choice; matches Linear AC (“tier 2 … one perk per branch path”). |
Frozen tier-2 perk ids salvage_scrap_efficiency_1, salvage_bulk_haul_1 |
Prefix with skill + branch semantics; unique globally for future multi-track catalogs. |
| Include WIP E2.M3 docs on branch | User kickoff choice; keeps decomposition aligned with Slice 4 backlog before implementation PR. |
Verification (implementation)
python3 scripts/validate_content.py— exit 0; reports 1 mastery catalog + 1 salvage track.python3 scripts/check_decomposition_language.py— exit 0.- Negative cases (restored catalog after each): unknown
skillId, orphanperkId, duplicateperkIdreference, Slice 4 multi-track gate — all non-zero exit with actionable stderr.