Trade & store logistics: store & trader directory (surfaces store owner, a DiscordLink gap) #50

Closed
opened 2026-07-05 01:22:34 +00:00 by coilysiren · 3 comments
Owner

Context

Part of the trade & store logistics push under the DiscordLink-replacement epic (#37), on the trades-ledger foundation (#6). DiscordLink's Trades <name> lookup resolves a search term to a store or player and lists that store's offers, but it does not show the store owner (confirmed gap in its FormatTrades). eco-app can do the join and exceed it.

No-reset spine

Everything here derives from the CurrencyTrade exporter the server already ships, joined to names via the jobs mod's /api/v1/citizens surface (the crafting atlas already does this id->name join). No new C# mod, no server reset. The ShopOwner, Buyer, Seller, and ActionLocation columns give store identity, participants, and place. Consume #6's normalized model if it has landed.

Scope

The agent builds store and trader directories from trade history:

  • Store profiles - keyed by store (ActionLocation + ShopOwner): items traded, buy vs sell mix, price points, total volume and value, unique counterparties, and recency (last trade). Resolve ShopOwner to a citizen name, falling back to Citizen #<id> like the atlas does.
  • Trader profiles - per citizen: what they buy and sell, top items, volume, and the stores they operate. This is the history-derived answer to Trades <player>.
  • Surfaces - an MCP tool (get_eco_stores) returning text + structured JSON + an _meta.ui card, plus /preview/stores.json for the SPA data plane.

Meets Trades <player> / Trades <store> from history and exceeds by surfacing the store owner and per-trader/per-store volume and recency that DiscordLink omits. The one thing history cannot give is the current shelf snapshot (what is listed right now) - that is the reset-gated stores exporter (sibling issue), which this directory upgrades to live once it lands.

Acceptance

  • MCP tool registered, ward exec smoke passes.
  • Unknown-id and empty-history paths degrade gracefully (owner shows Citizen #<id>).
  • respx-mocked unit tests for the aggregation and the citizen join.
  • ward exec test / ward exec lint green. docs/FEATURES.md updated.

Dependencies

Foundation: #6 and the jobs /api/v1/citizens join. Parent: #37. The logistics engine (sibling) consumes these profiles.

## Context Part of the trade & store logistics push under the DiscordLink-replacement epic ([#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37)), on the trades-ledger foundation ([#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)). DiscordLink's `Trades <name>` lookup resolves a search term to a **store** or **player** and lists that store's offers, but it **does not show the store owner** (confirmed gap in its `FormatTrades`). eco-app can do the join and exceed it. ## No-reset spine Everything here derives from the `CurrencyTrade` exporter the server **already ships**, joined to names via the jobs mod's `/api/v1/citizens` surface (the crafting atlas already does this id->name join). No new C# mod, no server reset. The `ShopOwner`, `Buyer`, `Seller`, and `ActionLocation` columns give store identity, participants, and place. Consume [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)'s normalized model if it has landed. ## Scope The agent builds **store and trader directories** from trade history: * **Store profiles** - keyed by store (`ActionLocation` + `ShopOwner`): items traded, buy vs sell mix, price points, total volume and value, unique counterparties, and recency (last trade). Resolve `ShopOwner` to a citizen name, falling back to `Citizen #<id>` like the atlas does. * **Trader profiles** - per citizen: what they buy and sell, top items, volume, and the stores they operate. This is the history-derived answer to `Trades <player>`. * **Surfaces** - an MCP tool (`get_eco_stores`) returning text + structured JSON + an `_meta.ui` card, plus `/preview/stores.json` for the SPA data plane. ## DiscordLink parity **Meets** `Trades <player>` / `Trades <store>` from history and **exceeds** by surfacing the store owner and per-trader/per-store volume and recency that DiscordLink omits. The one thing history cannot give is the current shelf snapshot (what is listed right now) - that is the reset-gated stores exporter (sibling issue), which this directory upgrades to live once it lands. ## Acceptance * MCP tool registered, `ward exec smoke` passes. * Unknown-id and empty-history paths degrade gracefully (owner shows `Citizen #<id>`). * `respx`-mocked unit tests for the aggregation and the citizen join. * `ward exec test` / `ward exec lint` green. `docs/FEATURES.md` updated. ## Dependencies Foundation: [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6) and the jobs `/api/v1/citizens` join. Parent: [#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37). The logistics engine (sibling) consumes these profiles.
Author
Owner

The live store-offer exporter mod landed in #55 (`mods/stores`, `GET /api/v1/stores`). The DTO this directory upgrades to for shelf-accurate owner/currency/location is documented here: mods/stores/docs/dto.md. A C# shell harness serves the same shape on :5101 (ward exec run-shell-stores) so the live path can be wired with no server restart.

The live store-offer exporter mod landed in #55 (\`mods/stores\`, \`GET /api/v1/stores\`). The DTO this directory upgrades to for shelf-accurate owner/currency/location is documented here: [mods/stores/docs/dto.md](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/src/branch/main/mods/stores/docs/dto.md). A C# shell harness serves the same shape on :5101 (`ward exec run-shell-stores`) so the live path can be wired with no server restart.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-50 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-05T03:27:15Z). 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-50` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-05T03:27:15Z). 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_stores MCP tool + /preview/stores.json land store & trader directories from trade history, surfacing the store owner DiscordLink omits.

The shape of this one fell out cleanly because #6 did the hard part: the CurrencyTrade parser, the citizen id->name join, and the defensive realignment were all already there. The main design call was resisting the urge to re-fetch — I pulled the ledger's fetch loop up into a shared trades.fetch_parsed_trades spine so the ledger and this directory fold the same streamed rows once, rather than hitting the exporter twice. That refactor was the only change to landed code, and the existing trades tests passing unchanged gave me confidence it was behavior-preserving.

The one thing I second-guessed: buy-vs-sell mix. The obvious source is the BoughtOrSold enum, but #6 flagged it as an undecoded guess. Reading the split structurally instead — owner-as-seller is a sale, owner-as-buyer a purchase, straight off the party columns — is both more robust and needs no decode, so a wrong enum polarity can never corrupt the mix. That felt like the right call and made the tests cleaner too.

Confident in the aggregation and the graceful-degradation paths (empty history, unknown ids, partial CSV failure all covered). Rougher edges / follow-ups worth filing: (1) no dedicated SPA page yet — I shipped the /preview/stores.json data plane matching the #53 currency precedent, but a /stores React route is a natural next step; (2) this is history-only, and the live shelf exporter already landed in #55 (GET /api/v1/stores) — wiring these profiles to that DTO for shelf-accurate current-offer data is the obvious upgrade and the reason I kept the store keying on owner+location so the join is easy.

WARD-OUTCOME: done - get_eco_stores MCP tool + /preview/stores.json land store & trader directories from trade history, surfacing the store owner DiscordLink omits. The shape of this one fell out cleanly because #6 did the hard part: the CurrencyTrade parser, the citizen id->name join, and the defensive realignment were all already there. The main design call was resisting the urge to re-fetch — I pulled the ledger's fetch loop up into a shared `trades.fetch_parsed_trades` spine so the ledger and this directory fold the same streamed rows once, rather than hitting the exporter twice. That refactor was the only change to landed code, and the existing trades tests passing unchanged gave me confidence it was behavior-preserving. The one thing I second-guessed: buy-vs-sell mix. The obvious source is the `BoughtOrSold` enum, but #6 flagged it as an undecoded guess. Reading the split structurally instead — owner-as-seller is a sale, owner-as-buyer a purchase, straight off the party columns — is both more robust and needs no decode, so a wrong enum polarity can never corrupt the mix. That felt like the right call and made the tests cleaner too. Confident in the aggregation and the graceful-degradation paths (empty history, unknown ids, partial CSV failure all covered). Rougher edges / follow-ups worth filing: (1) no dedicated SPA page yet — I shipped the `/preview/stores.json` data plane matching the #53 currency precedent, but a `/stores` React route is a natural next step; (2) this is history-only, and the live shelf exporter already landed in #55 (`GET /api/v1/stores`) — wiring these profiles to that DTO for shelf-accurate current-offer data is the obvious upgrade and the reason I kept the store keying on owner+location so the join is easy.
Sign in to join this conversation.
No description provided.