# NEO-36 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-36 | | **Title** | E2.M1: Read-only skill catalog HTTP projection + Bruno | | **Linear** | https://linear.app/neon-sprawl/issue/NEO-36/e2m1-read-only-skill-catalog-http-projection-bruno | | **Module** | [E2.M1 — SkillDefinitionRegistry](../decomposition/modules/E2_M1_SkillDefinitionRegistry.md) | ## Kickoff clarifications No questions were put to the user during kickoff. **Reason:** The issue specifies the response fields (`id`, `displayName`, `category`, `allowedXpSourceKinds`), excludes mutations and client polish, and points at “match server routing style.” The repo already exposes a read-only world projection at `GET /game/world/interactables` (`InteractablesWorldApi`) with a versioned JSON envelope—this story can mirror that pattern. **`ISkillDefinitionRegistry`** and the frozen prototype trio (`salvage`, `refine`, `intrusion`) are present on `main` from **NEO-33–NEO-35** (`prototype_skills.json`, `SkillDefinitionRegistryTests`). Linear may still list **blockedBy NEO-35**; the implementation branch is based on **`main`** where that work is already merged. **If you want a different public path** than the default in Technical approach (below), say so before implementation. ## Goal, scope, and out-of-scope **Goal:** Expose a stable, read-only JSON endpoint that returns all loaded prototype skill definitions for dev tooling, Bruno, and manual QA, backed by **`ISkillDefinitionRegistry`** without duplicating catalog truth outside the server. **In scope (from Linear):** - One GET route consistent with existing APIs (under `/game/`, same style as other read-only projections). - JSON rows including **`id`**, **`displayName`**, **`category`**, **`allowedXpSourceKinds`** per skill. - Bruno request(s) under `bruno/neon-sprawl-server/`, mirroring existing collections (e.g. `interaction/Get interactables list.bru`). - `docs/manual-qa/NEO-36.md` checklist (endpoint is user-visible for QA per repo rules). **Out of scope (from Linear):** - Mutations or authoring APIs. - Client UX / debug panel (optional and not required). ## Acceptance criteria checklist - [x] Bruno (and/or automated test) can call the endpoint against a running dev server and observe the **three frozen ids** (`salvage`, `refine`, `intrusion`). - [x] `docs/manual-qa/NEO-36.md` exists with a short checklist for hitting the endpoint. ## Technical approach 1. **Route:** Add **`GET /game/world/skill-definitions`** (parallel to **`/game/world/interactables`**) as a parameterless read. If “world” reads odd for catalog-only data, acceptable alternative is **`GET /game/content/skill-definitions`**—pick one in implementation and keep Bruno/tests aligned; default **`/game/world/skill-definitions`** for consistency with the other static registry projection. 2. **Response shape:** Follow the interactables pattern: top-level **`schemaVersion`** (const `1` for first ship of this contract) plus an array (e.g. **`skills`**) of objects with **`id`**, **`displayName`**, **`category`**, **`allowedXpSourceKinds`**. Order skills by **`id`** ordinal to match **`ISkillDefinitionRegistry.GetDefinitionsInIdOrder()`**. 3. **Implementation:** New static **`SkillDefinitionsWorldApi`** (or **`SkillCatalogApi`**) in `Game/Skills/` that injects **`ISkillDefinitionRegistry`**, maps rows to DTOs (either anonymous projection types or small sealed DTO types in a `*Dtos.cs` file next to the API), returns **`Results.Json`**. Wire **`app.MapSkillDefinitionsWorldApi()`** (name TBD) from **`Program.cs`** next to other `Map*Api` calls. 4. **Bruno:** New folder `bruno/neon-sprawl-server/skill-definitions/` with **`Get skill definitions.bru`** (and optional **`folder.bru`** if the collection uses per-folder metadata like `ability-cast`). Reuse **`{{baseUrl}}`** from environments; tests assert 200, JSON, schema version, array length ≥ 3, and presence of the three frozen ids (mirror style of **`Get interactables list.bru`**). 5. **Automated tests:** Add **`SkillDefinitionsWorldApiTests`** (or equivalent) using **`InMemoryWebApplicationFactory`**, **`HttpClient.GetAsync`**, **`ReadFromJsonAsync`** on the response DTO—same structure as **`InteractablesWorldApiTests`**, asserting the trio and ordering. ## Files to add | Path | Purpose | |------|---------| | `server/NeonSprawl.Server/Game/Skills/SkillDefinitionsWorldApi.cs` (or chosen name) | `Map*` extension registering GET route; maps registry → JSON. | | `server/NeonSprawl.Server/Game/Skills/SkillDefinitionsListDtos.cs` (or inline records in API file if kept tiny) | Versioned response + row DTOs for JSON serialization. | | `server/NeonSprawl.Server.Tests/Game/Skills/SkillDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema, three ids, id order. | | `bruno/neon-sprawl-server/skill-definitions/Get skill definitions.bru` | Manual / Bruno verification against dev server. | | `bruno/neon-sprawl-server/skill-definitions/folder.bru` | Optional; match Bruno folder conventions used elsewhere. | | `docs/manual-qa/NEO-36.md` | Checklist: start server, call GET, confirm trio (and note Bruno path). | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Program.cs` | Register the new `Map*Api()` alongside existing game APIs. | ## Tests | Test file | What it covers | |-----------|------------------| | `server/NeonSprawl.Server.Tests/Game/Skills/SkillDefinitionsWorldApiTests.cs` | **Integration:** `GET` new route via **`InMemoryWebApplicationFactory`**; **`ReadFromJsonAsync`**; assert **`schemaVersion`**, ordered **`id`** list equals **`intrusion`**, **`refine`**, **`salvage`** (ordinal sort); spot-check one row’s **`allowedXpSourceKinds`**. **AAA** comments per [csharp-style](../../.cursor/rules/csharp-style.md#unit-and-integration-tests-arrange-act-assert). | Bruno scripts are manual verification; they complement but do not replace the automated test above. ## Open questions / risks - **Linear relation:** Issue may still show **blockedBy NEO-35**; code on **`main`** already includes the registry. If **`main`** lags a fork, rebase onto **`main`** after NEO-35 merge. - **Path naming:** If product prefers **`/content/`** over **`/world/`**, swap in one place (route + Bruno + tests) before shipping.