# 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](../decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md) · 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`](../../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.json` validates the prototype catalog shape. - `content/mastery/prototype_salvage_mastery.json` — one **`MasteryTrack`** for **`salvage`** (tier 1 @ level 2 branches; tier 2 @ level 4 perks). - `scripts/validate_content.py` — unknown `skillId`, duplicate `perkId`, 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` / `intrusion` mastery rows (CI must **not** require them). ## Acceptance criteria checklist - [x] `content/schemas/mastery-catalog.schema.json` (or equivalent) validates prototype catalog. - [x] `content/mastery/prototype_salvage_mastery.json` ships one **MasteryTrack** for `salvage` only (tier 1 @ level 2: `scrap_efficiency` vs `bulk_haul`; tier 2 @ level 4 with one perk per branch path). - [x] CI fails on unknown `skillId`, duplicate perk ids, or invalid branch references. - [x] Designer note in module doc: mutually exclusive branch picks per tier; no respec in v1. - [x] `refine` / `intrusion` have **no** mastery rows yet (CI does not require them). ## Technical approach 1. **Schema (`mastery-catalog.schema.json`):** Draft 2020-12 object with `schemaVersion: 1`, top-level **`perks`** map (keys = perk `id`), and **`tracks`** array. Each track: `skillId`, ordered **`tiers`** (`tierIndex`, `requiredLevel`, `branches`). Each branch: `branchId`, optional `displayName`, `perkIds` (array, may be empty). Each perk: `id`, `displayName`, optional `effectKind` stub string. Enforce `snake_case` patterns for ids; `additionalProperties: false` on objects. 2. **Prototype catalog:** Single file `content/mastery/prototype_salvage_mastery.json` with exactly **one** track (`salvage`), **two** tiers, **two** branches per tier (same `branchId` values tier-to-tier), **two** perks in `perks` map referenced only from tier 2. Stub `effectKind` values (e.g. `gather_yield_modifier`) for future E3 wiring — no gameplay apply. 3. **`validate_content.py`:** After existing skill + level-curve validation, load skill ids from validated `*_skills.json` files, then validate `content/mastery/*_mastery.json` against mastery schema. Post-schema gates: - **Unknown `skillId`:** track `skillId` ∉ loaded skill ids. - **Duplicate `perkId`:** same id in `perks` map or referenced twice across branches. - **Branch integrity:** every `perkIds[]` entry exists in `perks`; tier 2 `branchId` set equals tier 1 `branchId` set; `requiredLevel` strictly increases by tier; unique `branchId` per tier. - **Slice 4 gate:** exactly **one** track; its `skillId` is **`salvage`** only (no `refine` / `intrusion` tracks). 4. **Docs:** Extend `content/README.md` with `mastery/` paths and freeze reminder. Ensure E2.M3 module freeze box lists concrete `perkId` values. Update CT.M1 CI prototype paragraph. Refresh `documentation_and_implementation_alignment.md` E2.M3 row. Commit included WIP decomposition/backlog edits. 5. **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.py` constants. - **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`, orphan `perkId`, duplicate `perkId` reference, Slice 4 multi-track gate — all non-zero exit with actionable stderr.