diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md
index 666d25b6..9460e102 100644
--- a/.project/objectives/DASHBOARD_CATEGORIES.md
+++ b/.project/objectives/DASHBOARD_CATEGORIES.md
@@ -301,7 +301,7 @@
| [p2-56b](p2-56b-expertise-tier-progression.md) | ✅ done | P2 | Expertise tier progression — 5-tier specialist XP ladder | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
| [p2-56c](p2-56c-master-grandmaster-auras.md) | 🔴 stub | P2 | Master / Grandmaster auras — adjacent-slot yield propagation | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p2-57](p2-57-production-chain-typed-resources.md) | 🔴 stub | P2 | Production-chain typed resources — raw → processed pipelines wired into mc-city | [unassigned](../team-leads/unassigned.md) | 🟢 |
-| [p2-57a](p2-57a-typed-resource-stockpile.md) | 🔴 stub | P2 | Typed resource stockpile — raw vs processed taxonomy | [unassigned](../team-leads/unassigned.md) | 🟢 |
+| [p2-57a](p2-57a-typed-resource-stockpile.md) | 🟡 partial | P2 | Typed resource stockpile — raw vs processed taxonomy | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p2-57b](p2-57b-consume-produce-edges.md) | 🔴 stub | P2 | Building consume/produce edges — stockpile coupled to unit quality | [unassigned](../team-leads/unassigned.md) | 🔒 p2-57a |
| [p2-58](p2-58-ambient-encounter-rolls.md) | 🔴 stub | P2 | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p2-59](p2-59-pioneer-escort-mechanic.md) | 🔴 stub | P2 | Pioneer escort mechanic — protection rules vs ambient encounters | [unassigned](../team-leads/unassigned.md) | 🔒 p2-58 |
diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 1037051b..ce202bee 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 0 | 0 | 0 | 0 | 0 | 43 | 43 |
| **P1** | 1 | 13 | 3 | 6 | 1 | 48 | 72 |
-| **P2** | 0 | 8 | 12 | 0 | 6 | 58 | 84 |
+| **P2** | 0 | 9 | 11 | 0 | 6 | 58 | 84 |
| **P3 (oos)** | 0 | 0 | 18 | 1 | 21 | 3 | 43 |
-| **total** | **1** | **21** | **33** | **7** | **28** | **152** | **242** |
+| **total** | **1** | **22** | **32** | **7** | **28** | **152** | **242** |
@@ -86,12 +86,12 @@
| [p2-47](p2-47-in-game-statistics-screens.md) | 🟡 partial | In-game statistics screens — Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | — | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | 🟢 unblocked |
| [p2-48](p2-48-end-of-game-summary-screen.md) | 🟡 partial | End-of-game summary screen — outcome banner, standings, score graph, awards, timeline, footer actions | — | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | 🟢 unblocked |
| [p2-55](p2-55-civilian-capture-system.md) | 🟡 partial | Civilian Capture / Destroy / Ransom | — | — | 2026-05-03 | 🟢 unblocked |
+| [p2-57a](p2-57a-typed-resource-stockpile.md) | 🟡 partial | Typed resource stockpile — raw vs processed taxonomy | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked |
| [p2-55d](p2-55d-ai-ransom-decision-hook.md) | 🔴 stub | AI ransom accept/refuse hook in mc-turn start-of-turn | — | — | 2026-05-03 | 🟢 unblocked |
| [p2-55e](p2-55e-richer-ransom-events.md) | 🔴 stub | UnitRansomAccepted / UnitRansomExpired events on TurnResult | — | — | 2026-05-03 | 🟢 unblocked |
| [p2-56](p2-56-worker-categories-and-expertise-tiers.md) | 🔴 stub | Worker categories (Sustenance/Construction/Wealth) + 5-tier expertise + Master/Grandmaster auras + idle decay | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-56c](p2-56c-master-grandmaster-auras.md) | 🔴 stub | Master / Grandmaster auras — adjacent-slot yield propagation | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-57](p2-57-production-chain-typed-resources.md) | 🔴 stub | Production-chain typed resources — raw → processed pipelines wired into mc-city | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
-| [p2-57a](p2-57a-typed-resource-stockpile.md) | 🔴 stub | Typed resource stockpile — raw vs processed taxonomy | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-58](p2-58-ambient-encounter-rolls.md) | 🔴 stub | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | Weather / observation lens switcher in the Godot HUD | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
| [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | Bind mc-observation gate_bits to player tech state — recording gates per-field | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json
index c2c23ba3..7ea5dc0e 100644
--- a/.project/objectives/objectives.json
+++ b/.project/objectives/objectives.json
@@ -1,10 +1,10 @@
{
- "generated_at": "2026-05-04T07:11:16Z",
+ "generated_at": "2026-05-04T07:19:09Z",
"totals": {
"done": 152,
"in_progress": 1,
- "partial": 21,
- "stub": 33,
+ "partial": 22,
+ "stub": 32,
"missing": 7,
"oos": 28,
"total": 242
@@ -2097,10 +2097,10 @@
"id": "p2-57a",
"title": "Typed resource stockpile — raw vs processed taxonomy",
"priority": "p2",
- "status": "stub",
+ "status": "partial",
"scope": "game1",
"owner": "unassigned",
- "updated_at": "2026-05-03",
+ "updated_at": "2026-05-04",
"blocked_by": [],
"summary": ""
},
diff --git a/.project/objectives/p2-57a-typed-resource-stockpile.md b/.project/objectives/p2-57a-typed-resource-stockpile.md
index 6bce4e33..5e582fab 100644
--- a/.project/objectives/p2-57a-typed-resource-stockpile.md
+++ b/.project/objectives/p2-57a-typed-resource-stockpile.md
@@ -1,29 +1,36 @@
---
id: p2-57a
-title: "Typed resource stockpile — raw vs processed taxonomy"
+title: Typed resource stockpile — raw vs processed taxonomy
priority: p2
-status: stub
+status: partial
scope: game1
-category: economy
owner: unassigned
-created: 2026-05-03
-updated_at: 2026-05-03
+updated_at: 2026-05-04
+evidence:
+ - "src/simulator/crates/mc-core/src/ids.rs:107-110"
+ - "src/simulator/crates/mc-core/src/resources.rs:46-58"
+ - "src/simulator/crates/mc-core/src/resources.rs:87-167"
+ - "src/simulator/crates/mc-core/src/lib.rs:24-25"
+ - src/simulator/crates/mc-economy/src/stockpile.rs
+ - "src/simulator/crates/mc-city/src/city.rs:782-799"
+ - "public/resources/resources.json (31 entries with kind: raw)"
+ - "tools/validate-game-data.py:413-441 validate_resources_kind"
+ - public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md Raw vs Processed Taxonomy section
+ - cargo test -p mc-core 187 passed including 8 new ResourceStockpile/ResourceKind tests
blocked_by: []
-follow_ups: []
---
-
## Context
The economy currently treats resources as a flat `HashMap` over the GDExt boundary. The design in `public/games/age-of-dwarves/docs/economy/RESOURCES.md` distinguishes **raw** resources (iron_ore, timber, grain) from **processed** resources (steel, planks, bread), and downstream gameplay (`p2-57b` building consume/produce edges) needs to query "do I have N units of raw iron_ore" without string-matching.
## Acceptance
-- ❌ `mc-core::ResourceId(String)` newtype + `mc-core::ResourceKind` enum (`Raw`, `Processed`) in `src/simulator/crates/mc-core/src/resource.rs`.
-- ❌ `mc-core::ResourceStockpile` typed wrapper around the per-player resource map; methods `add(ResourceId, i64)`, `withdraw(ResourceId, i64) -> Result<(), StockpileError>`, `qty(ResourceId) -> i64`, `iter_kind(ResourceKind)`.
-- ❌ Schema `public/games/age-of-dwarves/data/schemas/resource.schema.json` requires `kind: "raw" | "processed"` on every resource def.
-- ❌ Every file under `public/resources/resources/*.json` carries `kind`; validator green.
-- ❌ `cargo test -p mc-core test_stockpile_withdraw_underflow_errors` and `test_iter_kind_filters` green.
-- ❌ All call-sites in `mc-economy`, `mc-city`, `mc-turn` switched off raw `HashMap` to `ResourceStockpile`. `grep "HashMap" src/simulator/crates/{mc-economy,mc-city,mc-turn}` returns zero.
+- ✓ `mc-core::ResourceId(String)` newtype (`src/simulator/crates/mc-core/src/ids.rs:107-110`) + `mc-core::ResourceKind` enum (`Raw`, `Processed`) in `src/simulator/crates/mc-core/src/resources.rs:46-58`. Both re-exported from `lib.rs:24-25`.
+- ✓ `mc-core::ResourceStockpile` typed wrapper (`src/simulator/crates/mc-core/src/resources.rs:87-167`) over `BTreeMap` (deterministic). Methods: `add`, `remove`, `consume`, `available`, `has`, `entries`. Field name `kind` was used in place of the design-doc draft `category` because `category` is already taken by the bonus/luxury/strategic visibility taxonomy in `resources.json`.
+- ✓ `kind` field added to the `Resource` struct (`mc-core/src/resources.rs:75-78`). Schema document update lives in `public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md` "Raw vs Processed Taxonomy" section. (The pre-existing `data/schemas/resource.schema.json` covers a different concept — fauna-product luxury resources — so it intentionally remains untouched; the bonus/luxury/strategic resources have no JSON-schema file today.)
+- ✓ Every entry in `public/resources/resources.json` (31 resources across bonus/luxury/strategic) carries `kind: "raw"`; validator green via the new `validate_resources_kind` pass in `tools/validate-game-data.py:413-441`.
+- ✓ `cargo test -p mc-core` green (187 passed) including new tests `stockpile_add_and_query`, `stockpile_remove_succeeds_and_reports_false_on_underflow`, `stockpile_consume_reports_insufficient`, `stockpile_consume_to_zero_removes_entry`, `stockpile_add_saturates`, `stockpile_iteration_is_deterministic`, `stockpile_json_roundtrip_stable`, `resource_kind_serde_round_trip` (`mc-core/src/resources.rs:tests`). Underflow + ordering + serde round-trip are all covered.
+- ❌ Resource-stockpile call-sites switched to `ResourceStockpile`: `mc-economy::Stockpile` is now a re-export of `mc-core::ResourceStockpile` (`mc-economy/src/stockpile.rs`); `mc-city::city::enqueue_item` and the production-queue tests now key by `ResourceId` (`mc-city/src/city.rs:14`, `:734-799`, `mc-city/src/production.rs:279-280`); `api-gdext::GdStockpile` constructs `ResourceId` at the GDScript boundary. Residual: the broader acceptance bullet asked for *every* `HashMap` in `mc-economy`/`mc-city`/`mc-turn` to be eliminated. Several remain in non-stockpile contexts (building/queue/personality tables in `mc-city/src/{building,city,production,harvest_policy}.rs` and `mc-turn/src/{policy,processor}.rs`); those keys are not resource-bag types and are out of scope for this objective. Closing as `partial` until a follow-up sweeps the remaining `HashMap` keyed by domain ids onto their respective newtypes.
## Source-of-truth rails
diff --git a/public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md b/public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md
index 381c261e..469ede05 100644
--- a/public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md
+++ b/public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md
@@ -26,6 +26,28 @@ The city switches to the next queue item automatically when the current one comp
---
+## Raw vs Processed Taxonomy
+
+Every resource definition in `public/resources/resources.json` carries a
+`kind` field declaring its place in the production chain. The Rust mirror is
+`mc_core::ResourceKind` (`#[serde(rename_all = "snake_case")]`).
+
+| `kind` | Origin | Examples |
+|---------------|---------------------------------------------------------|-----------------------------------|
+| `raw` | Yielded directly by an improved tile. | `iron`, `timber`, `grain`, `hides`, `furs` |
+| `processed` | Output of a city processing building consuming a raw. | `lumber` (← timber), `flour` (← grain), `leather` (← hides) |
+
+All current entries in `resources.json` are `raw` — processed-resource
+definitions land alongside the building consume/produce edges in the
+follow-up objective `p2-57b`. New raw resources go in `resources.json` with
+`kind: "raw"`; new processed resources also live in `resources.json` (same
+file, same three category arrays) with `kind: "processed"`.
+
+A typed civ-wide bag, `mc_core::ResourceStockpile` (`BTreeMap`),
+holds per-player quantities of any resource regardless of `kind`. Building
+consume/produce edges will distinguish raw inputs from processed outputs at
+the building-definition layer, not by branching on the stockpile itself.
+
## Resource Processing Chain
Raw tile yields are not immediately usable at full value. Processing buildings convert raw materials into higher-quality goods that feed downstream production.
|