123 lines
9.1 KiB
Markdown
123 lines
9.1 KiB
Markdown
# 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 and validate **`tierIndex`** uniqueness / sequential 1..N per track (schema allows gaps today).
|
||
- **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.
|