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" }, depszod+@gw2priory/domain: workspace:*. Mirrorspackages/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 typesCuratedRecipe,Ingredient,Dataset(ids typedItemIdfrom@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), eachmethod: "mystic-forge", item ingredients, exact counts from research V1, a wikisource. - Create:
packages/legendary-recipes/src/index.ts— re-export schema/types;loadDataset(): Datasetimports../data/gen1-weapons.jsonand returnsDatasetSchema.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:*", thenpnpm 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-emptyrecipesarray), andSC2: a structurally invalid entry is rejected— four cases fed straight toDatasetSchema.parse:count: 0, missingsource,method: "forge"(not in the enum), and a duplicateoutputItemId. Runpnpm test— fails (module absent). - [ ] GREEN: write
schema.ts,index.ts, the seedgen1-weapons.json,package.json; add the api dep andpnpm install. Duplicate-outputItemIddetection needs a.superRefineonDatasetSchema(Zod won't catch it otherwise). Runpnpm test— passes;pnpm typecheckresolves the package. - [ ] REFACTOR: only with the test green.
- [ ] Confirm the test has teeth — set a seed ingredient
countto0, watchSC2fail, 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, asourceURL per recipe;legendaryOutputIdslists 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.jsonwith all 21 + gifts +legendaryOutputIds. Run — passes. - [ ] REFACTOR: only with the test green.
- [ ] Confirm teeth — drop one id from
legendaryOutputIds(or delete its recipe), watchSC1/P1 #5fail, 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 importimport dataset from '@gw2priory/legendary-recipes/data'(JSON subpath, F12) + type-onlyimport 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 againstDatasetSchemaby the T1/T2 guard test in CI, so no runtime parse runs here. Build aMap<number, CuratedRecipe>byoutputItemIdand aSet<number>oflegendaryOutputIds. - 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
getRecipelook up the wrong key (e.g.outputCount), watchP1 #1fail, 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 injectsGw2Service.interface ItemMeta { id: ItemId; name; type; rarity; flags: string[]; vendorValue }.metadata(ids)mapsGw2Item → ItemMeta(vendor_value → vendorValue), missing ids omitted.classify(item: { flags: string[] })returnsitem.flags.includes('AccountBound') ? 'gated' : 'buyable'. - Create:
apps/api/src/static-data/item-data.service.test.ts— NestTest.createTestingModulewith.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 guardSC3: 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
classifyto also gate onAccountBindOnUse, watch the The-Legend guard fail, restore toAccountBound-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 injectsGw2Service.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— mockGw2Service.searchRecipes+.recipesvia.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
getRecipesreturn[recipes[0]], watchP3 #2fail, 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 (mirrorsapps/api/src/gw2/gw2.service.test.ts). - Modify:
apps/api/src/app.module.ts— addStaticDataModuletoimports.
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 inAppModule. Run — passes. - [ ] REFACTOR: only with the test green.
- [ ] Confirm teeth — drop
ItemDataServicefromexports, 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 typecheckclean;pnpm testall 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.mdtraceability table from the test names above (transcription, not excavation). - [ ]
superpowers:requesting-code-review→superpowers:receiving-code-review, then the human reviews the diff (CLAUDE.mddefinition 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/pricessellability rule (F6/F7/F8) and the api-consumes-JSON rule (F12) move todocs/architecture/.