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

123 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 plans 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.