# NEO-42 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-42 |
| **Title** | Craft / refine awards skill XP (sourceKind: activity) |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-42/craft-refine-awards-skill-xp-sourcekind-activity |
| **Labels** | E3.M2, E2.M2, server |
| **Blocked by (Linear)** | [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) (grant path — **done** on `main`); **E3.M2** recipe/refine execution (not present in repo at kickoff). |

## Kickoff clarifications

| Topic | Question / note | Resolution |
|--------|-----------------|------------|
| **Delivery shape** | User asked what to recommend until E3.M2 HTTP/recipe loop exists. | **Agent recommendation (accepted unless you object in chat):** add **`RefineSkillXpConstants`** + a thin **`RefineActivitySkillXpGrant`** (or equivalent) that delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** — **no** new public craft HTTP in NEO-42. E3.M2 calls this helper from the real **`CraftResult`** success path when that module lands. |
| **Prototype XP amount** | User asked what to recommend. | **Agent recommendation:** **10** XP per successful craft/refine completion for the prototype, matching **[NEO-41](NEO-41-implementation-plan.md)** gather constant pattern; replace with recipe-driven amounts in a later issue when content exists. |
| **“Craft” vs “refine” skill id** | `content/skills/prototype_skills.json` defines **`refine`** (process) with **`activity`** in `allowedXpSourceKinds`; there is no separate **`craft`** skill row today. | **Prototype mapping:** both **recipe craft** and **refine** success paths call the helper with **`skillId: refine`** and **`sourceKind: activity`** so allowlist enforcement matches catalog. If a dedicated craft skill is added later, E3.M2 can call the same helper with a different `skillId` per recipe metadata. |

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

**Goal:** When **E3.M2** reports a **successful** craft or refine completion, award **refine** skill XP through the **NEO-38** grant stack (`TryApplyGrant` → registry allowlist → store → level curve) with **`sourceKind: activity`** wherever **`activity`** is listed on the target skill’s **`allowedXpSourceKinds`**.

**In scope (from Linear):**

- Wire (or **prepare the single call site** for) craft/refine completion → **`SkillProgressionGrantOperations.TryApplyGrant`** with **`activity`**.
- **Respect catalog allowlists** (no grant when `activity` is disallowed — mirror NEO-41 defensive tests).

**Out of scope:**

- Owning the full **E3.M2** recipe validation, inventory mutations, or new public craft routes (separate story/module).
- **Recipe-driven XP amounts** and multi-skill mapping from **`RecipeDef`** (follow-up once execution + content exist).
- **Telemetry** (`NEO-40`) — comment-only hook sites already noted on grant path.

## Acceptance criteria checklist

- [x] **Preparatory:** **`RefineActivitySkillXpGrant`** invokes the **NEO-38** path (**`TryApplyGrant`**, same stack as **`POST …/skill-progression`**). **Follow-up:** **E3.M2** success handler must call **`GrantOnSuccessfulCraftOrRefine`** (documented in **`server/README.md`** + manual QA); no craft route in repo yet.
- [x] Grant channels respect **skill catalog** `allowedXpSourceKinds` (stub registry test: **`refine`** without **`activity`** → no XP).

## Decisions (implementation)

- **`RefineActivityDeniedRegistryWebApplicationFactory`** added for allowlist-deny coverage (parallel to NEO-41 salvage-denied factory).

## Technical approach

1. **Constants** — Add `RefineSkillXpConstants` (parallel to `GatherSkillXpConstants`): **`refine`** skill id, **`activity`** source kind, **10** XP per completion (name clearly scoped to prototype craft/refine success).

2. **Shared grant helper** — Add a small static type (e.g. `RefineActivitySkillXpGrant`) whose sole job is to invoke **`SkillProgressionGrantOperations.TryApplyGrant`** with those constants. **Discard** the outcome on success paths intended to mirror gather (UI reads **`GET …/skill-progression`**); callers that need structured denies can call **`TryApplyGrant`** directly later.

3. **E3.M2 integration** — When **`CraftResult`** (or equivalent) success is finalized in server code, add **one call** to the helper with `playerId` + injected `ISkillDefinitionRegistry`, `IPlayerSkillProgressionStore`, `ISkillLevelCurve`. If E3.M2 is still absent when NEO-42 implements (1)–(2), document the **exact line / type** in this plan’s **Decisions** after the merge that adds the success handler.

4. **Tests** — (a) Happy path: grant applies **refine** XP with **`activity`** when catalog allows it. (b) Deny path: stub registry where **`refine`** disallows **`activity`** → helper run leaves **0** XP (same spirit as `SalvageActivityDeniedRegistryWebApplicationFactory` in NEO-41). Use **in-memory** store + real or test JSON catalog + placeholder level curve; **no** dependency on craft HTTP.

5. **Manual QA** — If only (1)–(2) ship before E3.M2: **`docs/manual-qa/NEO-42.md`** can note “verify via unit tests + temporary `POST …/skill-progression` control grant for `refine`/`activity`”; once E3.M2 exists, extend checklist to **Bruno** craft success + **GET** progression.

## Files to add

| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Skills/RefineSkillXpConstants.cs` | NEO-42 prototype constants: `refine`, `activity`, flat XP amount. |
| `server/NeonSprawl.Server/Game/Skills/RefineActivitySkillXpGrant.cs` | Single entry point calling `TryApplyGrant` for successful craft/refine (prototype). |
| `server/NeonSprawl.Server.Tests/Game/Skills/RefineActivitySkillXpGrantTests.cs` | AAA tests: grant when allowed; no XP when `activity` not in allowlist for `refine`. |
| `server/NeonSprawl.Server.Tests/Game/Skills/RefineActivityDeniedRegistryWebApplicationFactory.cs` | Test host with **`refine`** catalog row that omits **`activity`**. |
| `docs/manual-qa/NEO-42.md` | Curl checklist + E3.M2 follow-up note. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `server/README.md` | Document NEO-42 hook type and E3.M2 integration expectation. |
| **E3.M2 craft success handler** (path TBD) | Call **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** once on successful craft/refine — **not done in this PR** (module absent). |

## Tests

| Test file | What it covers |
|-----------|------------------|
| `RefineActivitySkillXpGrantTests.cs` | **Arrange / Act / Assert:** successful grant increases **`refine`** XP via shared path; registry without **`activity`** for **`refine`** yields **no** XP delta (allowlist respected). |

## Open questions / risks

- **Merge ordering:** If NEO-42 merges **before** E3.M2, acceptance checkbox “craft/refine completion” is only fully satisfied after a tiny follow-up commit adds the call in the new success handler — track in Linear or a child issue if needed.
- **XP balance:** Flat **10** is a placeholder; design may change before public playtests.
