Civics / Governance surface: elections, votes, laws, demographics, settlements (DiscordLink parity, no restart) #61

Closed
opened 2026-07-05 04:39:21 +00:00 by coilysiren · 2 comments
Owner

Context

Net-new Civics / Governance surface, the largest unconsumed vein in the pull-everything survey (#7) and the biggest remaining DiscordLink-parity gap under the epic (#37, which explicitly wants elections/laws/voting displays). All from data the server already exports - no game restart.

No-reset spine

Consume the civics action exporters (/api/v1/exporter/actions) and the 10 civics/people daily series (/datasets/get) already live on the server. Reuse the shared CSV plumbing the trade wave stood up: streamed aggregation (crafting.py), the #5 column-corrector and id-to-name join (/api/v1/citizens), and Time-to-day conversion. No new C# mod.

Exporters

Vote, DidntVote, StartElection, JoinOrLeaveElection, WonElection, LostElection, DemographicChange, ResidencyChanged, BecomeCitizen, LeaveCitizenship, SettlementFounded, PlaceNewSettlementFoundation, StartHomestead, plus the civics/people series.

Scope

  • MCP tool get_eco_civics (text + structured JSON + _meta.ui card): active/recent elections and outcomes, turnout (Vote vs DidntVote), laws-in-effect where derivable, demographic trend (citizens gained/lost, residency moves), settlements founded. Names via the citizens join, falling back to Citizen #<id>.
  • /preview/civics.json data plane.
  • SPA /civics route - elections + outcomes, turnout, demographics, settlements; cross-linked with /economy and /server, live badge on the homepage directory. Read the dataviz skill before charts.

Directly serves the epic's "elections and votes, laws, demographics" display set on the website instead of Discord embeds. Meets and exceeds by adding turnout and demographic trend over time.

Acceptance

  • MCP tool registered, ward exec smoke passes; thin/zero-data paths degrade gracefully.
  • respx-mocked unit tests for the aggregation + citizen join.
  • SPA route renders against /preview/civics.json, vitest component test, ward exec frontend-test/lint green.
  • ward exec test/lint green; docs/FEATURES.md updated. Parent: #37, survey #7.
## Context Net-new **Civics / Governance** surface, the largest unconsumed vein in the pull-everything survey ([#7](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/7)) and the biggest remaining DiscordLink-parity gap under the epic ([#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37), which explicitly wants elections/laws/voting displays). All from data the server **already exports** - no game restart. ## No-reset spine Consume the civics action exporters (`/api/v1/exporter/actions`) and the 10 civics/people daily series (`/datasets/get`) already live on the server. Reuse the shared CSV plumbing the trade wave stood up: streamed aggregation (`crafting.py`), the `#5` column-corrector and id-to-name join (`/api/v1/citizens`), and Time-to-day conversion. No new C# mod. ## Exporters `Vote`, `DidntVote`, `StartElection`, `JoinOrLeaveElection`, `WonElection`, `LostElection`, `DemographicChange`, `ResidencyChanged`, `BecomeCitizen`, `LeaveCitizenship`, `SettlementFounded`, `PlaceNewSettlementFoundation`, `StartHomestead`, plus the civics/people series. ## Scope * **MCP tool** `get_eco_civics` (text + structured JSON + `_meta.ui` card): active/recent elections and outcomes, turnout (Vote vs DidntVote), laws-in-effect where derivable, demographic trend (citizens gained/lost, residency moves), settlements founded. Names via the citizens join, falling back to `Citizen #<id>`. * **`/preview/civics.json`** data plane. * **SPA `/civics` route** - elections + outcomes, turnout, demographics, settlements; cross-linked with `/economy` and `/server`, live badge on the homepage directory. Read the `dataviz` skill before charts. ## DiscordLink parity Directly serves the epic's "elections and votes, laws, demographics" display set on the website instead of Discord embeds. Meets and exceeds by adding turnout and demographic trend over time. ## Acceptance * MCP tool registered, `ward exec smoke` passes; thin/zero-data paths degrade gracefully. * `respx`-mocked unit tests for the aggregation + citizen join. * SPA route renders against `/preview/civics.json`, vitest component test, `ward exec frontend-test`/`lint` green. * `ward exec test`/`lint` green; `docs/FEATURES.md` updated. Parent: [#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37), survey [#7](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/7).
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-61 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-05T04:39:30Z). 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-61` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-05T04:39:30Z). 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 - get_eco_civics MCP tool, /preview/civics.json plane, and the SPA /civics route landed on main (a6848b9), closing #61.

This one went smoothly where I expected friction and fought back where I didn't. The aggregation itself was easy - the trade wave left such a clean spine (streamed CSV, _corrected_index, the citizens join, Time/86400) that civics.py was mostly a matter of picking the right columns and folding. The genuinely honest caveat: the exact civic CSV headers and the 10 civics/people series names were never captured live (the #7 survey is inventory-only for those families), so I parse a generous candidate-column list and treat the daily series as the same names doubling as datasets. On a live server the turnout/demographic counts and the citizen actors are solid; the subject labels (election/settlement names) are best-effort until someone probes the real headers. I filed that as a follow-up in docs/civics.md rather than pretending it's nailed down.

The thing that actually ate the most time was respx: url__regex and path__regex silently refused to match in this env, and - the real trap - @respx.mock(assert_all_called=False) creates a local router while respx.get(...) registers on the global one, so every route read as "not mocked". Threading the injected respx_mock router through fixed it. Worth remembering for the next exporter test.

The other surprise was traffic: three sibling surfaces (social, world, progression) landed on main while I worked, so I resolved the same additive conflicts three times - render helpers, dispatch blocks, preview routes, and a .chart-legend/.legend-swatch CSS collision I namespaced to civ-. All additive, nothing lost, full suite green after each (453 py + frontend). Confident in the result; the only soft spot is the header guesswork above.

WARD-OUTCOME: done - get_eco_civics MCP tool, /preview/civics.json plane, and the SPA /civics route landed on main (a6848b9), closing #61. This one went smoothly where I expected friction and fought back where I didn't. The aggregation itself was easy - the trade wave left such a clean spine (streamed CSV, `_corrected_index`, the citizens join, Time/86400) that civics.py was mostly a matter of picking the right columns and folding. The genuinely honest caveat: the exact civic CSV headers and the 10 civics/people series names were never captured live (the #7 survey is inventory-only for those families), so I parse a generous candidate-column list and treat the daily series as the same names doubling as datasets. On a live server the turnout/demographic *counts* and the citizen actors are solid; the *subject* labels (election/settlement names) are best-effort until someone probes the real headers. I filed that as a follow-up in docs/civics.md rather than pretending it's nailed down. The thing that actually ate the most time was respx: `url__regex` and `path__regex` silently refused to match in this env, and - the real trap - `@respx.mock(assert_all_called=False)` creates a *local* router while `respx.get(...)` registers on the global one, so every route read as "not mocked". Threading the injected `respx_mock` router through fixed it. Worth remembering for the next exporter test. The other surprise was traffic: three sibling surfaces (social, world, progression) landed on main while I worked, so I resolved the same additive conflicts three times - render helpers, dispatch blocks, preview routes, and a `.chart-legend`/`.legend-swatch` CSS collision I namespaced to `civ-`. All additive, nothing lost, full suite green after each (453 py + frontend). Confident in the result; the only soft spot is the header guesswork above.
Sign in to join this conversation.
No description provided.