Skip to content

Tasks 006 — Static data module ​

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.

Commands (run from repo root): pnpm typecheck, pnpm test, pnpm --filter @gw2priory/api build. A new workspace dependency requires pnpm install before it resolves (monorepo.md F3).


T1 — Scaffold packages/legendary-recipes: schema, validated loader, guard test, seed data ​

Satisfies: R1, R2, R3, SC2 (malformed-fails half), F12 (JSON-at-runtime shape).

Files:

  • Create: packages/legendary-recipes/package.json — private, type: module, exports { ".": "./src/index.ts", "./data": "./data/gen1-weapons.json" }, deps zod + @gw2priory/domain: workspace:*. Mirrors packages/domain/package.json.
  • Create: packages/legendary-recipes/src/schema.ts — IngredientSchema (discriminatedUnion('kind', [item, currency])), MethodSchema (enum(['mystic-forge','discipline','vendor','collection'])), CuratedRecipeSchema, DatasetSchema; exported types CuratedRecipe, Ingredient, Dataset (ids typed ItemId from @gw2priory/domain).
  • Create: packages/legendary-recipes/data/gen1-weapons.json — seed only: { "legendaryOutputIds": [30698], "recipes": [ … ] } with The Bifrost (30698) final assembly + the shared gifts (Gift of Fortune 19626, Gift of Mastery 19674, Gift of Magic, Gift of Might, Gift of the Bifrost), each method: "mystic-forge", item ingredients, exact counts from research V1, a wiki source.
  • Create: packages/legendary-recipes/src/index.ts — re-export schema/types; loadDataset(): Dataset imports ../data/gen1-weapons.json and returns DatasetSchema.parse(json).
  • Create: packages/legendary-recipes/src/index.test.ts — the guard test (this task's half).
  • Modify: apps/api/package.json — add "@gw2priory/legendary-recipes": "workspace:*", then pnpm install.

Interfaces produced: loadDataset(): Dataset; types CuratedRecipe, Ingredient, Dataset; data subpath @gw2priory/legendary-recipes/data.

  • [ ] RED: in index.test.ts, SC2: loadDataset() parses the committed dataset (asserts it returns an object with a non-empty recipes array), and SC2: a structurally invalid entry is rejected — four cases fed straight to DatasetSchema.parse: count: 0, missing source, method: "forge" (not in the enum), and a duplicate outputItemId. Run pnpm test — fails (module absent).
  • [ ] GREEN: write schema.ts, index.ts, the seed gen1-weapons.json, package.json; add the api dep and pnpm install. Duplicate-outputItemId detection needs a .superRefine on DatasetSchema (Zod won't catch it otherwise). Run pnpm test — passes; pnpm typecheck resolves the package.
  • [ ] REFACTOR: only with the test green.
  • [ ] Confirm the test has teeth — set a seed ingredient count to 0, watch SC2 fail, restore.
  • [ ] Commit (specs:/feat: scoped, e.g. feat(legendary-recipes): schema, loader, guard test, seed).

Verified by: packages/legendary-recipes/src/index.test.ts; pnpm typecheck clean.


T2 — Curate the full Gen 1 dataset (all 21) with a completeness guard ​

Satisfies: R9, R11, SC1, P1 #5, P1 #6 (data).

Files:

  • Modify: packages/legendary-recipes/data/gen1-weapons.json — all 21 legendaries (17 land + 3 aquatic + Eternity, plan.md/research V3), every shared + weapon-specific gift, exact wiki-cited quantities, a source URL per recipe; legendaryOutputIds lists all 21 output ids. Eternity's recipe: ingredients: [Sunrise ×1, Twilight ×1, Pile of Crystalline Dust ×5, Philosopher's Stone ×10]. Gen 1 precursors are not recipe outputs (they are buyable leaves, research V2).
  • Modify: packages/legendary-recipes/src/index.test.ts — add completeness assertions.

Curation source: the GW2 Wiki page per weapon (wiki.guildwars2.com/wiki/<name>); read each recipe's exact counts and record the page URL as source. Verified anchors already in research V1/V5 (shared gifts + ids). Per-weapon-gift counts and ids are looked up here — this is the dataset's maintenance point; numeric accuracy beyond structure is a human-review item (plan.md Risks).

  • [ ] RED: extend index.test.ts — P1 #5: legendaryOutputIds has exactly 21 entries; SC1: every legendaryOutputId resolves to a recipe; SC1: each standard legendary's ingredients are Precursor + Gift of Fortune (19626) + Gift of Mastery (19674) + one weapon gift; P1 #6: Eternity's ingredients are Sunrise ×1 + Twilight ×1 + Pile of Crystalline Dust ×5 + Philosopher's Stone ×10; SC1: no Gen 1 precursor id appears as a recipe outputItemId. Run — fails (only Bifrost seeded).
  • [ ] GREEN: fill gen1-weapons.json with all 21 + gifts + legendaryOutputIds. Run — passes.
  • [ ] REFACTOR: only with the test green.
  • [ ] Confirm teeth — drop one id from legendaryOutputIds (or delete its recipe), watch SC1/P1 #5 fail, restore.
  • [ ] Commit (feat(legendary-recipes): curate all 21 Gen 1 legendaries).

Verified by: the completeness tests in packages/legendary-recipes/src/index.test.ts.


T3 — CuratedRecipeService ​

Satisfies: R4, P1 #1, P1 #2, P1 #3, P1 #4, P1 #6, SC4 (curated half).

Files:

  • Create: apps/api/src/static-data/curated-recipe.service.ts — @Injectable() CuratedRecipeService. Runtime value import import dataset from '@gw2priory/legendary-recipes/data' (JSON subpath, F12) + type-only import type { Dataset, CuratedRecipe } from '@gw2priory/legendary-recipes'. In the constructor, const data = dataset as Dataset — a single assertion carrying a comment: the JSON is validated against DatasetSchema by the T1/T2 guard test in CI, so no runtime parse runs here. Build a Map<number, CuratedRecipe> by outputItemId and a Set<number> of legendaryOutputIds.
  • Create: apps/api/src/static-data/curated-recipe.service.test.ts — instantiate directly against the real package (it's pure data, no mock).

Interfaces produced: getRecipe(id: ItemId): CuratedRecipe | null; legendaries(): CuratedRecipe[].

  • [ ] RED: P1 #1: getRecipe(30698) expands to precursor + Gift of Fortune + Gift of Mastery + weapon gift; P1 #2: getRecipe(19626) returns exact counts (77 Clover, 250 Ecto, Gift of Magic, Gift of Might); P1 #3 / SC4: getRecipe(29180) and getRecipe(<unknown id>) return null; P1 #6: getRecipe(<Eternity id>) returns Sunrise + Twilight + Pile of Crystalline Dust ×5 + Philosopher's Stone ×10; P1 #4: legendaries() returns 21 recipes, all legendary outputs. Run — fails (service absent).
  • [ ] GREEN: implement the service as above. Run — passes.
  • [ ] REFACTOR: only with the test green.
  • [ ] Confirm teeth — make getRecipe look up the wrong key (e.g. outputCount), watch P1 #1 fail, restore.
  • [ ] Commit (feat(api): CuratedRecipeService over the curated dataset).

Verified by: apps/api/src/static-data/curated-recipe.service.test.ts.


T4 — ItemDataService (metadata + classification) ​

Satisfies: R5, R10, P2 #1, P2 #2, P2 #3, SC3.

Files:

  • Create: apps/api/src/static-data/item-data.service.ts — @Injectable() ItemDataService, ctor injects Gw2Service. interface ItemMeta { id: ItemId; name; type; rarity; flags: string[]; vendorValue }. metadata(ids) maps Gw2Item → ItemMeta (vendor_value → vendorValue), missing ids omitted. classify(item: { flags: string[] }) returns item.flags.includes('AccountBound') ? 'gated' : 'buyable'.
  • Create: apps/api/src/static-data/item-data.service.test.ts — Nest Test.createTestingModule with .overrideProvider(Gw2Service).useValue({ items: async () => […] }) returning payloads shaped like the 005 __fixtures__/items.json, using the real gated-input flags captured in research V4.

Interfaces consumed: Gw2Service.items(ids: number[]): Promise<Gw2Item[]>. Interfaces produced: metadata(ids: ItemId[]): Promise<ItemMeta[]>; classify(item): 'buyable' | 'gated'.

  • [ ] RED: SC3 / P2 #1: classify() labels each of the five gated inputs (flags incl. AccountBound) as 'gated' (Gift of Exploration 19677, Gift of Battle 19678, Bloodstone Shard 20797, Obsidian Shard 19925, Mystic Clover 19675); P2 #2: classify(Ecto flags []) → 'buyable'; and a regression guard SC3: classify(The Legend flags [SoulBindOnUse, NoSell, …] without AccountBound) → 'buyable'; P2 #3: metadata([present, missing]) omits the missing id. Run — fails (service absent).
  • [ ] GREEN: implement ItemDataService. Run — passes.
  • [ ] REFACTOR: only with the test green.
  • [ ] Confirm teeth — change classify to also gate on AccountBindOnUse, watch the The-Legend guard fail, restore to AccountBound-only.
  • [ ] Commit (feat(api): ItemDataService metadata + AccountBound classifier).

Verified by: apps/api/src/static-data/item-data.service.test.ts.


T5 — StationDataService (all station recipes for an output) ​

Satisfies: R6, P3 #1, P3 #2, P3 #3, SC4 (station half), SC5.

Files:

  • Create: apps/api/src/static-data/station-data.service.ts — @Injectable() StationDataService, ctor injects Gw2Service. interface StationRecipe { outputItemId: ItemId; outputCount: number; ingredients: { itemId: ItemId; count: number }[]; disciplines: string[]; minRating: number }. getRecipes(outputItemId): const ids = await gw2.searchRecipes({ output: outputItemId }); if (ids.length === 0) return []; const recipes = await gw2.recipes(ids); return recipes.map(toStationRecipe) (output_item_id → outputItemId, output_item_count → outputCount, min_rating → minRating, ingredients[].item_id → itemId).
  • Create: apps/api/src/static-data/station-data.service.test.ts — mock Gw2Service.searchRecipes + .recipes via .overrideProvider, using 005 __fixtures__ shapes plus a two-recipe fixture for output 46742 (Bolt of Damask).

Interfaces consumed: Gw2Service.searchRecipes({ output }): Promise<number[]>; Gw2Service.recipes(ids: number[]): Promise<Gw2Recipe[]>. Interfaces produced: getRecipes(outputItemId: ItemId): Promise<StationRecipe[]>.

  • [ ] RED: P3 #1: a single-recipe output returns a one-element array with mapped fields; P3 #2: output 46742 (search → [a, b]) returns both recipes, none dropped; P3 #3 / SC4 / SC5: an output whose search returns [] yields []. Run — fails (service absent).
  • [ ] GREEN: implement StationDataService. Run — passes.
  • [ ] REFACTOR: only with the test green.
  • [ ] Confirm teeth — make getRecipes return [recipes[0]], watch P3 #2 fail, restore to all.
  • [ ] Commit (feat(api): StationDataService returning all recipes per output).

Verified by: apps/api/src/static-data/station-data.service.test.ts.


T6 — StaticDataModule and app wiring ​

Satisfies: R7, SC6 (by construction — all artifacts live under specs/006-static-data/).

Files:

  • Create: apps/api/src/static-data/static-data.module.ts — @Module({ imports: [Gw2Module], providers: [CuratedRecipeService, ItemDataService, StationDataService], exports: [CuratedRecipeService, ItemDataService, StationDataService] }).
  • Create: apps/api/src/static-data/static-data.module.test.ts — compile the module and assert each service resolves as a singleton (mirrors apps/api/src/gw2/gw2.service.test.ts).
  • Modify: apps/api/src/app.module.ts — add StaticDataModule to imports.

Interfaces consumed: Gw2Module (exports Gw2Service); the three services from T3–T5.

  • [ ] RED: R7: StaticDataModule compiles and resolves CuratedRecipeService, ItemDataService, StationDataService as singletons. Run — fails (module absent).
  • [ ] GREEN: implement static-data.module.ts; register it in AppModule. Run — passes.
  • [ ] REFACTOR: only with the test green.
  • [ ] Confirm teeth — drop ItemDataService from exports, watch the resolution assertion fail, restore.
  • [ ] Commit (feat(api): StaticDataModule wiring).

Verified by: apps/api/src/static-data/static-data.module.test.ts.


Closeout (Step 5 · Verify, after T6) ​

Not a task — the verification gate before requesting review (superpowers:verification-before-completion):

  • [ ] pnpm typecheck clean; pnpm test all green; pnpm --filter @gw2priory/api build (SWC) succeeds.
  • [ ] Boot the api (node apps/api/dist/main.js) and confirm it starts — proves the JSON subpath loads at runtime (F12) end-to-end, not just under Vitest.
  • [ ] Fill the spec.md traceability table from the test names above (transcription, not excavation).
  • [ ] superpowers:requesting-code-review → superpowers:receiving-code-review, then the human reviews the diff (CLAUDE.md definition of done).

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.

  • Graduation queue for Step 6 (from research.md): the legendary-type taxonomy + commerce/prices sellability rule (F6/F7/F8) and the api-consumes-JSON rule (F12) move to docs/architecture/.