Research 006 — Static data module
Status: complete Every [NEEDS VERIFICATION] marker in spec.md has a verdict below. One emergent [NEEDS CLARIFICATION] (V3) is raised for the human — it must be resolved in spec.md before the approval gate, but it is a scope decision, not a reality question.
Verified against: the live GW2 API v2 (api.guildwars2.com) and the official GW2 Wiki (wiki.guildwars2.com), on 2026-07-27. GW2 static shapes and TP prices drift with game patches; re-verify item ids, flags, and recipe quantities if they stop holding.
All item-flag and recipe-absence probes below were run with curl outside the repo (discovery spikes, discarded); only these findings are kept.
V1 — Are the shared-gift Mystic Forge recipe quantities exactly as the spec states?
Question. Spec P1 #2 assumes Gift of Fortune = Gift of Magic + Gift of Might + 77 Mystic Clovers + 250 Globs of Ectoplasm, and (P1 #1) Gift of Mastery = Bloodstone Shard + 250 Obsidian Shards + Gift of Exploration + Gift of Battle.
Verdict. Confirmed.
Evidence.
- Gift of Fortune (item id 19626):
77 Mystic Clover + 250 Glob of Ectoplasm + 1 Gift of Might + 1 Gift of Magic— GW2 Wiki,wiki.guildwars2.com/wiki/Gift_of_Fortune. - Gift of Mastery (item id 19674):
1 Bloodstone Shard + 250 Obsidian Shard + 1 Gift of Exploration + 1 Gift of Battle— GW2 Wiki,wiki.guildwars2.com/wiki/Gift_of_Mastery. - The Bifrost (item id 30698) final assembly:
The Legend + Gift of the Bifrost + Gift of Fortune + Gift of Mastery— GW2 Wiki,wiki.guildwars2.com/wiki/The_Bifrost.
Caveat. Only the shared gifts and The Bifrost were transcribed here. The per-weapon gifts and precursor ids for the other legendaries are available on the wiki in the same uniform structure but are not transcribed in this file — that is curation work for implementation, guarded by the schema test (R3), not a discovery deliverable.
V2 — Is a Gen 1 precursor a buyable leaf (no deterministic forge recipe we model)?
Question. Spec P1 #3 assumes Gen 1 precursors have no deterministic Mystic Forge recipe and are treated as buyable Trading Post leaves.
Verdict. Confirmed.
Evidence. The Legend (item id 29180, The Bifrost's precursor):
- Acquired via random drops, random Mystic Forge combines, or the Legendary Crafting collection — no single deterministic recipe (GW2 Wiki,
wiki.guildwars2.com/wiki/The_Legend). - Is tradeable on the Trading Post:
/v2/commerce/prices?ids=29180→buys.unit_price 110010c,sells.unit_price 150000c(≈11–15 gold), on 2026-07-27. A live price ⇒ TP-listed ⇒ buyable.
V3 — What is the canonical set of "all Gen 1 legendaries", and do they share one recipe shape?
Question. Spec P1 #5 / SC1 say "every Gen 1 legendary weapon". Discovery must fix the exact set and confirm the uniform Precursor + 3-Gifts shape.
Verdict. Confirmed — scope resolved. Count confirmed at 21; human decided all 21 are in scope (2026-07-27), including Eternity. No schema variant is needed: the general ingredients[] list holds Eternity's recipe, and Sunrise/Twilight resolve recursively (spec R11, SC1, P1 #6).
Correction (2026-07-28, found during implementation, Task 2). This entry originally recorded Eternity's recipe as Sunrise + Twilight only. The live wiki recipe (verified, wiki.guildwars2.com/wiki/Eternity) is Sunrise ×1 + Twilight ×1 + 5 Pile of Crystalline Dust (24277) + 10 Philosopher's Stone (20796) — the two bulk mats were missed in discovery. The architecture point stands (the general ingredients[] list holds all four with no schema variant); only the ingredient list was incomplete. Data, the P1 #6 test, and the spec/plan/tasks artifacts were corrected to the full recipe, and the V3/F3/Resolved-Clarification passages below were swept to match.
Evidence. GW2 Wiki, wiki.guildwars2.com/wiki/Legendary_weapon lists 21 first-generation legendary weapons:
- 17 land weapons with the uniform shape (Precursor + Gift of Fortune + Gift of Mastery + Gift of <weapon>): Frostfang, Incinerator, The Moot, Quip, Meteorlogicus, Bolt, The Minstrel, The Flameseeker Prophecies, Rodgort, Howler, Sunrise, Twilight, The Juggernaut, Kudzu, The Predator, The Dreamer, The Bifrost.
- 3 aquatic weapons, same uniform shape: Frenzy (harpoon gun), Kamohoali'i Kotaki (spear), Kraitkin (trident).
- Eternity (greatsword) — special shape: forged from Sunrise + Twilight + 5 Pile of Crystalline Dust + 10 Philosopher's Stone (two finished legendaries), no precursor and no gifts. It does not fit the
ForgeRecipeshape assumed by R2/SC1.
Caveat. This is why V3 is a scope call, not a fact: "all Gen 1" resolves to 17, 20, or 21 depending on whether aquatic weapons and Eternity are in. See the clarification below.
V4 — Does flags[] alone classify each gated input correctly?
Question. Spec P2 #1 / SC3 (and domain.md) assume buyable-vs-gated can be read from item flags[].
Verdict. Confirmed for a refined rule; the naive rule is Refuted. The working rule is: an item is gated ⟺ its flags[] contains AccountBound (bind-on-acquire). SoulBindOnUse, AccountBindOnUse, and NoSell do not imply gated.
Evidence (/v2/items?ids=…, live, 2026-07-27):
| Item | id | flags | class |
|---|---|---|---|
| Mystic Clover | 19675 | AccountBound, NoSalvage, AccountBindOnUse, DeleteWarning | gated |
| Obsidian Shard | 19925 | AccountBound, NoSalvage, NoSell, AccountBindOnUse, DeleteWarning | gated |
| Bloodstone Shard | 20797 | AccountBound, NoSalvage, NoSell, DeleteWarning, AccountBindOnUse | gated |
| Gift of Exploration | 19677 | AccountBound, NoSalvage, AccountBindOnUse, DeleteWarning | gated |
| Gift of Battle | 19678 | AccountBound, NoSalvage, AccountBindOnUse, DeleteWarning | gated |
| Glob of Ectoplasm | 19721 | (empty) | buyable |
| The Legend (precursor) | 29180 | HideSuffix, NoSalvage, NoSell, DeleteWarning, SoulBindOnUse | buyable |
Caveat / refutation. The Legend carries NoSell and SoulBindOnUse yet is freely TP-tradeable (V2). So any rule of the form "has a binding/no-sell flag ⇒ gated" misclassifies the precursor. Only keying on AccountBound matches all seven probes. See F2 for the more robust alternative the plan should adopt.
V5 — Are the item ids the dataset relies on real?
Question. Spec R9 assumes real GW2 item ids for the gated inputs, shared gifts, precursor, and a legendary output.
Verdict. Confirmed. Every id below returned a live /v2/items record on 2026-07-27: 19626 (Gift of Fortune), 19674 (Gift of Mastery), 19675 (Mystic Clover), 19677 (Gift of Exploration), 19678 (Gift of Battle), 19721 (Glob of Ectoplasm), 19925 (Obsidian Shard), 20797 (Bloodstone Shard), 29180 (The Legend), 30698 (The Bifrost).
F1 — The GW2 API confirms it has no forge recipe for legendaries or gifts
/v2/recipes/search?output=30698 (The Bifrost) → []; ?output=19626 (Gift of Fortune) → [] (live, 2026-07-27).
Why it matters. Direct, live proof of domain.md's central premise — the Mystic Forge is absent from the API — so the curated dataset (R1–R4) is not merely convenient but necessary. StationDataService (P3) will get [] for these ids and must return null (leaf), delegating them to ForgeDataService.
F2 — The robust classifier is AccountBound-flag OR /v2/commerce/prices membership
flags[] keyed on AccountBound matches every probe (V4), but flags are an indirect proxy for "is it on the TP". The authoritative buyable signal is presence in /v2/commerce/prices (a live price ⇒ TP-listed). Recommendation for the plan: classify gated ⟺ AccountBound in flags[], and where a price is needed anyway (the resolver always prices buyable leaves), treat commerce-listing as the ground truth and reconcile. This keeps classification correct even if a future item has surprising flags.
Why it matters. Touches R5 and SC3 — the classifier's rule must be AccountBound-specific, not "any binding flag", and the plan should note the commerce cross-check.
F3 — Eternity breaks the uniform ForgeRecipe shape
Eternity = Sunrise + Twilight + 5 Pile of Crystalline Dust + 10 Philosopher's Stone (two finished legendaries + two bulk mats), no precursor, no Fortune/Mastery/weapon gift. If in scope, either the schema must allow a non-precursor 2-legendary assembly, or Eternity is excluded.
Why it matters. R2/SC1 assume Precursor + 3 gifts. Feeds the V3 clarification and the schema design. Resolution: keep ForgeRecipe as a general outputItemId + ingredients[] + source shape (no fixed precursor/gift slots), so Eternity is just ingredients: [Sunrise ×1, Twilight ×1, Pile of Crystalline Dust ×5, Philosopher's Stone ×10] — no variant needed.
F4 — packages/mystic-forge should mirror packages/domain
packages/domain is a src-only workspace package: package.json = { name: "@gw2priory/domain", private: true, type: "module", exports: { ".": "./src/index.ts" } }, no build step. Root tsconfig.json already globs packages/**/*.ts, so a new packages/mystic-forge is typechecked with no extra wiring. pnpm-workspace.yaml already includes packages/*. ItemId is already branded in @gw2priory/domain and should be reused for outputItemId/itemId in the forge schema.
Why it matters. R1 — the new package needs no bespoke build/tsconfig setup; follow the existing src-only pattern and depend on @gw2priory/domain via workspace:*.
F5 — SC6 guard test located
The "zero files under docs/superpowers/" invariant lives in tests/workflow/repo-invariants.test.ts. No new work needed; SC6 is satisfied by writing all artifacts to specs/006-static-data/ (as done).
[NEEDS CLARIFICATION] — RESOLVED
Scope of "all Gen 1 legendaries" (V3/F3). Resolved 2026-07-27 (human decision): all 21 in scope — 17 land + 3 aquatic + Eternity. Reflected in spec.md R11, SC1, and P1 #6. Eternity needs no schema variant (F3): the general ingredients[] list represents it as Sunrise ×1 + Twilight ×1 + Pile of Crystalline Dust ×5 + Philosopher's Stone ×10. No open clarifications remain.
Future-proofing discovery — Gen 2/3, armor, trinkets, back items, runes
Requested scope check (2026-07-27): will 006's schema and service boundaries survive later specs for Gen 2/3 weapons, legendary armor, trinkets, back items, and runes/sigils? Verified against the live API and wiki. Headline: the 006 architecture holds for every legendary type; the only real extension is a currency ingredient kind, needed by armor alone.
F6 — The dominant pattern is a ~4-ingredient Mystic Forge combine, shared across almost all types
| Type | Example | Top-level combine (Mystic Forge unless noted) | Item-only? |
|---|---|---|---|
| Weapon Gen 1 | The Bifrost | Precursor + Gift of <wpn> + Gift of Fortune + Gift of Mastery | yes |
| Weapon Gen 2 | Nevermore | Precursor + Gift of <wpn> + Mystic Tribute + Gift of Maguuma/Desert Mastery | yes |
| Weapon Gen 3 | Aurene's Bite | Precursor + Gift of <wpn> + Draconic Tribute + Gift of Jade Mastery | yes |
| Trinket | Aurora | Spark of Sentience + Gift of Sentience + Mystic Tribute + Gift of Draconic Mastery | yes |
| Back | Ad Infinitum | Unbound (precursor) + Gift of Infinity + Gift of Fortune + Gift of Ascension | yes |
| Rune | Legendary rune | Gift of Runes + Gift of Craftsmanship + Gift of Condensed Might + Gift of Condensed Magic | yes |
| Armor | Obsidian armor | discipline-crafted (Armorsmith/Leatherworker/Tailor, 500) + vendor; consumes items and currencies | no |
Why it matters. The general ForgeRecipe { outputItemId, outputCount, ingredients[], source } shape (no fixed precursor/gift slots — the same generality that already absorbs Eternity, V3) represents every weapon generation, the trinket, the back item, and the rune with zero schema change — they are all item-only Forge combines. Only armor falls outside (see F9).
F7 — "Sellable / buyable" is a per-item commerce/prices fact, NOT a per-generation rule
Live API, 2026-07-27:
| Item | Gen | AccountBound flag | /v2/commerce/prices | Sellable? |
|---|---|---|---|---|
| The Bifrost (30698) | 1 | no | listed, sell ~1,450g | yes |
| Nevermore (71383) | 2 | yes | omitted | no |
| Aurene's Bite (96356) | 3 | no | listed, sell ~2,029g | yes |
Gen 1 and Gen 3 weapons are TP-sellable; Gen 2 is account-bound. (The Legendary_weapon overview's prose claimed the opposite for both — refuted by the API; trust commerce/prices membership.) All armor, trinkets, and back items are account-bound. Consequence: the craft-for-profit persona applies to Gen 1 + Gen 3 weapons only; everything else is craft-for-self (cost + time, no resale).
Why it matters. Reinforces F2 and R10: the classifier's ground truth is /v2/commerce/prices membership, with the AccountBound flag as a fast proxy. Never hardcode "generation ⇒ sellable".
F8 — Shared gifts are reused across types; a flat table keyed by output id dedupes them for free
Gift of Fortune appears in Gen 1 weapons and Ad Infinitum; Mystic Tribute in Gen 2 weapons and Aurora; Gift of Condensed Might/Magic in armor and runes. A single flat curated table keyed by outputItemId stores each shared node once and every consumer (any type) resolves it by id.
Why it matters. Validates the flat, output-id-keyed dataset and the unified getRecipe(itemId) interface as the right long-term shape — adding a type is adding rows, not restructuring.
F9 — The one genuine extension: ingredient kinds (item vs currency), needed by armor
Weapons/trinket/back/rune combines are item-only. Armor recipes consume currencies (Provisioner Tokens, Rift Essences, map currencies, plus account-bound mats like Mystic Clovers) and are discipline-crafted at a station (in the API) + vendor, not a single Forge combine. So the full model eventually needs: (a) an ingredient that can be an item or a currency (/v2/account/wallet currencies), and (b) an acquisition method beyond mystic-forge (discipline, vendor, collection). Collection/achievement-only components bottom out as gated items — "how you earn them" is acquisition metadata for the future time-estimator, not a recipe-graph change.
Why it matters. This is the sole schema pressure the other types add, and it comes from armor only. It sizes the "how much to future-proof now" decision (F10).
F10 — Assessment: what 006 already future-proofs, and the bounded delta
Already future-proof (no change needed), with evidence above:
getRecipe(outputItemId)interface — every legendary type is keyed by an output item id (F6).- Two-source unification (curated + API station) — armor interleaves API station recipes and curated gifts, so unification is necessary, not just convenient (F9, and F1's proof the API lacks forge nodes).
- Flat output-id-keyed table — dedupes shared nodes across all types (F8).
- Classifier on
commerce/pricesground truth +AccountBoundproxy — generalizes to all types (F7). - General
ingredients[]list — represents all weapon gens + trinket + back + rune unchanged (F6).
Bounded delta, deferrable to the armor/trinket spec (real code — YAGNI for 006):
- Currency ingredient kind + non-forge
method(discipline/vendor/collection) + currency/gold costs. Armor-only (F9). Building it now adds unused machinery.
Cheap seams worth baking into 006 now (structural, avoid shared-package churn):
- Generic package name — not
mystic-forge(it will hold back/trinket/rune recipes too, and later the non-forge nodes). Suggestpackages/legendary-recipes. - Type name
CuratedRecipe+ amethodfield set to'mystic-forge'for all 006 entries. Self-documenting; no rename/migration when other methods arrive. - (Judgment call) Model ingredients as a discriminated union
{ kind: 'item', itemId, count }now, so armor's{ kind: 'currency', currencyId, count }slots in without migrating the weapon entries. Deferring means a later breaking change to the shared package (mechanical, guard-test-caught, but churn in a package 007's resolver depends on). This is the one mildly-speculative call — human to decide.
F11 — Variant skins (Gen 3) and newer types are out of scope but noted
Gen 3 weapons carry multiple variant skins per weapon (one item id, many appearances) — irrelevant to cost/recipe, ignore for planning. Legendary relic, sigils, and aquabreather exist as further account-bound types; runes/relics are Forge/gift-based like F6, sigils under-documented. None change the model; all are additional rows/sources for a future spec.
F12 — The SWC-built api cannot import a source-.ts package at runtime; it must consume JSON
Surfaced while planning (2026-07-28). packages/* ship raw .ts (monorepo.md), but the api is SWC-compiled and run as plain node dist/main.js. pnpm symlinks a workspace package intonode_modules, and Node 26 refuses to type-strip files under node_modules:
require('@gw2priory/legendary-recipes')andawait import(...)of a package whoseexportspoint to./src/index.tsboth fail: "Stripping types is currently unsupported for files under node_modules" (measured, Node v26.5.0, scratchpad spike, discarded).- The same package works fine under tsc (compile-only) and Vitest (resolves the symlink to the real path outside
node_modulesand transforms it). Only the running api is blocked. - A JSON subpath works: with
exports: { "./data": "./data/gen1-weapons.json" },require('@gw2priory/legendary-recipes/data')returns the parsed array at runtime — JSON is not type-stripped, so the restriction does not apply (measured, same spike).
Verdict. The curated data stays in packages/legendary-recipes (spec R1) but is authored as a JSON data file the api loads at runtime, alongside a source-.ts schema + validated loader consumed by Vitest (the R3 guard test) and any source consumer. The api imports the JSON subpath for values and the CuratedRecipe type-only (erased by SWC) — it never imports the package .ts at runtime. Runtime Zod validation is not needed in the api hot path because the R3 guard test validates the JSON against the schema in CI; the api's typed view of the JSON is a single, guard-test-justified assertion at the module boundary (not an unexplained any).
Why it matters. Shapes the plan's file structure and is a project-wide fact: any future api consumption of a packages/* module must go through JSON (or a built artifact), never source .ts. Graduation candidate for docs/architecture/monorepo.md.
Refuted claims
- "
flags[]alone (any binding flag) classifies buyable vs gated." Believed from domain.md's "classified programmatically fromflags[]". Reality (V4): The Legend is TP-buyable despiteNoSell+SoulBindOnUse. Change: the rule must key specifically onAccountBound; R5/SC3 wording should say so, and the plan should add the commerce-listing cross-check (F2). This is a refinement of the classifier, not a collapse of the spec — the module shape stands.
Graduation
Candidates to move to docs/architecture/domain.md at step 6 so spec 007 doesn't re-derive them:
- The gated-input item ids + the
AccountBound-based classification rule and itscommerce/pricescross-check (V4/F2). - The confirmed shared-gift recipes and item ids (V1) and the precursor-as-buyable-leaf fact (V2).
- The live confirmation that the API has no forge recipes (F1).
- The legendary-type taxonomy and universal ~4-ingredient Forge pattern (F6), the per-item sellable-via-
commerce/pricesrule (F7), and shared-node reuse across types (F8) — these are project-wide domain facts (docs/architecture/domain.md) that later specs (Gen 2/3, armor, trinkets) will build on directly. - The bounded schema delta for armor (currency ingredients + non-forge methods, F9) — a pointer for the eventual armor/trinket spec so it isn't re-discovered.
- The api-consumes-package rule (F12) →
docs/architecture/monorepo.md: the SWC/plain-node api must load apackages/*module via a JSON subpath (or a built artifact), never source.tsundernode_modules. Every later api+package spec needs this.