diff --git a/bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru b/bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru
new file mode 100644
index 0000000..6a48dfb
--- /dev/null
+++ b/bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru
@@ -0,0 +1,81 @@
+meta {
+ name: GET recipe definitions
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/game/world/recipe-definitions
+ body: none
+ auth: none
+}
+
+tests {
+ test("returns 200 JSON with schema v1 and recipes array", function () {
+ expect(res.getStatus()).to.equal(200);
+ expect(res.getHeader("content-type")).to.contain("application/json");
+ const body = res.getBody();
+ expect(body.schemaVersion).to.equal(1);
+ expect(body.recipes).to.be.an("array");
+ expect(body.recipes.length).to.equal(8);
+ });
+
+ test("recipes are ascending by id (ordinal)", function () {
+ const body = res.getBody();
+ const ids = body.recipes.map((x) => x.id);
+ // Match server StringComparer.Ordinal (prototype ids are ASCII snake_case).
+ const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
+ expect(ids).to.eql(sorted);
+ });
+
+ test("frozen prototype eight matches registry id order", function () {
+ const body = res.getBody();
+ const ids = body.recipes.map((x) => x.id);
+ expect(ids).to.eql([
+ "make_armor_quick",
+ "make_contract_token",
+ "make_field_stim_batch",
+ "make_field_stim_mk0",
+ "make_prototype_armor",
+ "make_survey_drone_kit",
+ "refine_scrap_efficient",
+ "refine_scrap_standard",
+ ]);
+ });
+
+ test("frozen prototype eight is present", function () {
+ const body = res.getBody();
+ const ids = new Set(body.recipes.map((x) => x.id));
+ expect(ids.has("make_armor_quick")).to.equal(true);
+ expect(ids.has("make_contract_token")).to.equal(true);
+ expect(ids.has("make_field_stim_batch")).to.equal(true);
+ expect(ids.has("make_field_stim_mk0")).to.equal(true);
+ expect(ids.has("make_prototype_armor")).to.equal(true);
+ expect(ids.has("make_survey_drone_kit")).to.equal(true);
+ expect(ids.has("refine_scrap_efficient")).to.equal(true);
+ expect(ids.has("refine_scrap_standard")).to.equal(true);
+ });
+
+ test("refine_scrap_standard row matches catalog", function () {
+ const body = res.getBody();
+ const row = body.recipes.find((x) => x.id === "refine_scrap_standard");
+ expect(row).to.be.an("object");
+ expect(row.displayName).to.equal("Refine Scrap (Standard)");
+ expect(row.recipeKind).to.equal("process");
+ expect(row.requiredSkillId).to.equal("refine");
+ expect(row.inputs).to.eql([{ itemId: "scrap_metal_bulk", quantity: 5 }]);
+ expect(row.outputs).to.eql([{ itemId: "refined_plate_stock", quantity: 1 }]);
+ });
+
+ test("make_field_stim_mk0 row matches catalog", function () {
+ const body = res.getBody();
+ const row = body.recipes.find((x) => x.id === "make_field_stim_mk0");
+ expect(row).to.be.an("object");
+ expect(row.recipeKind).to.equal("make");
+ expect(row.inputs).to.eql([
+ { itemId: "refined_plate_stock", quantity: 2 },
+ { itemId: "scrap_metal_bulk", quantity: 1 },
+ ]);
+ expect(row.outputs).to.eql([{ itemId: "field_stim_mk0", quantity: 1 }]);
+ });
+}
diff --git a/bruno/neon-sprawl-server/recipe-definitions/folder.bru b/bruno/neon-sprawl-server/recipe-definitions/folder.bru
new file mode 100644
index 0000000..73f8c35
--- /dev/null
+++ b/bruno/neon-sprawl-server/recipe-definitions/folder.bru
@@ -0,0 +1,3 @@
+meta {
+ name: recipe-definitions
+}
diff --git a/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md b/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md
index 100e260..f0575fd 100644
--- a/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md
+++ b/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md
@@ -13,7 +13,7 @@
**Prep (NEO-42):** Server ships **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** + **`RefineSkillXpConstants`** under `server/NeonSprawl.Server/Game/Skills/` — delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** with **`refine`** + **`activity`** (prototype **10** XP), same persistence and **`allowedXpSourceKinds`** rules as **`POST …/skill-progression`**. **No** `CraftRequest` / **`CraftResult`** route yet; when this module adds authoritative craft/refine success, call that helper once on success ([NEO-42 implementation plan](../../plans/NEO-42-implementation-plan.md); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42)).
-**E3M2-01 (NEO-65):** Frozen eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65 plan](../../plans/NEO-65-implementation-plan.md)). **E3M2-02 (NEO-66):** fail-fast server load under `server/NeonSprawl.Server/Game/Crafting/` — `RecipeDefinitionCatalogLoader`, `RecipeDefinitionCatalog`, cross-check vs item + skill catalogs, Slice 3 gate; [NEO-66 plan](../../plans/NEO-66-implementation-plan.md); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **E3M2-03 (NEO-67):** injectable **`IRecipeDefinitionRegistry`** + DI; [NEO-67 plan](../../plans/NEO-67-implementation-plan.md).
+**E3M2-01 (NEO-65):** Frozen eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65 plan](../../plans/NEO-65-implementation-plan.md)). **E3M2-02 (NEO-66):** fail-fast server load under `server/NeonSprawl.Server/Game/Crafting/` — `RecipeDefinitionCatalogLoader`, `RecipeDefinitionCatalog`, cross-check vs item + skill catalogs, Slice 3 gate; [NEO-66 plan](../../plans/NEO-66-implementation-plan.md); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **E3M2-03 (NEO-67):** injectable **`IRecipeDefinitionRegistry`** + DI; [NEO-67 plan](../../plans/NEO-67-implementation-plan.md). **E3M2-04 (NEO-68):** **`GET /game/world/recipe-definitions`** — versioned read-only projection (`schemaVersion` **1**, **`recipes`**) backed by **`IRecipeDefinitionRegistry`**; Bruno `bruno/neon-sprawl-server/recipe-definitions/`. Plan: [NEO-68 implementation plan](../../plans/NEO-68-implementation-plan.md).
## Purpose
diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md
index d0af413..c10c93f 100644
--- a/docs/decomposition/modules/documentation_and_implementation_alignment.md
+++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md
@@ -55,7 +55,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
| E2.M2 | In Progress | **NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([NEO-37](../../plans/NEO-37-implementation-plan.md)) — read model for every registered skill; known-player gate via `IPositionStateStore`; [server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37); manual QA [`NEO-37`](../../manual-qa/NEO-37.md). **NEO-38 landed:** **`POST`** same path — grant apply, persistence (`IPlayerSkillProgressionStore`, `V003` migration), structured denies + **`levelUps`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); manual QA [`NEO-38`](../../manual-qa/NEO-38.md); Bruno `bruno/neon-sprawl-server/skill-progression/`; [server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38). **NEO-39 landed:** data-driven `LevelCurve` content + schema + CI validation (`*_level_curve.json`) with fail-fast startup schema checks; progression GET/POST level resolution now uses `ISkillLevelCurve` content-backed thresholds ([NEO-39](../../plans/NEO-39-implementation-plan.md)); manual QA [`NEO-39`](../../manual-qa/NEO-39.md). **NEO-40 landed:** comment-only telemetry hook sites in [`SkillProgressionSnapshotApi.cs`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs) on **`POST …/skill-progression`** for future **`xp_grant`** / **`level_up`** ([NEO-40](../../plans/NEO-40-implementation-plan.md), [`NEO-40` manual QA](../../manual-qa/NEO-40.md)). **NEO-41 landed:** gather prototype — **`POST …/interact`** with **`kind: resource_node`** grants **`salvage`** + **`activity`** (10 XP) via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) ([NEO-41](../../plans/NEO-41-implementation-plan.md), [`NEO-41` manual QA](../../manual-qa/NEO-41.md)); [server README — Interaction](../../../server/README.md#interaction-neo-9). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack; **E3.M2** must invoke on craft/refine success ([NEO-42](../../plans/NEO-42-implementation-plan.md), [`NEO-42` manual QA](../../manual-qa/NEO-42.md)); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42). **NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** / **`MissionRewardSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack with fixed **`sourceKind: mission_reward`**; **E7.M2** must invoke from quest hand-in ([NEO-43](../../plans/NEO-43-implementation-plan.md), [`NEO-43` manual QA](../../manual-qa/NEO-43.md)); [server README — Mission / quest reward (NEO-43)](../../../server/README.md#mission--quest-reward--skill-xp-neo-43). **Slice 3 still open:** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [NEO-37](../../plans/NEO-37-implementation-plan.md), [NEO-38](../../plans/NEO-38-implementation-plan.md), [NEO-39](../../plans/NEO-39-implementation-plan.md), [NEO-40](../../plans/NEO-40-implementation-plan.md), [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-43](../../plans/NEO-43-implementation-plan.md), [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37–NEO-41, NEO-42–NEO-44 |
| E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **Slice 5 client (planned):** gather feedback HUD [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/` |
| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. **Slice 5 client (planned):** inventory HUD [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
-| E3.M2 | In Progress | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-67 landed:** injectable **`IRecipeDefinitionRegistry`** + lookup tests ([NEO-67](../../plans/NEO-67-implementation-plan.md)). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** — craft/refine success should award **`refine`** XP via shared NEO-38 grant path; no craft route yet. **Slice 3 backlog in Linear:** [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md) — **E3M2-04**–**E3M2-07** open. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **Slice 5 client (planned):** craft UI [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74); capstone playable loop [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](https://linear.app/neon-sprawl/issue/NEO-67) … [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) |
+| E3.M2 | In Progress | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-67 landed:** injectable **`IRecipeDefinitionRegistry`** + lookup tests ([NEO-67](../../plans/NEO-67-implementation-plan.md)). **NEO-68 landed:** **`GET /game/world/recipe-definitions`** — `RecipeDefinitionsWorldApi` + DTOs in `Game/Crafting/` ([NEO-68](../../plans/NEO-68-implementation-plan.md), [`NEO-68` manual QA](../../manual-qa/NEO-68.md)); [server README — Recipe definitions (NEO-68)](../../../server/README.md#recipe-definitions-neo-68); Bruno `bruno/neon-sprawl-server/recipe-definitions/`. **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** — craft/refine success should award **`refine`** XP via shared NEO-38 grant path; no craft route yet. **Slice 3 backlog in Linear:** [NEO-69](https://linear.app/neon-sprawl/issue/NEO-69) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md) — **E3M2-05**–**E3M2-07** open. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **Slice 5 client (planned):** craft UI [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74); capstone playable loop [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](../../plans/NEO-67-implementation-plan.md), [NEO-68](../../plans/NEO-68-implementation-plan.md) … [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) |
---
diff --git a/docs/manual-qa/NEO-68.md b/docs/manual-qa/NEO-68.md
new file mode 100644
index 0000000..d036b35
--- /dev/null
+++ b/docs/manual-qa/NEO-68.md
@@ -0,0 +1,32 @@
+# NEO-68 — Manual QA checklist
+
+| Field | Value |
+|-------|-------|
+| Key | NEO-68 |
+| Title | E3.M2: GET world recipe-definitions + Bruno |
+| Linear | https://linear.app/neon-sprawl/issue/NEO-68/e3m2-get-world-recipe-definitions-bruno |
+| Plan | `docs/plans/NEO-68-implementation-plan.md` |
+| Branch | `NEO-68-get-world-recipe-definitions-bruno` |
+
+## Preconditions
+
+- Server built and configured with default content paths pointing at repo `content/recipes`, `content/items`, and `content/skills` (local dev / `InMemoryWebApplicationFactory` tests use the same layout).
+
+## Checklist
+
+1. Start **`NeonSprawl.Server`** (e.g. `dotnet run` from `server/NeonSprawl.Server`).
+2. **`GET /game/world/recipe-definitions`** — expect **200** and **`Content-Type`** containing **`application/json`**. Example (default dev URL from `Properties/launchSettings.json` and Bruno `environments/Local.bru`; change the host/port if yours differs):
+
+ ```bash
+ curl -sS -i "http://localhost:5253/game/world/recipe-definitions"
+ ```
+
+3. Parse JSON — expect **`schemaVersion`** === **1**, **`recipes`** array length **8**.
+4. Confirm **`id`** values match the frozen prototype eight in **registry id order** (ordinal):
+
+ **`make_armor_quick`**, **`make_contract_token`**, **`make_field_stim_batch`**, **`make_field_stim_mk0`**, **`make_prototype_armor`**, **`make_survey_drone_kit`**, **`refine_scrap_efficient`**, **`refine_scrap_standard`**
+
+ (same sequence as `RecipeDefinitionsWorldApiTests.FrozenEightInIdOrder`).
+5. Spot-check **`refine_scrap_standard`**: **`displayName`** “Refine Scrap (Standard)”, **`recipeKind`** **`process`**, **`requiredSkillId`** **`refine`**, **`inputs`** `[{ itemId: scrap_metal_bulk, quantity: 5 }]`, **`outputs`** `[{ itemId: refined_plate_stock, quantity: 1 }]`.
+6. Spot-check **`make_field_stim_mk0`**: **`recipeKind`** **`make`**, two **`inputs`** rows (`refined_plate_stock` × 2, `scrap_metal_bulk` × 1), **`outputs`** `[{ itemId: field_stim_mk0, quantity: 1 }]`.
+7. Optional: run **`bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru`** against the same **`baseUrl`** (see `environments/Local.bru`).
diff --git a/docs/plans/E3M2-prototype-backlog.md b/docs/plans/E3M2-prototype-backlog.md
index dfecd2a..6289120 100644
--- a/docs/plans/E3M2-prototype-backlog.md
+++ b/docs/plans/E3M2-prototype-backlog.md
@@ -149,8 +149,10 @@ Working backlog for **Epic 3 — Slice 3** ([recipes and crafting pipeline](../d
**Acceptance criteria**
-- [ ] GET returns all eight prototype recipes with schema version field and input/output summary fields needed for QA.
-- [ ] Bruno happy path documented.
+- [x] GET returns all eight prototype recipes with schema version field and input/output summary fields needed for QA.
+- [x] Bruno happy path documented.
+
+**Landed ([NEO-68](https://linear.app/neon-sprawl/issue/NEO-68)):** `GET /game/world/recipe-definitions` — `RecipeDefinitionsWorldApi` + DTOs in `server/NeonSprawl.Server/Game/Crafting/`; plan [NEO-68-implementation-plan.md](NEO-68-implementation-plan.md); manual QA [`NEO-68`](../manual-qa/NEO-68.md); Bruno `bruno/neon-sprawl-server/recipe-definitions/`.
---
diff --git a/docs/plans/NEO-68-implementation-plan.md b/docs/plans/NEO-68-implementation-plan.md
new file mode 100644
index 0000000..dae0f14
--- /dev/null
+++ b/docs/plans/NEO-68-implementation-plan.md
@@ -0,0 +1,113 @@
+# NEO-68 — Implementation plan
+
+## Story reference
+
+| Field | Value |
+|--------|--------|
+| **Key** | NEO-68 |
+| **Title** | E3.M2: GET world recipe-definitions + Bruno |
+| **Linear** | https://linear.app/neon-sprawl/issue/NEO-68/e3m2-get-world-recipe-definitions-bruno |
+| **Module** | [E3.M2 — RefinementAndRecipeExecution](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) · Epic 3 Slice 3 · backlog **E3M2-04** |
+| **Branch** | `NEO-68-get-world-recipe-definitions-bruno` |
+| **Precursor** | [NEO-67](https://linear.app/neon-sprawl/issue/NEO-67) — `IRecipeDefinitionRegistry` + DI (**Done** on `main`) |
+| **Pattern** | [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) — `GET /game/world/item-definitions` |
+| **Client counterpart** | [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) — E3S5-03 craft UI + recipe list (blocked by this issue; server-only in this story) |
+| **Blocks** | [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74), [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) (Linear relations) |
+
+## Kickoff clarifications
+
+| Topic | Question | Agent recommendation | Answer |
+|--------|----------|----------------------|--------|
+| **JSON row fields** | Expose required six + I/O only vs optional nullables? | **Required six + `inputs`/`outputs` only** — prototype rows omit `requiredSkillLevel` / `stationTag`; mirror NEO-53 “loaded fields only”. | **User:** required six + I/O only. |
+| **Response envelope** | Array property name? | **`recipes`** (with `schemaVersion` **1**) — parallel to NEO-36 `skills` / NEO-53 `items`; matches content vocabulary. | **Adopted** — no objection; follows precedent. |
+| **Route path** | `/game/world/…` vs `/game/content/…`? | **`GET /game/world/recipe-definitions`** — E3M2 backlog + parallel to item/skill defs. | **Adopted** — backlog. |
+| **Manual QA doc** | Add `docs/manual-qa/NEO-68.md`? | **Yes** — player-visible HTTP; NEO-53 pattern. | **User:** yes. |
+
+## Goal, scope, and out-of-scope
+
+**Goal:** Expose a stable, read-only JSON endpoint that returns all eight loaded prototype recipe definitions for Bruno, manual QA, client preview ([NEO-74](https://linear.app/neon-sprawl/issue/NEO-74)), and tooling — backed by **`IRecipeDefinitionRegistry`** without duplicating catalog truth.
+
+**In scope (from Linear + [E3M2-04](E3M2-prototype-backlog.md#e3m2-04--get-world-recipe-definitions)):**
+
+- **`GET /game/world/recipe-definitions`** with versioned envelope (`schemaVersion` **1**, **`recipes`** array).
+- Row fields: **`id`**, **`displayName`**, **`recipeKind`**, **`requiredSkillId`**, **`inputs`**, **`outputs`** (I/O rows: **`itemId`**, **`quantity`**; camelCase JSON).
+- Recipes ordered by **`id`** ordinal, matching **`GetDefinitionsInIdOrder()`**.
+- Bruno folder `bruno/neon-sprawl-server/recipe-definitions/`.
+- `docs/manual-qa/NEO-68.md`.
+- `server/README.md` section.
+- API integration tests (AAA).
+
+**Out of scope (from Linear + backlog):**
+
+- Per-player craft history or cooldown state.
+- Optional schema fields (`requiredSkillLevel`, `stationTag`) until content ships them.
+- Craft execution ([NEO-69](https://linear.app/neon-sprawl/issue/NEO-69)).
+- Godot client ([NEO-74](https://linear.app/neon-sprawl/issue/NEO-74)).
+
+## Acceptance criteria checklist
+
+- [x] GET returns all **eight** prototype recipes with **`schemaVersion`** **1** and input/output summary fields needed for QA.
+- [x] Bruno happy path documented and runnable against dev server.
+- [x] Automated API tests (AAA) assert eight ids, id order, and spot-check metadata + I/O rows.
+
+## Technical approach
+
+1. **Route:** **`GET /game/world/recipe-definitions`** — parameterless read; inject **`IRecipeDefinitionRegistry`** only (not `RecipeDefinitionCatalog`).
+
+2. **Response shape:** Top-level **`schemaVersion`** (`RecipeDefinitionsListResponse.CurrentSchemaVersion` = **1**) plus **`recipes`** array. Each row maps from **`RecipeDefRow`**: **`id`**, **`displayName`**, **`recipeKind`**, **`requiredSkillId`**, **`inputs`**, **`outputs`**. Do **not** emit optional null fields on this contract.
+
+3. **I/O projection:** Map **`RecipeIoRow`** → JSON with **`itemId`**, **`quantity`** (same keys as content schema).
+
+4. **Ordering:** Build list from **`registry.GetDefinitionsInIdOrder()`** (already ordinal by id). Tests assert exact id sequence (frozen eight from [E3.M2 snapshot](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01)):
+
+ `make_armor_quick`, `make_contract_token`, `make_field_stim_batch`, `make_field_stim_mk0`, `make_prototype_armor`, `make_survey_drone_kit`, `refine_scrap_efficient`, `refine_scrap_standard`.
+
+5. **Implementation:** New **`RecipeDefinitionsWorldApi`** + **`RecipeDefinitionsListDtos.cs`** in `Game/Crafting/` (mirror [`ItemDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Items/ItemDefinitionsWorldApi.cs)). Wire **`app.MapRecipeDefinitionsWorldApi()`** from **`Program.cs`** next to **`MapItemDefinitionsWorldApi()`**.
+
+6. **Bruno:** `bruno/neon-sprawl-server/recipe-definitions/` with **`Get recipe definitions.bru`** + **`folder.bru`**. Tests: 200, JSON, **`schemaVersion` 1**, **`recipes.length === 8`**, ascending id order, **exact frozen-eight registry sequence** (mirror NEO-53 item-definitions Bruno), spot-check **`refine_scrap_standard`** (`recipeKind` `process`, inputs/outputs) and **`make_field_stim_mk0`** (`recipeKind` `make`, multi-input).
+
+7. **Docs:** `docs/manual-qa/NEO-68.md` (mirror NEO-53). Update **`server/README.md`** recipe section with GET + curl. Update [E3_M2](E3_M2_RefinementAndRecipeExecution.md) **Related implementation slices** and [documentation_and_implementation_alignment.md](documentation_and_implementation_alignment.md) E3.M2 row when landed.
+
+## Files to add
+
+| Path | Purpose |
+|------|---------|
+| `server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs` | `Map*` extension registering GET route; maps registry → JSON. |
+| `server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs` | Versioned response + row + I/O DTOs for JSON serialization. |
+| `server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema v1, eight ids in order, spot-check rows + I/O. |
+| `bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru` | Manual / Bruno verification against dev server. |
+| `bruno/neon-sprawl-server/recipe-definitions/folder.bru` | Bruno folder metadata (match `item-definitions`). |
+| `docs/manual-qa/NEO-68.md` | Checklist: start server, curl GET, confirm eight ids + spot-check I/O. |
+| `docs/plans/NEO-68-implementation-plan.md` | This plan. |
+
+## Files to modify
+
+| Path | Rationale |
+|------|-----------|
+| `server/NeonSprawl.Server/Program.cs` | Register `MapRecipeDefinitionsWorldApi()` alongside other game world definition APIs. |
+| `server/README.md` | Document `GET /game/world/recipe-definitions`, curl example, links to plan/manual QA/Bruno. |
+| `docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md` | **Related implementation slices** — HTTP read model bullet (NEO-68). |
+| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M2 row — note NEO-68 HTTP projection when landed. |
+
+## Tests
+
+| Test file | What it covers |
+|-----------|----------------|
+| `server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs` | **Integration:** `GET /game/world/recipe-definitions` via **`InMemoryWebApplicationFactory`**; **`ReadFromJsonAsync`**; assert **`schemaVersion` 1**; **`recipes`** count **8**; ordered **`id`** list equals **`FrozenEightInIdOrder`** (shared constant referenced by manual QA / Bruno); spot-check **`refine_scrap_standard`** and **`make_field_stim_mk0`**. **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-67** | Branch from **`main`** where NEO-67 is merged (confirmed at kickoff). | **adopted** |
+| **Optional fields later** | When content adds `requiredSkillLevel` / `stationTag`, bump **`schemaVersion`** or extend v1 with documented null semantics in a follow-up issue. | **deferred** |
+| **Client not in this story** | Cross-link **NEO-74** in plan; Bruno + manual QA are not prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md). | **adopted** |
+
+## Decisions (kickoff)
+
+- **Required six + I/O only** — no optional null fields on v1 contract (user confirmed).
+- **`recipes` array key** — consistent with entity naming in sibling world-definition endpoints.
+- **`docs/manual-qa/NEO-68.md` included** (user confirmed).
+- **Registry-only injection** — HTTP layer depends on **`IRecipeDefinitionRegistry`**, not **`RecipeDefinitionCatalog`**.
diff --git a/docs/reviews/2026-05-24-NEO-68.md b/docs/reviews/2026-05-24-NEO-68.md
new file mode 100644
index 0000000..b2dcb1a
--- /dev/null
+++ b/docs/reviews/2026-05-24-NEO-68.md
@@ -0,0 +1,69 @@
+# Code review — NEO-68 GET world recipe-definitions (kickoff plan)
+
+**Date:** 2026-05-24
+**Scope:** Branch `NEO-68-get-world-recipe-definitions-bruno` · commit `6de6a17` vs `origin/main` (implementation plan only — no server/Bruno/test code yet)
+**Base:** `origin/main`
+
+## Verdict
+
+**Approve** — kickoff plan is ready to implement; **re-review required** once HTTP/DTO/tests/Bruno land.
+
+## Summary
+
+The branch adds only [`docs/plans/NEO-68-implementation-plan.md`](../plans/NEO-68-implementation-plan.md): kickoff clarifications, acceptance criteria, and a technical approach that mirrors the landed [NEO-53](../plans/NEO-53-implementation-plan.md) item-definitions pattern. Scope is correctly bounded to **`GET /game/world/recipe-definitions`** backed by **`IRecipeDefinitionRegistry`** (NEO-67 on `main`), with **`schemaVersion` 1** / **`recipes`** envelope, six required row fields plus I/O arrays, Bruno, manual QA, and AAA integration tests. Client work is explicitly deferred to **NEO-74**. No correctness or security risk yet — there is no runtime code on this branch.
+
+## Documentation checked
+
+| Document | Result |
+|----------|--------|
+| [`docs/plans/NEO-68-implementation-plan.md`](../plans/NEO-68-implementation-plan.md) | **Matches** — kickoff decisions recorded; files-to-add/modify list complete; registry-only injection; frozen eight id order documented; NEO-74 cross-link present. |
+| [`docs/plans/E3M2-prototype-backlog.md`](../plans/E3M2-prototype-backlog.md) · **E3M2-04** | **Matches** — route, DTOs, API tests, Bruno, README align with backlog; plan adds `docs/manual-qa/NEO-68.md` (appropriate for player-visible HTTP, NEO-53 precedent). |
+| [`docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md`](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) | **Partially matches** — freeze table and eight ids match plan test assertions; **Related implementation slices** HTTP bullet correctly listed as a modify-on-land item. |
+| [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Partially matches** — E3.M2 row update deferred until implementation (correct). |
+| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Partially matches** — E3.M2 **Status** still **Planned** (same as post–NEO-67); optional bump when Slice 3 server spine completes. |
+| [`docs/decomposition/modules/client_server_authority.md`](../decomposition/modules/client_server_authority.md) | **Matches** (planned) — read-only world catalog; server source of truth; no client mutation in this story. |
+| NEO-53 reference implementation | **Matches** — plan cites [`ItemDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Items/ItemDefinitionsWorldApi.cs), DTO pattern, Bruno folder layout, and integration test shape. |
+| Full-stack epic decomposition | **Matches** — **NEO-74** documented as client counterpart; plan does not claim prototype slice complete without Godot. |
+
+Register/tracking: no alignment updates expected until implementation merges.
+
+## Blocking issues
+
+None (plan-only scope).
+
+## Suggestions
+
+1. ~~**Bruno — exact frozen-eight id order** — Plan section 6 requires ascending order and frozen eight presence. After NEO-53 review, [`Get item definitions.bru`](../../bruno/neon-sprawl-server/item-definitions/Get%20item%20definitions.bru) also asserts the **exact** registry sequence (not just sort + set membership). Mirror that with a `frozen prototype eight matches registry id order` Bruno test using the plan’s frozen list so manual runs catch ordering regressions without `dotnet test`.~~ **Done.** Bruno test asserts exact frozen-eight sequence.
+
+2. ~~**Shared frozen id constant** — Follow [`ItemDefinitionsWorldApiTests`](../../server/NeonSprawl.Server.Tests/Game/Items/ItemDefinitionsWorldApiTests.cs): define a `FrozenEightInIdOrder` string array in the integration test file and reference the same sequence in manual QA / Bruno docs to avoid drift from the E3.M2 freeze table.~~ **Done.** `RecipeDefinitionsWorldApiTests.FrozenEightInIdOrder`; manual QA and Bruno mirror the sequence.
+
+3. ~~**E3M2 backlog checkboxes on land** — When implementation merges, update **E3M2-04** acceptance checkboxes and landed note in [`E3M2-prototype-backlog.md`](../plans/E3M2-prototype-backlog.md) (same pattern as NEO-66/67).~~ **Done.**
+
+## Nits
+
+- ~~Nit: Plan lists **`RecipeDefinitionsListDtos.cs`** as a separate file — NEO-53 keeps DTOs in one file with the API class; either layout is fine; pick one and stay consistent with the Items folder convention.~~ **Done.** Separate DTO file matches NEO-53 (`ItemDefinitionsListDtos.cs` + `ItemDefinitionsWorldApi.cs`).
+
+- ~~Nit: Manual QA template in plan says “mirror NEO-53” — include the metadata table header (`Key`, `Title`, `Linear`, `Plan`, `Branch`) from [`docs/manual-qa/NEO-53.md`](../manual-qa/NEO-53.md) for consistency.~~ **Done.**
+
+## Verification
+
+Plan phase (current):
+
+```bash
+# Confirm branch is plan-only vs main
+git diff origin/main...HEAD --stat
+```
+
+After implementation (required before merge):
+
+```bash
+cd server
+dotnet test NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj --filter "FullyQualifiedName~RecipeDefinitionsWorldApiTests"
+dotnet test NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj
+```
+
+Manual:
+
+- Start server; `curl -sS "http://localhost:5253/game/world/recipe-definitions"` — **200**, **`schemaVersion` 1**, **`recipes.length` 8**, frozen eight ids in ordinal order.
+- Run `bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru` against `environments/Local.bru`.
+- Walk through `docs/manual-qa/NEO-68.md`.
diff --git a/server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs
new file mode 100644
index 0000000..6ecb771
--- /dev/null
+++ b/server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs
@@ -0,0 +1,60 @@
+using System.Linq;
+using System.Net;
+using System.Net.Http.Json;
+using NeonSprawl.Server.Game.Crafting;
+using Xunit;
+
+namespace NeonSprawl.Server.Tests.Game.Crafting;
+
+public class RecipeDefinitionsWorldApiTests
+{
+ /// Frozen prototype eight in registry id order (ordinal). Keep in sync with manual QA and Bruno.
+ public static readonly string[] FrozenEightInIdOrder =
+ [
+ "make_armor_quick",
+ "make_contract_token",
+ "make_field_stim_batch",
+ "make_field_stim_mk0",
+ "make_prototype_armor",
+ "make_survey_drone_kit",
+ "refine_scrap_efficient",
+ "refine_scrap_standard",
+ ];
+
+ [Fact]
+ public async Task GetRecipeDefinitions_ShouldReturnSchemaV1_WithFrozenEightInIdOrder()
+ {
+ // Arrange
+ await using var factory = new InMemoryWebApplicationFactory();
+ var client = factory.CreateClient();
+ // Act
+ var response = await client.GetAsync("/game/world/recipe-definitions");
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadFromJsonAsync();
+ Assert.NotNull(body);
+ Assert.Equal(RecipeDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
+ Assert.NotNull(body.Recipes);
+ var ids = body.Recipes.Select(static r => r.Id).ToList();
+ Assert.Equal(FrozenEightInIdOrder, ids);
+
+ var refine = body.Recipes.Single(r => r.Id == "refine_scrap_standard");
+ Assert.Equal("Refine Scrap (Standard)", refine.DisplayName);
+ Assert.Equal("process", refine.RecipeKind);
+ Assert.Equal("refine", refine.RequiredSkillId);
+ Assert.Single(refine.Inputs);
+ Assert.Equal("scrap_metal_bulk", refine.Inputs[0].ItemId);
+ Assert.Equal(5, refine.Inputs[0].Quantity);
+ Assert.Single(refine.Outputs);
+ Assert.Equal("refined_plate_stock", refine.Outputs[0].ItemId);
+ Assert.Equal(1, refine.Outputs[0].Quantity);
+
+ var stim = body.Recipes.Single(r => r.Id == "make_field_stim_mk0");
+ Assert.Equal("make", stim.RecipeKind);
+ Assert.Equal(2, stim.Inputs.Count);
+ Assert.Equal("refined_plate_stock", stim.Inputs[0].ItemId);
+ Assert.Equal(2, stim.Inputs[0].Quantity);
+ Assert.Equal("scrap_metal_bulk", stim.Inputs[1].ItemId);
+ Assert.Equal(1, stim.Inputs[1].Quantity);
+ }
+}
diff --git a/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs
new file mode 100644
index 0000000..ebd8c02
--- /dev/null
+++ b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs
@@ -0,0 +1,48 @@
+using System.Text.Json.Serialization;
+
+namespace NeonSprawl.Server.Game.Crafting;
+
+/// JSON body for GET /game/world/recipe-definitions (NEO-68).
+public sealed class RecipeDefinitionsListResponse
+{
+ public const int CurrentSchemaVersion = 1;
+
+ [JsonPropertyName("schemaVersion")]
+ public int SchemaVersion { get; init; } = CurrentSchemaVersion;
+
+ /// Loaded recipes ordered by stable id (ordinal), matching .
+ [JsonPropertyName("recipes")]
+ public required IReadOnlyList Recipes { get; init; }
+}
+
+/// One row in the read-only recipe definition projection.
+public sealed class RecipeDefinitionJson
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; init; }
+
+ [JsonPropertyName("displayName")]
+ public required string DisplayName { get; init; }
+
+ [JsonPropertyName("recipeKind")]
+ public required string RecipeKind { get; init; }
+
+ [JsonPropertyName("requiredSkillId")]
+ public required string RequiredSkillId { get; init; }
+
+ [JsonPropertyName("inputs")]
+ public required IReadOnlyList Inputs { get; init; }
+
+ [JsonPropertyName("outputs")]
+ public required IReadOnlyList Outputs { get; init; }
+}
+
+/// One input or output row in the read-only recipe definition projection.
+public sealed class RecipeIoJson
+{
+ [JsonPropertyName("itemId")]
+ public required string ItemId { get; init; }
+
+ [JsonPropertyName("quantity")]
+ public required int Quantity { get; init; }
+}
diff --git a/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs
new file mode 100644
index 0000000..a6d6a64
--- /dev/null
+++ b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs
@@ -0,0 +1,54 @@
+namespace NeonSprawl.Server.Game.Crafting;
+
+/// Maps GET /game/world/recipe-definitions (NEO-68).
+public static class RecipeDefinitionsWorldApi
+{
+ public static WebApplication MapRecipeDefinitionsWorldApi(this WebApplication app)
+ {
+ app.MapGet(
+ "/game/world/recipe-definitions",
+ (IRecipeDefinitionRegistry registry) =>
+ {
+ var defs = registry.GetDefinitionsInIdOrder();
+ var recipes = new List(defs.Count);
+ foreach (var d in defs)
+ {
+ recipes.Add(
+ new RecipeDefinitionJson
+ {
+ Id = d.Id,
+ DisplayName = d.DisplayName,
+ RecipeKind = d.RecipeKind,
+ RequiredSkillId = d.RequiredSkillId,
+ Inputs = MapIoRows(d.Inputs),
+ Outputs = MapIoRows(d.Outputs),
+ });
+ }
+
+ return Results.Json(
+ new RecipeDefinitionsListResponse
+ {
+ SchemaVersion = RecipeDefinitionsListResponse.CurrentSchemaVersion,
+ Recipes = recipes,
+ });
+ });
+
+ return app;
+ }
+
+ private static List MapIoRows(IReadOnlyList rows)
+ {
+ var list = new List(rows.Count);
+ foreach (var row in rows)
+ {
+ list.Add(
+ new RecipeIoJson
+ {
+ ItemId = row.ItemId,
+ Quantity = row.Quantity,
+ });
+ }
+
+ return list;
+ }
+}
diff --git a/server/NeonSprawl.Server/Program.cs b/server/NeonSprawl.Server/Program.cs
index 2f54b74..7d7e2e7 100644
--- a/server/NeonSprawl.Server/Program.cs
+++ b/server/NeonSprawl.Server/Program.cs
@@ -44,6 +44,7 @@ app.MapInteractionApi();
app.MapInteractablesWorldApi();
app.MapSkillDefinitionsWorldApi();
app.MapItemDefinitionsWorldApi();
+app.MapRecipeDefinitionsWorldApi();
app.MapResourceNodeDefinitionsWorldApi();
app.MapPlayerInventoryApi();
app.MapSkillProgressionSnapshotApi();
diff --git a/server/README.md b/server/README.md
index 554459e..bef322b 100644
--- a/server/README.md
+++ b/server/README.md
@@ -80,6 +80,14 @@ On startup the host loads every **`*_recipes.json`** under the recipes directory
On success, **Information** logs include the resolved recipes directory path, distinct recipe count, and catalog file count. Game code should use **`IRecipeDefinitionRegistry`** for lookups (NEO-67). The catalog singleton remains for fail-fast startup only; do not inject **`RecipeDefinitionCatalog`** in new game code.
+## Recipe definitions (NEO-68)
+
+**`GET /game/world/recipe-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`recipes`**) backed by **`IRecipeDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`recipeKind`**, **`requiredSkillId`**, and nested **`inputs`** / **`outputs`** (`itemId`, `quantity`). Plan: [NEO-68 implementation plan](../../docs/plans/NEO-68-implementation-plan.md); manual QA: [`docs/manual-qa/NEO-68.md`](../../docs/manual-qa/NEO-68.md); Bruno: `bruno/neon-sprawl-server/recipe-definitions/`.
+
+```bash
+curl -sS -i "http://localhost:5253/game/world/recipe-definitions"
+```
+
## Resource node definitions (NEO-60)
**`GET /game/world/resource-node-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`nodes`**) backed by **`IResourceNodeDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`gatherLens`**, **`maxGathers`**, and nested **`yield`** (`itemId`, `quantity`). Plan: [NEO-60 implementation plan](../../docs/plans/NEO-60-implementation-plan.md); manual QA: [`docs/manual-qa/NEO-60.md`](../../docs/manual-qa/NEO-60.md); Bruno: `bruno/neon-sprawl-server/resource-node-definitions/`.