# E2.M1 — SkillDefinitionRegistry

## Summary

| Field | Value |
|--------|--------|
| **Module ID** | E2.M1 |
| **Epic** | [Epic 2 — Skills and Progression Framework](../epics/epic_02_skills_and_progression.md) |
| **Linear (Epic 2 project)** | [Epic 2 — Classless Skill and Progression Framework](https://linear.app/neon-sprawl/project/epic-2-classless-skill-and-progression-framework-5200f84bf8b6) |
| **Stage target** | Prototype |
| **Status** | In Progress (see [dependency register](module_dependency_register.md)) |

## Linear backlog (Epic 2 Slice 1)

Issues use label **`E2.M1`**. Dependency order: **NEO-33 → NEO-34 → NEO-35 → NEO-36** (each issue’s description is canonical).

| Order | Issue | Summary |
|------:|-------|---------|
| 1 | [NEO-33](https://linear.app/neon-sprawl/issue/NEO-33/e2m1-lock-prototype-skilldef-trio-ci-catalog-contract) | Lock prototype `SkillDef` trio + CI catalog contract + designer note |
| 2 | [NEO-34](https://linear.app/neon-sprawl/issue/NEO-34/e2m1-server-loads-skilldef-catalog-at-startup-fail-fast) | Server loads catalog at startup (fail-fast) |
| 3 | [NEO-35](https://linear.app/neon-sprawl/issue/NEO-35/e2m1-skilldefinitionregistry-service-lookup-tests) | `SkillDefinitionRegistry` service + lookup tests |
| 4 | [NEO-36](https://linear.app/neon-sprawl/issue/NEO-36/e2m1-read-only-skill-catalog-http-projection-bruno) | Read-only skill catalog HTTP projection + Bruno |

`docs/plans/NEO-*-implementation-plan.md` files should be added when implementation kicks off (per [planning-implementation-docs](../../../.cursor/rules/planning-implementation-docs.md)).

## Purpose

Central catalog for **non-combat** skill metadata: **stable IDs**, **categories**, **allowed XP sources**, and unlock prerequisites. All prototype activities reference skills by **`SkillDef.id`** validated against this registry. **Combat role progression** is **not** a `SkillDef`—use gig progression ([`docs/game-design/gigs.md`](../../game-design/gigs.md)).

## Data contract (schema)

| Artifact | Path |
|----------|------|
| **JSON Schema** | [`content/schemas/skill-def.schema.json`](../../../content/schemas/skill-def.schema.json) |
| **Sample catalog** | [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json) |

**`SkillDef` fields (prototype):**

| Field | Rule |
|--------|------|
| **`id`** | Lowercase `snake_case`, **immutable** after ship. Used in saves, recipes, quests, `XpGrantEvent.skillId`, telemetry. **Never** rename to “fix” a skill—add a new `id` and migrate content if a skill splits. |
| **`category`** | One of `gather` \| `process` \| `make` \| `tech` — aligned with [`docs/game-design/skills.md`](../../game-design/skills.md) roster buckets (not gigs). |
| **`displayName`** | Player-facing string; **may change** without migrating progression data. |
| **`allowedXpSourceKinds`** | Non-empty set of grant channels [E2.M2](E2_M2_XpAwardAndLevelEngine.md) may apply to this skill. **Combat encounters** must **not** use skill XP—those award **gig XP**; `mission_reward` covers explicit mission/contract skill payouts. |

**`allowedXpSourceKinds` values:**

| Kind | Meaning |
|------|--------|
| `activity` | Primary loop (e.g. gather node complete, recipe craft complete, skill-based world interaction)—callers set `XpGrantEvent.sourceKind` to match. |
| `mission_reward` | Quest/contract hand-in or other scripted payout ([`docs/game-design/progression.md`](../../game-design/progression.md) **Mission rewards vs combat XP**). |
| `trainer` | NPC or teaching beat. |
| `book_or_item` | Book, chip, training consumable, or similar one-shot grant. |

Add new enum values only with a **schema version** / migration note in [CT.M1](CT_M1_ContentValidationPipeline.md) when CI enforces catalogs.

### Designer note: allowedXpSourceKinds and grant call sites

Every `SkillDef` row **must** include a **non-empty** `allowedXpSourceKinds` array (enforced by JSON Schema). Designers use it to agree with engineering **which systems may ever grant XP** to that skill:

- **`activity`** — wire gather nodes, recipe completions, skill-based world objectives, and similar **primary-loop** payouts; set `XpGrantEvent.sourceKind` (or successor) to `activity` on those code paths.
- **`mission_reward`** — wire **explicit** mission/contract hand-ins or scripted bonuses only; not a stand-in for combat encounter XP ([`docs/game-design/progression.md`](../../game-design/progression.md) — mission vs combat).
- **`trainer`** / **`book_or_item`** — wire NPC teaching beats and consumable/one-shot grants respectively; do not use for combat kills.

**Combat encounters award gig XP, not skill XP.** If a grant channel is missing from a skill’s list, [E2.M2](E2_M2_XpAwardAndLevelEngine.md) rejects the grant at apply time (**NEO-38** HTTP grant path)—keep catalogs honest so call sites do not drift.

## Prototype Slice 1 freeze ([NEO-33](https://linear.app/neon-sprawl/issue/NEO-33))

The **first shipped trio** under `content/skills/*.json` is **frozen** for downstream references (quests, recipes, E2.M2, E3) until a deliberate migration or follow-up issue expands the roster.

| `SkillDef.id` | `category` | Role |
|---------------|------------|------|
| **`salvage`** | `gather` | Prototype Gather anchor |
| **`refine`** | `process` | Prototype Process anchor (satisfies “Process or Make” for Slice 1) |
| **`intrusion`** | `tech` | Prototype Tech anchor |

**Rules:** Do **not** rename these `id` values to “fix” wording—change `displayName` only, or add a new `id` and migrate content. CI ([`scripts/validate_content.py`](../../../scripts/validate_content.py)) enforces **exactly** these three ids across all skill JSON files and **category coverage** (gather + tech + at least one of process/make). Relaxing or changing that gate belongs in a new Linear issue once the catalog intentionally grows.

## Responsibilities

- Load and validate `SkillDef` from data; expose `SkillCategory` and `UnlockRequirement` for gating.
- Reject unknown `skillId` or **XP grants** whose `sourceKind` is **not** listed on the target skill’s `allowedXpSourceKinds` (enforced in [E2.M2](E2_M2_XpAwardAndLevelEngine.md) at grant time).

## Key contracts

| Contract | Role |
|----------|------|
| `SkillDef` | Per-skill metadata: **id**, **category**, **displayName**, **allowedXpSourceKinds** (+ future unlock fields). |
| `SkillCategory` | `gather` \| `process` \| `make` \| `tech` for UX and balance. |
| `UnlockRequirement` | Prerequisites before XP or use counts apply. |

## Acceptance criteria (E2.M1 / Slice 1)

- Every loaded `SkillDef` validates against [`skill-def.schema.json`](../../../content/schemas/skill-def.schema.json) in CI ([CT.M1](CT_M1_ContentValidationPipeline.md#ci-prototype) — [`scripts/validate_content.py`](../../../scripts/validate_content.py) in **PR gate**) or at server boot.
- **Duplicate `id`** in a catalog fails validation.
- **Roster freeze for prototype:** Slice 1 locks **`salvage`**, **`refine`**, and **`intrusion`**; their **`id`** values are **stable**—downstream content (recipes, quests) may reference them; changing an `id` requires a **migration** or new row.
- Document for designers: each new skill row **must** declare **`allowedXpSourceKinds`** so engineers know which systems may grant XP (gather/craft hooks vs E7.M2 mission rewards only, etc.).

## Module dependencies

- **None**

## Dependents (by design)

- **E2.M2** — XpAwardAndLevelEngine (primary): validates grants against `allowedXpSourceKinds`.

## Related implementation slices

Epic 2 **Slice 1** — skill registry and prototype **non-combat** skills; invalid references fail at load or in CI.

## Source anchors

- Game design: [`docs/game-design/skills.md`](../../game-design/skills.md) — roster, **XP philosophy**, gig/skill boundary.
- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 2.
- [Module dependency register](module_dependency_register.md)
