Skip to content

Tasks 034 — Client-direct ArenaNet (prices & account off our origin) ​

Execution skill: superpowers:subagent-driven-development — one implementer per task, then a two-stage review (spec compliance, then code quality). superpowers:test-driven-development applies inside every task: no production code before a failing test that demands it. Reach for superpowers:systematic-debugging on any surprise rather than guessing.

Derived from plan.md (approved). Each task is small, independently verifiable, and reviewed as its own diff. Split where a reviewer could reject one task while approving its neighbour — not where the work merely changes subject. A task is done only when it satisfies the definition of done in CLAUDE.md.

Global Constraints in plan.md apply to every task and are not repeated per task.

Build-window note. T1–T3 are behaviour-preserving moves and keep the whole monorepo green. T4 deletes the proxy routes (api stays green — the services are kept). T5 regenerates the web client, which breaks the web build/tests (the three account hooks lose their generated client); the web stays red through T6 and is restored by T7 (account hooks) and enhanced by T8 (priced tree). T4–T8 are the migration and must all land before the branch is merge-ready; the reviewer gates each diff on its own tests, not on a green web build mid-migration.


T1 — Move the pricing math into @gw2priory/recipe-graph ​

Satisfies: R3, SC6 (single priceGraph), and relocates the P1 #3/P1 #4 pricing tests.

  • [ ] RED: create packages/recipe-graph/src/pricing.ts and project-tree.ts by moving the bodies of apps/api/src/recipe-graph/pricing.ts and project-tree.ts verbatim (they import only package types + @gw2priory/domain's netSellPrice). Move their tests to packages/recipe-graph/src/__tests__/{pricing,project-tree}.test.ts. Export both from packages/recipe-graph/src/index.ts. Run the moved tests — they fail (module not yet resolvable / @gw2priory/domain not yet a dep of the package).
  • [ ] GREEN: add "@gw2priory/domain": "workspace:*" to packages/recipe-graph/package.json dependencies. Update apps/api/src/recipe-graph/recipe-graph.service.ts to import { priceGraph, type PriceMap } from @gw2priory/recipe-graph (was ./pricing). Delete apps/api/src/recipe-graph/{pricing,project-tree}.ts and their old tests; fix any remaining api import of the app-local copies (e.g. recipe-graph.service.bifrost.test.ts) to the package. pnpm install to relink the workspace.
  • [ ] REFACTOR: only with tests green.
  • [ ] Confirm teeth — flip the tie-break in priceGraph (unitBuyPrice <= craftCost → <), watch a pricing "tie → buy" assertion fail, restore.
  • [ ] Commit.

Verified by: packages/recipe-graph pricing/project-tree tests; apps/api resolvePriced + mcp.tools.test.ts green (MCP behaviour unchanged); a grep test finds exactly one export function priceGraph. pnpm --filter @gw2priory/recipe-graph test && pnpm --filter @gw2priory/api test green.


T2 — New @gw2priory/gw2 package: shared GW2 wire schemas (api re-exports) ​

Satisfies: R13 (schema half), SC6 (single wire-schema definition).

  • [ ] RED: create packages/gw2/ — package.json and tsconfig.json mirroring packages/domain (same type: module, exports: "./src/index.ts", same build/lint wiring so the SWC api can runtime-load it — research.md V5/F12). Move the web-needed schemas from apps/api/src/gw2/gw2.schemas.ts into packages/gw2/src/schemas.ts: Gw2ItemSchema, Gw2AccountSchema, Gw2PriceSchema, Gw2MaterialSchema, Gw2MaterialCategorySchema, Gw2WalletEntrySchema, Gw2CurrencySchema and their inferred types. Barrel them in packages/gw2/src/index.ts. Add a packages/gw2/src/__tests__/schemas.test.ts parsing a real ArenaNet price row and a raw material row. Run it — fails (package not linked yet).
  • [ ] GREEN: add "@gw2priory/gw2": "workspace:*" to apps/api/package.json. In apps/api/src/gw2/gw2.schemas.ts, re-export the moved schemas/types from @gw2priory/gw2 (export { Gw2PriceSchema, type Gw2Price, … } from '@gw2priory/gw2') and keep the api-only schemas (Gw2BuildSchema, Gw2ItemSlotSchema, Gw2CharacterInventorySchema, Gw2RecipeSchema, Gw2RecipeSearchSchema, parseOrThrow) local — so every existing from './gw2.schemas' import in apps/api keeps resolving unchanged. pnpm install.
  • [ ] Confirm the api runtime-loads it: pnpm --filter @gw2priory/api build succeeds and a smoke test (or existing gw2-client test) parses a price through the re-exported schema.
  • [ ] Confirm teeth — tighten Gw2PriceSchema to require a nonexistent field, watch the schema test fail, restore.
  • [ ] Commit.

Verified by: packages/gw2 schema test; apps/api full test + build green (re-export + runtime load work); a grep test finds one definition of each moved schema.


T3 — Shared enrichment joins in @gw2priory/gw2; AccountService delegates ​

Satisfies: R14.

  • [ ] RED: create packages/gw2/src/enrich.ts with the pure functions moved from AccountService: enrichMaterials(rows, items, categories, prices): Material[] (the item/category join + sellPrice fold + category-order sort from account.service.ts:19-66) and enrichWallet(rows, currencies): WalletEntry[] (from :94-112), plus the Material/WalletEntry output types (moved from apps/api/src/account/account.schema.ts). Move the join assertions from account.service.test.ts into packages/gw2/src/__tests__/enrich.test.ts (sort order, sellPrice fold, omit rows with no item/category, currency join). Run — fails (not linked).
  • [ ] GREEN: export the joins + types from packages/gw2/src/index.ts. In apps/api/src/account/account.service.ts, getMaterials fetches the rows/items/categories/prices as today then returns enrichMaterials(...); getWallet returns enrichWallet(...). In account.schema.ts, re-export Material/WalletEntry from @gw2priory/gw2 (keep the DTO classes for now — deleted in T4). Update mcp.shape.ts/mcp.tools.ts type imports if the path changed. Refocus account.service.test.ts to "delegates to the shared join" (stub gw2, assert the service returns the join's output).
  • [ ] Confirm teeth — break the category sort in enrichMaterials, watch the moved order assertion fail, restore.
  • [ ] Commit.

Verified by: packages/gw2 enrich.test.ts; apps/api account.service.test.ts + mcp.* tests green (MCP priory_account_* behaviour unchanged). pnpm --filter @gw2priory/api test.


T4 — Delete the web-facing proxies; regenerate the OpenAPI ​

Satisfies: R6, R8, R9, P2 #4, SC5, SC8 (api half). Build window: api green; web green until T5.

  • [ ] RED: extend apps/api/src/generate-openapi.test.ts to assert the generated document has no/account, /account/materials, /account/wallet, or /commerce/prices path; add a grep/fs guard test asserting account.controller.ts and the commerce/ directory are gone. Watch both fail.
  • [ ] GREEN: delete apps/api/src/commerce/** (controller, service, module, schema, tests) and apps/api/src/account/account.controller.ts + its test. In account.module.ts drop controllers: [AccountController] (keep providers/exports: [AccountService]). In app.module.ts remove CommerceModule from imports. In account.schema.ts delete the now-unused DTO classes (AccountResponseDto, MaterialListDto, WalletListDto) after grepping nothing else imports them. Correct the stale spec 033→034 comment in recipe-graph.controller.ts:28-29 (R9). Regenerate: pnpm --filter @gw2priory/api build && pnpm --filter @gw2priory/api generate:openapi (writes apps/api/openapi.json). Update generate-openapi.test.ts snapshots/assertions to the smaller doc.
  • [ ] Confirm teeth — restore CommerceModule in app.module.ts, watch the "no /commerce/prices path" assertion fail, remove again.
  • [ ] Commit (including the regenerated apps/api/openapi.json).

Verified by: generate-openapi.test.ts + the controller-absence grep; pnpm --filter @gw2priory/api test && build green; all MCP tests green (capability retained). Web unaffected until T5.


T5 — Regenerate the web API client (drops account & commerce) ​

Satisfies: R12, SC8 (web half). Build window: web goes red here (through T6), restored T7.

  • [ ] Run pnpm --filter @gw2priory/web generate:api (orval, apps/web/orval.config.ts) against the apps/api/openapi.json regenerated in T4. This deletes apps/web/src/api/generated/endpoints/{account,commerce} and their zod/models.
  • [ ] Verify git diff touches only apps/web/src/api/generated/**; the account/commerce clients are gone and the recipe-graph/legendaries clients remain.
  • [ ] Commit (generated files only — no hand edits).

Verified by: pnpm verify:contract reports openapi.json + generated/ diff-clean; the generated tree no longer contains account/commerce. (apps/web typecheck/tests are expected red — the three account hooks still import the removed clients; fixed in T7.)


T6 — Web browser→ArenaNet fetch layer ​

Satisfies: R1 (fetch half), R10; foundation for R4/P1 #2/SC4.

  • [ ] RED: create apps/web/src/shared/lib/gw2/__tests__/client.test.ts (MSW, base https://api.guildwars2.com/v2): fetchPrices of 250 ids issues 2 requests of ≤199 ids and reassembles (SC4/P1 #2); each fetcher validates with the @gw2priory/gw2 schema and throws on a malformed row; fetchAccountMaterials/fetchAccountWallet/fetchAccount put the key in ?access_token= (never a header — research.md V1) and hit the ArenaNet host. Watch it fail.
  • [ ] GREEN: add "@gw2priory/gw2": "workspace:*" to apps/web/package.json. Create apps/web/src/shared/lib/gw2/client.ts: const GW2_API_BASE = 'https://api.guildwars2.com/v2', a chunk(ids, 199) helper, and fetchers — fetchPrices(ids), fetchItems(ids), fetchMaterialCategories(), fetchCurrencies(ids) (keyless), and fetchAccount(key), fetchAccountMaterials(key), fetchAccountWallet(key) (append ?access_token=<key>). Each parses its response with the matching @gw2priory/gw2 schema. No bespoke cache (R10).
  • [ ] Confirm teeth — set the batch size to 999, watch the "2 requests" assertion fail, restore to 199.
  • [ ] Commit.

Verified by: gw2/__tests__/client.test.ts green. (apps/web overall still red until T7.)


T7 — Rewire the account hooks browser-direct + shared joins ​

Satisfies: R4, R5 (account half), R14 (web half), P2 #1, P2 #2, P2 #3, SC1/SC2 (account half). Restores the web build.

  • [ ] RED: rewrite apps/web/src/api/__tests__/{useMaterials,useWallet,useAccount}.test.tsx (MSW): the hook calls api.guildwars2.com/v2/account* with ?access_token=, our origin receives no request and no key (SC1/SC2/P2 #1/P2 #2); rendered rows equal enrichMaterials/enrichWallet output; an ArenaNet 401 surfaces through the existing connect/error boundary (P2 #3). Watch fail (hooks still import the removed generated client).
  • [ ] GREEN: rewrite useMaterials (fetchAccountMaterials + fetchItems + fetchMaterialCategories + fetchPrices → enrichMaterials), useWallet (fetchAccountWallet + fetchCurrencies → enrichWallet), useAccount (fetchAccount), each with queryKey: [..., hashKey(apiKey)] (raw key never in the key — research.md V1). Update apps/web/src/api/index.ts facade exports and any materials/wallet feature import that referenced the old generated types.
  • [ ] Confirm teeth — assert in one test that a request reaches our origin, watch it fail (nothing does), restore to the "no origin request" assertion.
  • [ ] Commit.

Verified by: the three hook tests + features/materials / features/wallet tests; pnpm --filter @gw2priory/web test green for these. Web build compiles again.


T8 — Rewire useRecipeTree: fetch prices + priceGraph; restore cost/profit UI ​

Satisfies: R1/R2 (merge), R5 (price half), R9 (web comment), P1 #1/#2/#3/#4, SC1/SC2/SC3/SC4 (price half), SC6.

  • [ ] RED: rewrite apps/web/src/api/__tests__/useRecipeTree.test.tsx (MSW): the hook fetches the priceless graph (our origin, keyless) and prices from api.guildwars2.com/v2/commerce/prices, with no /commerce/* request to our origin (P1 #1/SC1); >199 priceable ids batch to ≤199 (P1 #2/SC4); the returned tree's per-node fields + root summary equal priceGraph(graph, priceMap) for a fixture (P1 #3/SC3); ids omitted from the price response resolve to unknown/null (P1 #4). Restore cost/decision/profit assertions in the features/legendaries detail tests + fixtures.ts. Watch fail.
  • [ ] GREEN: rewrite useRecipeTree.ts — fetch the graph, fetchPrices(collectPricedIds(graph)), build the PriceMap, return priceGraph(graph, priceMap) (PricedTree from @gw2priory/recipe-graph); delete the local project()/projectGraph() and the local RecipeTree type. Restore the cost/decision/profit UI in LegendaryTree.tsx, ShoppingList.tsx, LegendarySummary.tsx (the UI spec 032 removed), consuming PricedTree. Fix the stale spec 033→034 comment in useRecipeTree.ts:16-17. Leave ranking (useLegendaryRanking, RankingPage) untouched (grep).
  • [ ] Confirm teeth — return an empty PriceMap, watch a "profit renders" assertion fall to unknown/null, restore.
  • [ ] Commit.

Verified by: useRecipeTree.test.tsx + the legendary-detail tests; pnpm --filter @gw2priory/web test && pnpm --filter @gw2priory/web build green (the React-compiler gate runs only in build — pnpm build, not just vitest). Whole monorepo green again.


T9 — Park the doc-supersession findings in docs/gaps/ ​

Satisfies: the spec's Parked findings.

  • [ ] Create docs/gaps/034-key-transport-and-shared-gw2.md: (1) stack.md's "key sent to the api as Authorization: Bearer" is superseded for prices/account by browser→ArenaNet ?access_token= (per this spec / the epic); (2) gw2-api.md should gain the CORS/transport facts from research.md (header CORS-blocked, per-IP limit, body readable); (3) the "types-only @gw2priory/recipe-graph" comments are stale now that both apps runtime-import it. Records, not proposals — deferred to a later doc pass; link spec 034 and research.md.
  • [ ] Commit.

Verified by: file exists; pnpm docs:build green (VitePress compiles it).


T10 — Verify end to end; traceability; status → implemented; PR ​

Satisfies: SC7, the CLAUDE.md definition of done.

  • [ ] Run the full gate from the repo root: pnpm lint && pnpm test && pnpm build && pnpm docs:build && pnpm verify:contract. All green.
  • [ ] Run the app (run skill / pnpm dev), connect a key, open a legendary detail page + the materials and wallet pages: prices/costs and account data render; in the browser network panel, the commerce/prices and account* calls go to api.guildwars2.com, nothing account/commerce hits our origin, and no request to our origin carries the key. Record the observation (SC1/SC2).
  • [ ] Fill the spec.md Traceability table — every scenario/criterion → the real test name from T1–T8.
  • [ ] On the human's decision, transcribe spec.md status approved → implemented (in-branch, part of the PR diff) — and say it was the human's call, not the agent's.
  • [ ] Commit; open the PR (034-client-direct-anet → main) via superpowers:finishing-a-development-branch, first running superpowers:requesting-code-review.

Verified by: the full gate green; the running app + network observation; the completed traceability table; code review on the branch before merge.


Notes ​

Staging area for decisions and surprises found during implementation — including anything that turned out differently from what plan.md assumed. Move each one into spec.md, research.md, or docs/ before closing the feature; this section is not a home.

  • If the new @gw2priory/gw2 package fails to runtime-load in the SWC api build (the @gw2priory/legendary-recipes F12 failure mode), diff its package.json/tsconfig.json against @gw2priory/domain — which api already runtime-loads — rather than adding a build workaround.
  • If MSW does not intercept api.guildwars2.com in the web test setup, register the ArenaNet base in the shared test server (apps/web test setup) — do not fall back to mocking fetch per test.