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.tsandproject-tree.tsby moving the bodies ofapps/api/src/recipe-graph/pricing.tsandproject-tree.tsverbatim (they import only package types +@gw2priory/domain'snetSellPrice). Move their tests topackages/recipe-graph/src/__tests__/{pricing,project-tree}.test.ts. Export both frompackages/recipe-graph/src/index.ts. Run the moved tests — they fail (module not yet resolvable /@gw2priory/domainnot yet a dep of the package). - [ ] GREEN: add
"@gw2priory/domain": "workspace:*"topackages/recipe-graph/package.jsondependencies. Updateapps/api/src/recipe-graph/recipe-graph.service.tsto import{ priceGraph, type PriceMap }from@gw2priory/recipe-graph(was./pricing). Deleteapps/api/src/recipe-graph/{pricing,project-tree}.tsand 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 installto 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.jsonandtsconfig.jsonmirroringpackages/domain(sametype: module,exports: "./src/index.ts", same build/lint wiring so the SWC api can runtime-load it —research.mdV5/F12). Move the web-needed schemas fromapps/api/src/gw2/gw2.schemas.tsintopackages/gw2/src/schemas.ts:Gw2ItemSchema,Gw2AccountSchema,Gw2PriceSchema,Gw2MaterialSchema,Gw2MaterialCategorySchema,Gw2WalletEntrySchema,Gw2CurrencySchemaand their inferred types. Barrel them inpackages/gw2/src/index.ts. Add apackages/gw2/src/__tests__/schemas.test.tsparsing a real ArenaNet price row and a raw material row. Run it — fails (package not linked yet). - [ ] GREEN: add
"@gw2priory/gw2": "workspace:*"toapps/api/package.json. Inapps/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 existingfrom './gw2.schemas'import inapps/apikeeps resolving unchanged.pnpm install. - [ ] Confirm the api runtime-loads it:
pnpm --filter @gw2priory/api buildsucceeds and a smoke test (or existinggw2-clienttest) parses a price through the re-exported schema. - [ ] Confirm teeth — tighten
Gw2PriceSchemato 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.tswith the pure functions moved fromAccountService:enrichMaterials(rows, items, categories, prices): Material[](the item/category join +sellPricefold + category-order sort fromaccount.service.ts:19-66) andenrichWallet(rows, currencies): WalletEntry[](from:94-112), plus theMaterial/WalletEntryoutput types (moved fromapps/api/src/account/account.schema.ts). Move the join assertions fromaccount.service.test.tsintopackages/gw2/src/__tests__/enrich.test.ts(sort order,sellPricefold, omit rows with no item/category, currency join). Run — fails (not linked). - [ ] GREEN: export the joins + types from
packages/gw2/src/index.ts. Inapps/api/src/account/account.service.ts,getMaterialsfetches the rows/items/categories/prices as today then returnsenrichMaterials(...);getWalletreturnsenrichWallet(...). Inaccount.schema.ts, re-exportMaterial/WalletEntryfrom@gw2priory/gw2(keep the DTO classes for now — deleted in T4). Updatemcp.shape.ts/mcp.tools.tstype imports if the path changed. Refocusaccount.service.test.tsto "delegates to the shared join" (stubgw2, 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.tsto assert the generated document has no/account,/account/materials,/account/wallet, or/commerce/pricespath; add a grep/fsguard test assertingaccount.controller.tsand thecommerce/directory are gone. Watch both fail. - [ ] GREEN: delete
apps/api/src/commerce/**(controller, service, module, schema, tests) andapps/api/src/account/account.controller.ts+ its test. Inaccount.module.tsdropcontrollers: [AccountController](keepproviders/exports: [AccountService]). Inapp.module.tsremoveCommerceModulefromimports. Inaccount.schema.tsdelete the now-unused DTO classes (AccountResponseDto,MaterialListDto,WalletListDto) after grepping nothing else imports them. Correct the stalespec 033→034comment inrecipe-graph.controller.ts:28-29(R9). Regenerate:pnpm --filter @gw2priory/api build && pnpm --filter @gw2priory/api generate:openapi(writesapps/api/openapi.json). Updategenerate-openapi.test.tssnapshots/assertions to the smaller doc. - [ ] Confirm teeth — restore
CommerceModuleinapp.module.ts, watch the "no/commerce/pricespath" 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 theapps/api/openapi.jsonregenerated in T4. This deletesapps/web/src/api/generated/endpoints/{account,commerce}and their zod/models. - [ ] Verify
git difftouches onlyapps/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, basehttps://api.guildwars2.com/v2):fetchPricesof 250 ids issues 2 requests of ≤199 ids and reassembles (SC4/P1 #2); each fetcher validates with the@gw2priory/gw2schema and throws on a malformed row;fetchAccountMaterials/fetchAccountWallet/fetchAccountput the key in?access_token=(never a header —research.mdV1) and hit the ArenaNet host. Watch it fail. - [ ] GREEN: add
"@gw2priory/gw2": "workspace:*"toapps/web/package.json. Createapps/web/src/shared/lib/gw2/client.ts:const GW2_API_BASE = 'https://api.guildwars2.com/v2', achunk(ids, 199)helper, and fetchers —fetchPrices(ids),fetchItems(ids),fetchMaterialCategories(),fetchCurrencies(ids)(keyless), andfetchAccount(key),fetchAccountMaterials(key),fetchAccountWallet(key)(append?access_token=<key>). Each parses its response with the matching@gw2priory/gw2schema. 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 callsapi.guildwars2.com/v2/account*with?access_token=, our origin receives no request and no key (SC1/SC2/P2 #1/P2 #2); rendered rows equalenrichMaterials/enrichWalletoutput; an ArenaNet401surfaces 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 withqueryKey: [..., hashKey(apiKey)](raw key never in the key —research.mdV1). Updateapps/web/src/api/index.tsfacade 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 fromapi.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 + rootsummaryequalpriceGraph(graph, priceMap)for a fixture (P1 #3/SC3); ids omitted from the price response resolve tounknown/null(P1 #4). Restore cost/decision/profit assertions in thefeatures/legendariesdetail tests +fixtures.ts. Watch fail. - [ ] GREEN: rewrite
useRecipeTree.ts— fetch the graph,fetchPrices(collectPricedIds(graph)), build thePriceMap, returnpriceGraph(graph, priceMap)(PricedTreefrom@gw2priory/recipe-graph); delete the localproject()/projectGraph()and the localRecipeTreetype. Restore the cost/decision/profit UI inLegendaryTree.tsx,ShoppingList.tsx,LegendarySummary.tsx(the UI spec 032 removed), consumingPricedTree. Fix the stalespec 033→034comment inuseRecipeTree.ts:16-17. Leave ranking (useLegendaryRanking,RankingPage) untouched (grep). - [ ] Confirm teeth — return an empty
PriceMap, watch a "profit renders" assertion fall tounknown/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 asAuthorization: Bearer" is superseded for prices/account by browser→ArenaNet?access_token=(per this spec / the epic); (2)gw2-api.mdshould gain the CORS/transport facts fromresearch.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 andresearch.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 (
runskill /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, thecommerce/pricesandaccount*calls go toapi.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.mdTraceability table — every scenario/criterion → the real test name from T1–T8. - [ ] On the human's decision, transcribe
spec.mdstatusapproved→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) viasuperpowers:finishing-a-development-branch, first runningsuperpowers: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/gw2package fails to runtime-load in the SWC api build (the@gw2priory/legendary-recipesF12 failure mode), diff itspackage.json/tsconfig.jsonagainst@gw2priory/domain— which api already runtime-loads — rather than adding a build workaround. - If MSW does not intercept
api.guildwars2.comin the web test setup, register the ArenaNet base in the shared test server (apps/webtest setup) — do not fall back to mockingfetchper test.