# NEO-76 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-76 |
| **Title** | E5M1-01: Prototype AbilityDef catalog + schemas + CI |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-76/e5m1-01-prototype-abilitydef-catalog-schemas-ci |
| **Module** | [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) · Epic 5 Slice 1 · backlog **E5M1-01** |
| **Branch** | `NEO-76-e5m1-prototype-backlog` |
| **Precursor** | [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md) — hardcoded allowlist in [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) (unchanged this story) |
| **Pattern** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) / [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) — row JSON Schema + catalog envelope + `scripts/validate_content.py` gate |
| **Blocks** | [NEO-77](https://linear.app/neon-sprawl/issue/NEO-77) — server ability catalog load |

## Kickoff clarifications

| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Prototype stat freeze** | `baseDamage` / `cooldownSeconds` / `abilityKind` per row? | **Recommended table** — pulse 25 dmg / 3.0s; burst 40 / 5.0; guard 0 / 6.0 utility; dash 0 / 4.0 movement; four pulse casts = 100 HP dummy defeat; pulse cooldown matches NEO-32 global 3s. | **User:** recommended table (adopted below). |
| **abilityKind on rows** | Required on all four prototype rows? | **Include on all four** — optional in schema but aids E5M1-04 GET / E5M1-11 HUD without follow-up schema churn. | **Adopted** (bundled with stat freeze). |
| **Catalog envelope** | File shape? | `{ "schemaVersion": 1, "abilities": [ … ] }` in `content/abilities/prototype_abilities.json` (mirror items / recipes). | **Adopted** (NEO-50 / NEO-65 precedent). |
| **CI constant name** | Slice gate identifier? | **`PROTOTYPE_E5M1_ABILITY_IDS`** — Epic 5 Slice 1; avoids collision with economy Slice 1–4 numbering in `validate_content.py`. | **Adopted**. |

## Goal, scope, and out-of-scope

**Goal:** Lock content shape and CI validation for **four** frozen `AbilityDef` rows before server load (NEO-77+).

**In scope (from Linear + [E5M1-01](E5M1-prototype-backlog.md#e5m1-01--prototype-abilitydef-catalog--schemas--ci)):**

- `content/schemas/ability-def.schema.json`.
- `content/abilities/prototype_abilities.json` — four frozen ids (freeze table below).
- `scripts/validate_content.py` — schema validation, duplicate `id`, exact four-id allowlist, non-empty `displayName`.
- Designer note + **Prototype Slice 1 freeze** table in [E5_M1](../decomposition/modules/E5_M1_CombatRulesEngine.md) + `content/README.md`.
- **CT.M1** PR gate paragraph for ability catalogs.
- `documentation_and_implementation_alignment.md` E5.M1 row → **In Progress** / NEO-76.

**Out of scope (from Linear):**

- Server loader, combat engine, HTTP, Godot (NEO-77+).
- Changing [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) or global cooldown behavior (NEO-77 / E5M1-07).

## Acceptance criteria checklist

- [x] PR gate validates ability JSON against schema.
- [x] Exactly four prototype ability ids; duplicate `id` fails CI.
- [x] Stable id list documented in module doc freeze box.

## Implementation reconciliation (shipped)

- **Schema:** [`ability-def.schema.json`](../../content/schemas/ability-def.schema.json) — required `id`, `displayName`, `baseDamage`, `cooldownSeconds`; optional `abilityKind`.
- **Catalog:** [`prototype_abilities.json`](../../content/abilities/prototype_abilities.json) — four frozen rows per kickoff stat table.
- **CI:** `PROTOTYPE_E5M1_ABILITY_IDS` gate in [`validate_content.py`](../../scripts/validate_content.py).
- **Docs:** E5.M1 freeze table, `content/README.md`, CT.M1, alignment register, E5M1-01 backlog checkboxes.

## Technical approach

1. **Row schema (`ability-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `id`, `displayName`, `baseDamage`, `cooldownSeconds`. `id` pattern `^[a-z][a-z0-9_]*$`. `displayName` `minLength: 1`. `baseDamage` integer ≥ 0. `cooldownSeconds` number exclusiveMinimum 0 (positive). Optional: `abilityKind` enum `attack` \| `utility` \| `movement` (present on all four prototype rows).

2. **Catalog file:** `content/abilities/prototype_abilities.json` with `schemaVersion: 1` and four rows from the freeze table.

3. **Frozen content values (E5M1-01 — kickoff adopted):**

   | `AbilityDef.id` | `displayName` | `baseDamage` | `cooldownSeconds` | `abilityKind` |
   |-----------------|---------------|--------------|-------------------|---------------|
   | `prototype_pulse` | Prototype Pulse | 25 | 3.0 | `attack` |
   | `prototype_burst` | Prototype Burst | 40 | 5.0 | `attack` |
   | `prototype_guard` | Prototype Guard | 0 | 6.0 | `utility` |
   | `prototype_dash` | Prototype Dash | 0 | 4.0 | `movement` |

   **Rules:** Do **not** rename `id` values after ship — change `displayName` only, or add a new `id` in a follow-up issue. Relaxing the four-id CI gate requires a new Linear issue. **`prototype_pulse`** at 25 dmg × 4 casts = 100 HP dummy defeat (E5M1 backlog max HP default). **`prototype_pulse`** cooldown 3.0s matches current [`AbilityPrototypeCooldown.GlobalDuration`](../../server/NeonSprawl.Server/Game/AbilityInput/AbilityPrototypeCooldown.cs) until E5M1-07 switches to per-ability catalog cooldowns.

4. **`validate_content.py`:**
   - Add `ABILITY_SCHEMA`, `ABILITIES_DIR`; discover `content/abilities/*_abilities.json`.
   - `_validate_ability_catalogs`: envelope `schemaVersion: 1`, top-level `abilities` array; validate each row against `ability-def.schema.json`; track `seen_ids`; reject duplicates across files.
   - **`_prototype_e5m1_ability_gate`:** ids must equal `PROTOTYPE_E5M1_ABILITY_IDS` (frozenset of four ids above).
   - Run ability validation after existing catalogs succeed (no cross-catalog refs in Slice 1).
   - Require ability schema file in `main()` startup checks; extend module docstring + success summary with ability file + id counts.
   - Keep `PROTOTYPE_E5M1_ABILITY_IDS` in sync with [E5.M1 freeze table](../decomposition/modules/E5_M1_CombatRulesEngine.md#prototype-slice-1-freeze-e5m1-01) and future NEO-77 server loader.

5. **Docs:**
   - `content/README.md` — add `abilities/` table row + E5 Slice 1 freeze paragraph.
   - `E5_M1_CombatRulesEngine.md` — add **Prototype Slice 1 freeze (E5M1-01)** subsection with id + stat table + CI rules line.
   - `CT_M1_ContentValidationPipeline.md` — ability catalog PR gate paragraph.
   - `documentation_and_implementation_alignment.md` — E5.M1 row → **In Progress** / NEO-76 catalog landed.
   - `E5M1-prototype-backlog.md` — E5M1-01 checkboxes when implementation completes.

6. **No server/C#/client changes** in this story.

## Files to add

| Path | Purpose |
|------|---------|
| `content/schemas/ability-def.schema.json` | JSON Schema for a single `AbilityDef` row. |
| `content/abilities/prototype_abilities.json` | Prototype four-ability catalog (`schemaVersion` + `abilities` array). |
| `docs/plans/NEO-76-implementation-plan.md` | This plan. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `scripts/validate_content.py` | Load/validate ability catalogs; duplicate `id`; E5M1 four-id gate; success summary. |
| `content/README.md` | Document `content/abilities/`, schema path, E5 Slice 1 freeze (active catalog). |
| `docs/decomposition/modules/E5_M1_CombatRulesEngine.md` | Freeze table + CI enforcement note; link to plan when complete. |
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for ability catalog validation. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M1 / NEO-76 status after catalog lands. |
| `docs/plans/E5M1-prototype-backlog.md` | E5M1-01 checkboxes + landed note when complete. |

## Tests

| Item | Coverage |
|------|----------|
| **CI / PR gate** | `.github/workflows/pr-gate.yml` already runs `python scripts/validate_content.py` — extended script is the regression signal. |
| **Manual negative cases** | From repo root after implementation: (1) duplicate ability `id` → non-zero exit; (2) remove one frozen id → gate error; (3) break schema (negative `baseDamage` or zero `cooldownSeconds`) → schema error; (4) empty `displayName` → schema error. |
| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports ability catalog + existing catalogs OK. |

No dedicated pytest module in-repo today (same as NEO-50 / NEO-65).

## Open questions / risks

| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Catalog growth** | E5M1 gate requires **exactly** the four frozen ids; expanding the roster needs a follow-up issue to change `PROTOTYPE_E5M1_ABILITY_IDS`. | **adopted** |
| **C# vs Python drift** | None until NEO-77; document “keep in sync” constant names in both places when server load lands (mirror NEO-66 / `PrototypeSlice3RecipeCatalogRules`). | **adopted** |
| **Runtime still uses global 3s cooldown** | Expected — content lands before E5M1-07 wires per-ability `cooldownSeconds`; pulse 3.0s pre-aligns catalog with NEO-32 behavior. | **adopted** |
| **Zero-damage guard/dash before E5M1-06** | Catalog values are forward-looking; cast accept path still has no damage until E5M1-07. Zero-damage rows support E5M1-06 unit tests. | **adopted** |

None blocking.
