# NEO-60 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-60 |
| **Title** | E3.M1: GET world resource-node-definitions + Bruno |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-60/e3m1-get-world-resource-node-definitions-bruno |
| **Module** | [E3.M1 — ResourceNodeAndGatherLoop](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) · Epic 3 Slice 2 · backlog **E3M1-04** |
| **Branch** | `NEO-60-e3m1-get-world-resource-node-definitions-bruno` |
| **Precursor** | [NEO-59](https://linear.app/neon-sprawl/issue/NEO-59) — `IResourceNodeDefinitionRegistry` + DI (**Done** on `main`) |
| **Pattern** | [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) — `GET /game/world/item-definitions` |

## Kickoff clarifications

| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Yield projection** | Nested `yield` object vs flat `itemId`/`quantity` on the row? | **Nested `yield: { itemId, quantity }`** — mirrors catalog split (node def + yield table) and matches NEO-59 registry shape; room for multi-yield later. | **User:** nested yield object. |
| **Row id JSON name** | `id` vs `nodeDefId`? | **`id`** — same HTTP convention as item/skill definition endpoints (NEO-53 / NEO-36). | **User:** `id`. |
| **Manual QA doc** | Add `docs/manual-qa/NEO-60.md`? | **Yes** — user-visible HTTP surface; mirror NEO-53. | **User:** yes. |

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

**Goal:** Expose a stable, read-only JSON endpoint that returns all loaded prototype resource-node definitions (with yield summary) for dev tooling, Bruno, manual QA, and future client/codegen, backed by **`IResourceNodeDefinitionRegistry`** without duplicating catalog truth outside the server.

**In scope (from Linear + [E3M1-04](E3M1-prototype-backlog.md#e3m1-04--get-world-resource-node-definitions)):**

- **`GET /game/world/resource-node-definitions`** with versioned envelope (`schemaVersion` **1**, **`nodes`** array).
- Each row: **`id`**, **`displayName`**, **`gatherLens`**, **`maxGathers`**, nested **`yield`** with **`itemId`** and **`quantity`** (camelCase JSON).
- Nodes ordered by **`id`** ordinal, matching **`GetDefinitionsInIdOrder()`**.
- Bruno folder `bruno/neon-sprawl-server/resource-node-definitions/`.
- `docs/manual-qa/NEO-60.md`.
- `server/README.md` section.
- API integration tests (AAA).

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

- Per-instance remaining capacity / depletion state ([NEO-61](https://linear.app/neon-sprawl/issue/NEO-61)).
- Gather engine, inventory mutation, interact wiring ([NEO-62+](E3M1-prototype-backlog.md)).
- Client HUD.

## Acceptance criteria checklist

- [x] GET returns all **four** prototype defs with **`schemaVersion`** **1** and yield summary fields needed for QA.
- [x] Bruno happy path documented and runnable against dev server.
- [x] Automated API tests (AAA) assert four ids in order and spot-check node + yield metadata.

## Technical approach

1. **Route:** **`GET /game/world/resource-node-definitions`** — parameterless read; inject **`IResourceNodeDefinitionRegistry`** only (not `ResourceNodeCatalog`).

2. **Response shape:** Top-level **`schemaVersion`** (`ResourceNodeDefinitionsListResponse.CurrentSchemaVersion` = **1**) plus **`nodes`** array. Each row maps from **`ResourceNodeDefRow`** + joined **`ResourceYieldRow`** via **`TryGetYield`**: **`id`** ← `NodeDefId`, **`displayName`**, **`gatherLens`**, **`maxGathers`**, **`yield`** ← `{ itemId, quantity }`. Prototype catalog guarantees one yield per node at load time; if **`TryGetYield`** fails for an enumerated def, treat as programmer error (should not occur post-NEO-58 validation).

3. **Ordering:** Build list from **`registry.GetDefinitionsInIdOrder()`**. Tests assert exact id sequence (ordinal):

   `prototype_bio_mat_gamma`, `prototype_resource_node_alpha`, `prototype_subsurface_vein_beta`, `prototype_urban_bulk_delta`.

4. **Implementation:** New **`ResourceNodeDefinitionsWorldApi`** + **`ResourceNodeDefinitionsListDtos.cs`** in `Game/Gathering/` (mirror [`ItemDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Items/ItemDefinitionsWorldApi.cs)). Wire **`app.MapResourceNodeDefinitionsWorldApi()`** from **`Program.cs`** next to **`MapItemDefinitionsWorldApi()`**.

5. **Bruno:** `bruno/neon-sprawl-server/resource-node-definitions/` with **`Get resource node definitions.bru`** + **`folder.bru`**. Tests: 200, JSON, **`schemaVersion` 1**, **`nodes.length === 4`**, ascending id order, frozen four ids in registry order, spot-check **`prototype_resource_node_alpha`** (`gatherLens` `consumer_salvage`, `maxGathers` 10, `yield.itemId` `scrap_metal_bulk`, `yield.quantity` 1) and **`prototype_urban_bulk_delta`** (`yield.quantity` 5).

6. **Docs:** `docs/manual-qa/NEO-60.md` (mirror NEO-53). Update **`server/README.md`** resource-node section with GET + curl. Update [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md) **Related implementation slices** and [documentation_and_implementation_alignment.md](documentation_and_implementation_alignment.md) E3.M1 row when landed. Flip E3M1-04 acceptance checkboxes in [E3M1-prototype-backlog.md](E3M1-prototype-backlog.md).

## Files to add

| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs` | `Map*` extension registering GET route; maps registry → JSON (node + nested yield). |
| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsListDtos.cs` | Versioned response + row/yield DTOs for JSON serialization. |
| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema v1, four ids in order, spot-check rows + yields. |
| `bruno/neon-sprawl-server/resource-node-definitions/Get resource node definitions.bru` | Manual / Bruno verification against dev server. |
| `bruno/neon-sprawl-server/resource-node-definitions/folder.bru` | Bruno folder metadata (match `item-definitions`). |
| `docs/manual-qa/NEO-60.md` | Checklist: start server, curl GET, confirm four ids + spot-check yield. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Register `MapResourceNodeDefinitionsWorldApi()` alongside other game world APIs. |
| `server/README.md` | Document `GET /game/world/resource-node-definitions`, curl example, links to plan/manual QA/Bruno. |
| `docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md` | **Related implementation slices** — HTTP read model bullet (NEO-60). |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M1 row — note NEO-60 HTTP projection when landed. |
| `docs/plans/E3M1-prototype-backlog.md` | E3M1-04 acceptance checkboxes when landed. |

## Tests

| Test file | What it covers |
|-----------|----------------|
| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs` | **Integration:** `GET /game/world/resource-node-definitions` via **`InMemoryWebApplicationFactory`**; **`ReadFromJsonAsync`**; assert **`schemaVersion` 1**; **`nodes`** count **4**; ordered **`id`** list equals frozen four (ordinal); spot-check **`prototype_resource_node_alpha`** (`displayName` “Prototype Salvage Heap”, `gatherLens` `consumer_salvage`, `maxGathers` 10, `yield.itemId` `scrap_metal_bulk`, `yield.quantity` 1) and **`prototype_urban_bulk_delta`** (`gatherLens` `urban_bulk`, `yield.quantity` 5). **AAA** per [csharp-style](../../.cursor/rules/csharp-style.md). |

Bruno scripts complement automated tests for manual dev-server verification.

## Open questions / risks

| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Linear blockedBy NEO-59** | Branch from **`main`** where NEO-59 is merged (confirmed at kickoff). | **adopted** |
| **Missing yield at map time** | Rely on NEO-58 load parity (every node has exactly one yield row); no silent omission in HTTP mapper. | **adopted** |
| **Instance depletion in response** | Omit remaining gathers — instance state is NEO-61; this endpoint is definition catalog only. | **adopted** |

None blocking.

## Decisions (kickoff)

- **Nested `yield` object** — `{ itemId, quantity }` per row; joined from registry’s separate TryGet methods.
- **Row `id` JSON name** — consistent with item/skill world definition endpoints.
- **`nodes` top-level array** — parallel to `items` / `skills` naming on sibling routes.
- **Manual QA doc included** — `docs/manual-qa/NEO-60.md`.
