Biodiversity Drift + Ecoregion Match #20

Closed
opened 2026-06-17 07:29:26 +00:00 by coilysiren · 2 comments
Owner

Originally filed by @coilysiren on 2026-05-02T23:10:19Z

Task 10 — Biodiversity Drift + Ecoregion Match

Prereq: read todo/README.md first.

Goal

Build an MCP tool get_eco_ecoregion rendering a card that:

  1. Classifies the world's biome composition against real-world ecoregions.
  2. Shows per-species population drift since cycle start.

Data sources

Public:

  • GET http://eco.coilysiren.me:3001/api/v1/worldlayers/layers — list of 7 top-level categories. Each entry: {Category, List: [{LayerName, LayerDisplayName, Summary, Tooltip, Category, DisplayRow}]}.
    • Category strings confirmed in live data include "Biome" and "Animal". Check the other category strings before assuming "Plant Group" is one of them — pull the distinct Category values from the response first.
    • Biome layers (current cycle): TaigaBiome, DesertBiome, WetlandBiome, ColdForestBiome, ForestBiome, WarmForestBiome, TundraBiome, DeepOceanBiome, OceanBiome, GrasslandBiome, RainforestBiome, IceBiome — 12 total.
    • Each biome's Summary is a string like "4%". Parse with float(re.match(r"(\d+(?:\.\d+)?)", summary).group(1)).

Admin (header X-API-Key from SSM /eco-mcp-app/api-admin-token, region us-east-1):

  • GET /api/v1/exporter/specieslistnewline-delimited plain text, NOT JSON. Parse resp.text.splitlines().
  • GET /api/v1/exporter/species?speciesName=X — CSV "Time","Value". Time is seconds since cycle start (600 s cadence).

Static, committed in-repo:

  • A WWF Terrestrial Ecoregions summary table. Fetch once and commit to data/ecoregions.json (don't fetch at runtime). Source candidates: https://ecoregions.appspot.com/ (manual download) or WWF's Terrestrial Ecoregions of the World publication. Shape: [{name, biome_vector: {TaigaBiome: 0.0, DesertBiome: 0.15, ...}, description}, ...].

IMPORTANT: biome percentages do NOT sum to 100

In live data (verified today):

ColdForestBiome 4%, DeepOceanBiome 1%, DesertBiome 4%, ForestBiome 0%,
GrasslandBiome 7%, IceBiome 0%, OceanBiome 13%, RainforestBiome 4%,
TaigaBiome 1%, TundraBiome 1%, WarmForestBiome 3%, WetlandBiome 1%
Sum ≈ 39%

The summaries are % of world area, but large chunks of the world (mountains, shallow water, transitional zones) don't fall into any named biome. Normalize the vector to sum=1.0 before doing cosine similarity with the WWF vectors. Do not assert on raw-sum >= 95%.

Card layout

  1. Top: pie/donut chart of raw biome percentages, labeled with raw values ("Ocean 13%, Grassland 7%, …"). Note somewhere on the card that the remaining % is "unclassified / mixed terrain".
  2. Middle: closest 3 real-world ecoregions to the normalized biome vector, with a short description of each. Use cosine similarity: sim(a, b) = dot(a, b) / (norm(a) * norm(b)).
  3. Bottom: biodiversity drift strip — two sub-sections:
    • Boom (green): top 5 species by positive (latest − first) / first delta.
    • Bust (red): top 5 species by negative delta.

Dead-end (do not attempt)

Rendering world biomes as spatial tiles. The per-layer endpoint only returns summary text, no grid pixels — verified dead in a prior spike. This tool works from percentages only.

Implementation notes

  • Cache species CSVs in-process for 60 s; 92-ish species × 6 KB = ~0.5 MB, re-fetch is cheap.
  • Cache the worldlayers/layers response for 5 minutes — biome percentages don't change minute-to-minute.
  • Bundle chart.js in the HTML; no CDN at runtime (CSP).

Acceptance

  • Biome donut chart renders with raw percentages and a "unclassified" slice filling the gap to 100%.
  • Top-3 ecoregion matches are stable across consecutive calls (deterministic for same inputs).
  • Drift strip renders boom and bust lists; on Day 3 both lists may have near-zero deltas — render "drift minimal so far" as a placeholder in that case.
  • Unit tests verify normalization and cosine similarity using the committed ecoregions.json.
  • inv smoke passes.

Migrated from coilyco-flight-deck/eco-mcp-app#6 during the eco repo consolidation (coilysiren/inbox#100).

_Originally filed by @coilysiren on 2026-05-02T23:10:19Z_ # Task 10 — Biodiversity Drift + Ecoregion Match **Prereq**: read `todo/README.md` first. ## Goal Build an MCP tool `get_eco_ecoregion` rendering a card that: 1. Classifies the world's biome composition against real-world ecoregions. 2. Shows per-species population drift since cycle start. ## Data sources Public: - `GET http://eco.coilysiren.me:3001/api/v1/worldlayers/layers` — list of 7 top-level categories. Each entry: `{Category, List: [{LayerName, LayerDisplayName, Summary, Tooltip, Category, DisplayRow}]}`. - Category strings confirmed in live data include `"Biome"` and `"Animal"`. **Check the other category strings before assuming `"Plant Group"` is one of them — pull the distinct `Category` values from the response first.** - Biome layers (current cycle): `TaigaBiome`, `DesertBiome`, `WetlandBiome`, `ColdForestBiome`, `ForestBiome`, `WarmForestBiome`, `TundraBiome`, `DeepOceanBiome`, `OceanBiome`, `GrasslandBiome`, `RainforestBiome`, `IceBiome` — 12 total. - Each biome's `Summary` is a string like `"4%"`. Parse with `float(re.match(r"(\d+(?:\.\d+)?)", summary).group(1))`. Admin (header `X-API-Key` from SSM `/eco-mcp-app/api-admin-token`, **region `us-east-1`**): - `GET /api/v1/exporter/specieslist` — **newline-delimited plain text, NOT JSON**. Parse `resp.text.splitlines()`. - `GET /api/v1/exporter/species?speciesName=X` — CSV `"Time","Value"`. `Time` is **seconds since cycle start** (600 s cadence). Static, committed in-repo: - A WWF Terrestrial Ecoregions summary table. Fetch **once** and commit to `data/ecoregions.json` (don't fetch at runtime). Source candidates: `https://ecoregions.appspot.com/` (manual download) or WWF's Terrestrial Ecoregions of the World publication. Shape: `[{name, biome_vector: {TaigaBiome: 0.0, DesertBiome: 0.15, ...}, description}, ...]`. ## IMPORTANT: biome percentages do NOT sum to 100 In live data (verified today): ``` ColdForestBiome 4%, DeepOceanBiome 1%, DesertBiome 4%, ForestBiome 0%, GrasslandBiome 7%, IceBiome 0%, OceanBiome 13%, RainforestBiome 4%, TaigaBiome 1%, TundraBiome 1%, WarmForestBiome 3%, WetlandBiome 1% Sum ≈ 39% ``` The summaries are % of world area, but large chunks of the world (mountains, shallow water, transitional zones) don't fall into any named biome. **Normalize the vector** to `sum=1.0` before doing cosine similarity with the WWF vectors. Do not assert on raw-sum >= 95%. ## Card layout 1. **Top**: pie/donut chart of raw biome percentages, labeled with raw values ("Ocean 13%, Grassland 7%, …"). Note somewhere on the card that the remaining % is "unclassified / mixed terrain". 2. **Middle**: closest **3 real-world ecoregions** to the normalized biome vector, with a short description of each. Use cosine similarity: `sim(a, b) = dot(a, b) / (norm(a) * norm(b))`. 3. **Bottom**: biodiversity drift strip — two sub-sections: - **Boom** (green): top 5 species by positive `(latest − first) / first` delta. - **Bust** (red): top 5 species by negative delta. ## Dead-end (do not attempt) Rendering world biomes as spatial tiles. The per-layer endpoint only returns summary text, no grid pixels — verified dead in a prior spike. This tool works from percentages only. ## Implementation notes - Cache species CSVs in-process for 60 s; 92-ish species × 6 KB = ~0.5 MB, re-fetch is cheap. - Cache the `worldlayers/layers` response for 5 minutes — biome percentages don't change minute-to-minute. - Bundle chart.js in the HTML; no CDN at runtime (CSP). ## Acceptance - Biome donut chart renders with raw percentages and a "unclassified" slice filling the gap to 100%. - Top-3 ecoregion matches are stable across consecutive calls (deterministic for same inputs). - Drift strip renders `boom` and `bust` lists; on Day 3 both lists may have near-zero deltas — render "drift minimal so far" as a placeholder in that case. - Unit tests verify normalization and cosine similarity using the committed `ecoregions.json`. - `inv smoke` passes. --- _Migrated from coilyco-flight-deck/eco-mcp-app#6 during the eco repo consolidation (coilysiren/inbox#100)._
coilysiren added
P3
and removed
P2
labels 2026-06-17 08:39:07 +00:00
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-20 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-05T04:37:28Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-20` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-05T04:37:28Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - shipped the /ecoregion SPA surface for get_eco_ecoregion and fixed an inf-delta JSON crash the new endpoint would have hit.

Honest retro: most of this tool was already here. The MCP side (ecoregion.py, the card, the committed WWF fixture, its tests) rode in with the repo consolidation, so the real gap was the product surface - every sibling tool has an SPA page and this one didn't, which is what kept the issue open under the "product UX is the SPA" doctrine. Copying the Climate/Crafting page pattern made that part smooth.

What fought back: a latent bug the SPA route exposed. A species growing from a zero baseline yields deltaRel=inf, and Starlette's JSONResponse rejects inf outright, so /preview/get_eco_ecoregion.json would have 500'd in production. I only caught it by checking the serialization before trusting it. Fixed it at the payload boundary (deltaRel=null + a fromZero flag, rendered as "new" in all three consumers) and drove the real HTTP route with a from-zero grower to confirm the 200.

Confidence is high on the code - 404 python + 58 frontend tests green, lint/build/smoke clean, and the concurrent /replay landing that raced me to main merged cleanly. One caveat: I could not reach the live game server from the container, so I validated against mocked upstream rather than real biome percentages - worth an eyeball on the donut once it deploys. Possible follow-up: the SPA donut shows raw percents with an explicit unclassified slice (per the issue acceptance) while the older in-chat MCP card shows normalized classified-share, so the two surfaces diverge on that choice and might be worth reconciling.

WARD-OUTCOME: done - shipped the `/ecoregion` SPA surface for `get_eco_ecoregion` and fixed an inf-delta JSON crash the new endpoint would have hit. Honest retro: most of this tool was already here. The MCP side (`ecoregion.py`, the card, the committed WWF fixture, its tests) rode in with the repo consolidation, so the real gap was the product surface - every sibling tool has an SPA page and this one didn't, which is what kept the issue open under the "product UX is the SPA" doctrine. Copying the Climate/Crafting page pattern made that part smooth. What fought back: a latent bug the SPA route exposed. A species growing from a zero baseline yields `deltaRel=inf`, and Starlette's `JSONResponse` rejects `inf` outright, so `/preview/get_eco_ecoregion.json` would have 500'd in production. I only caught it by checking the serialization before trusting it. Fixed it at the payload boundary (`deltaRel=null` + a `fromZero` flag, rendered as "new" in all three consumers) and drove the real HTTP route with a from-zero grower to confirm the 200. Confidence is high on the code - 404 python + 58 frontend tests green, lint/build/smoke clean, and the concurrent `/replay` landing that raced me to main merged cleanly. One caveat: I could not reach the live game server from the container, so I validated against mocked upstream rather than real biome percentages - worth an eyeball on the donut once it deploys. Possible follow-up: the SPA donut shows raw percents with an explicit unclassified slice (per the issue acceptance) while the older in-chat MCP card shows normalized classified-share, so the two surfaces diverge on that choice and might be worth reconciling.
Sign in to join this conversation.
No description provided.