# NEO-43 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-43 |
| **Title** | Mission / quest rewards grant skill XP (sourceKind: mission_reward) |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-43/mission-quest-rewards-grant-skill-xp-sourcekind-mission-reward |
| **Labels** | E7.M2, E2.M2, server, Story |
| **Blocked by (Linear text)** | [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) — **done** on `main` (grant stack + `allowedXpSourceKinds`). **E7.M2** [RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) — **design only** at kickoff; no quest hand-in / `QuestRewardBundle` code in `server/` yet. |

## Kickoff clarifications

| Topic | Question / note | Resolution |
|--------|-----------------|------------|
| **Delivery before E7.M2 router** | E7.M2 router does not exist in repo; how to ship NEO-43? | **User (implementation):** accepted agent recommendation — **NEO-42-style prep:** **`MissionRewardSkillXpGrant`** + tests + README + manual QA; **E7.M2** wires the real quest hand-in later. |
| **Reward shape** | Fixed prototype vs generic skill + amount? | **User (implementation):** accepted agent recommendation — **generic** **`skillId`** + **`amount`**; helper fixes **`sourceKind: mission_reward`**. |
| **Combat encounters ≠ skill XP** | No encounter/gig XP code under `server/` today. | **User (implementation):** accepted agent recommendation — **README + manual QA + plan** document the invariant and current call sites; no synthetic encounter guard test until encounter code exists. |

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

**Goal:** When **E7.M2** (or any scripted mission/quest hand-in) applies **explicit** skill XP from a **mission reward** channel, route the grant through **E2.M2** / **NEO-38** using **`sourceKind: mission_reward`**, validated against the target skill’s **`allowedXpSourceKinds`**. **Not** the combat encounter pipe ([progression.md](../game-design/progression.md) **Mission rewards vs combat XP**).

**In scope (from Linear + defaults above):**

- **Preparatory (this slice):** one **server-side** helper that delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** with caller-supplied **`skillId`** / **`amount`** and fixed **`mission_reward`** `sourceKind`.
- **Tests:** happy path (grant applies when allowlist permits); deny path when **`mission_reward`** ∉ **`allowedXpSourceKinds`** for the chosen skill (stub registry pattern aligned with NEO-41/NEO-42 denied factories).
- **Docs:** README subsection + **`docs/manual-qa/NEO-43.md`** (verify via **`POST …/skill-progression`** with **`mission_reward`** and via helper in a test host, same spirit as NEO-42).

**Out of scope:**

- Full **`QuestRewardBundle`** schema, idempotent **`RewardDeliveryEvent`**, inventory/unlocks ([E7_M2_RewardAndUnlockRouter.md](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) backlog).
- **Combat encounter** implementation or gig XP awards.
- **Batch** mission grants in one HTTP call (can follow once router batches rewards).

## Acceptance criteria checklist

- [x] **Scripted mission rewards can include skill XP;** validated against **`allowedXpSourceKinds`** (helper + tests; manual QA documents **`POST …/skill-progression`** with **`mission_reward`** for skills that allow it).
- [x] **Combat encounters do not grant skill XP by default** — documented invariant + **current codebase** has no encounter grant path; gig XP remains separate (design reference: [progression.md](../game-design/progression.md), [E2_M1](../decomposition/modules/E2_M1_SkillDefinitionRegistry.md)).

## Decisions (implementation)

- **`MissionRewardSkillXpConstants.MissionRewardSourceKind`** — single canonical string for the helper and docs.
- **`MissionRewardDeniedRegistryWebApplicationFactory`** — **`salvage`** stub omits **`mission_reward`** (deny coverage); happy paths use disk catalog via **`InMemoryWebApplicationFactory`**.
- **Code review follow-up (2026-05-15):** **`documentation_and_implementation_alignment.md`** E2.M2 row lists **NEO-43 landed** and **Slice 3 still open** → **NEO-44** only; **`E7_M2_RewardAndUnlockRouter.md`** Summary **Status** notes NEO-43 prep; **`NEO-43-implementation-plan.md`** technical approach uses **`MissionRewardSkillXpConstants.MissionRewardSourceKind`** (matches code).

## Technical approach

1. Add **`MissionRewardSkillXpConstants`** (or equivalent single type) holding the canonical **`mission_reward`** string used by the helper (avoid string drift; match **`prototype_skills.json`** / NEO-38 docs).

2. Add **`MissionRewardSkillXpGrant`** (static class, parallel to **`RefineActivitySkillXpGrant`**):
   - Method e.g. **`GrantFromMissionReward(playerId, skillId, amount, registry, xpStore, levelCurve)`** calling **`TryApplyGrant`** with **`MissionRewardSkillXpConstants.MissionRewardSourceKind`**.
   - **Caller contract:** `playerId` normalized like other grant hooks; **`amount`** > 0 (delegate invalid amount to existing deny behavior); outcome may be ignored on “fire and forget” success paths or inspected when E7.M2 needs telemetry/deny handling.

3. **Tests** — factory with registry where a prototype skill **excludes** **`mission_reward`** for deny coverage; in-memory progression store; assert snapshot / store deltas consistent with **`RefineActivitySkillXpGrantTests`** style.

4. **Documentation** — **`server/README.md`**: short “Mission / quest skill XP (NEO-43)” subsection: E7.M2 should call **`MissionRewardSkillXpGrant`**; until then verify **`mission_reward`** via **`POST …/skill-progression`**; link **`docs/manual-qa/NEO-43.md`**.

5. **Decomposition anchor** — one sentence in **`docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md`** (or **`documentation_and_implementation_alignment.md`** if E7.M2 file should stay design-pure): server prep hook name + NEO-43 plan link.

## Files to add

| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpConstants.cs` | Canonical `mission_reward` source kind string for mission/quest grant path. |
| `server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpGrant.cs` | Static helper delegating to `SkillProgressionGrantOperations.TryApplyGrant` with fixed `mission_reward`. |
| `server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardSkillXpGrantTests.cs` | Happy path + GET snapshot alignment (optional mirror NEO-42); deny when `mission_reward` not allowed. |
| `server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardDeniedRegistryWebApplicationFactory.cs` | WebApplicationFactory with stub `ISkillDefinitionRegistry` where target skill omits `mission_reward` from `allowedXpSourceKinds`. |
| `docs/manual-qa/NEO-43.md` | curl/Bruno order: grant via `POST …/skill-progression` with `mission_reward` + note future E7.M2 calls helper. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `server/README.md` | Document NEO-43 mission-reward skill XP hook and verification until E7.M2 wires quest hand-in. |
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | Implementation anchor: which server helper to call when applying skill XP from `QuestRewardBundle` / mission payout. |

## Tests

| Path | Coverage |
|------|----------|
| `server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardSkillXpGrantTests.cs` | **Granted:** helper applies XP when `mission_reward` ∈ `allowedXpSourceKinds` for chosen `skillId`; optional round-trip **`GET …/skill-progression`**. **Denied:** stub registry without `mission_reward` on target skill → no XP delta; no throw. |
| Existing `SkillProgressionGrantApiTests.cs` | Regression only if POST path or shared types change during implementation (likely unchanged). |

## Open questions / risks

None. **Risk:** Low — additive surface; helper **unreferenced** until E7.M2 (same pattern as NEO-42).
