Skip to content

Research 007 — Recipe graph resolver ​

Status: complete

Step 1.5 output, written between the spec draft and the approval gate. Every [NEEDS VERIFICATION] marker in spec.md now has a verdict below, and the one clarification it surfaced (F7) was resolved by the human on 2026-07-29 (Option A). The spec was updated accordingly (R12, corrected SC4/SC5/SC7, F6 limitation). The approval gate — moving spec.md to approved — remains the human's.

Verified against: the live GW2 API v2 (https://api.guildwars2.com/v2) and the repo at branch 007-recipe-graph-resolver, on 2026-07-29. GW2 drifts static shapes with game patches — re-check if a finding stops holding.

V1 — Does an output with multiple station recipes exist, and is a buyable item expanded past? ​

Question. SC5 / P1 #3 rest on (a) some output having ≥2 station recipes so the "keep all options / min over them" path is real, and (b) a buyable-and-craftable item being expanded rather than truncated.

Verdict. Confirmed — with an example correction (the claim holds; the spec's illustrative ids were wrong, see Refuted claims). Both properties exist in reality:

  • Multiple station recipes: recipes/search?output=46742 → [7319, 12053] — two recipes for the same output (7319 = the 6 armor/weapon disciplines, 12053 = Jeweler), differing only by discipline.
  • Buyable-and-craftable expansion: Bolt of Damask (46741, flags ["NoSalvage"] → buyable; TP buy 7301 / sell 8860) has recipe 7309 = 20x#19740 + 10x#19742 + 20x#19744 + 1x#46740, and its sub-ingredient Bolt of Silk (19747) itself has recipe [8] — station chains descend to raw mats and terminate.

Evidence. curl .../recipes/search?output=46742|46741, .../recipes?ids=7309, .../commerce/prices?ids=46741, .../items?ids=46741 (2026-07-29).

Caveat. 46742 (2 recipes) is AccountBound — a gated item that is nonetheless craftable (see F8); it demonstrates the plural-recipe path but not "buyable." 46741 demonstrates "buyable+craftable" but has a single recipe. No single Gen 1 item is cleanly both, so the two SC5 sub-claims want two different example ids.

V2 — Does a warm-cache resolve issue zero new GW2 API calls? (SC7) ​

Question. SC7 claims a re-resolve on a warm static-data cache makes 0 new GW2 calls.

Verdict. Confirmed for the "0 calls" guarantee; the latency number is deferred to step 5 (it measures code not yet written and can't be pinned now).

Evidence. apps/api/src/gw2/gw2-client.ts:64-80 — staticCache (items+recipes) and searchCache (recipe searches) are BoundedCache instances constructed with no TTL; only priceCache has PRICE_TTL_MS = 60_000 (line 28). The resolver reads only items / recipes / recipe-search (never prices), so once those are cached a repeat resolve hits no network.

Caveat. SC7's [NEEDS VERIFICATION: target ms] stays a step-5 measurement; recommend SC7 assert 0 new calls (structural, testable now via mocked call counts) and treat any ms figure as an observed value at verification, not a spec-time promise.

V3 — Do any curated outputs also have a station recipe? (the union "both" branch, R4) ​

Question. R4 unions curated + station recipes. Does the "both sources return a recipe for the same item" case actually occur?

Verdict. Refuted (absent). No curated output has a station recipe, so the union's both-branch never fires for the Gen 1 set — it remains a defensive code path, correct but unexercised by real data.

Evidence. recipes/search?output= returns [] for Gift of Fortune (19626), Gift of Mastery (19674), and The Bifrost (30698) (2026-07-29). Curated forge outputs are forge-only, as domain.md states.

V4 — Is the curated graph acyclic, and does Eternity recurse without a cycle? (SC3 / SC8) ​

Question. SC3 needs Eternity to expand Sunrise + Twilight recursively; SC8's cycle guard must be defense-in-depth, not masking a real cycle.

Verdict. Confirmed. Eternity (30689) → Sunrise (30703) + Twilight (30704) + Pile of Crystalline Dust (24277 ×5) + Philosopher's Stone (20796 ×10); Sunrise and Twilight are themselves curated legendary outputs with standard recipes, and neither references Eternity back — no cycle. The happy-path graph is acyclic.

Evidence. packages/legendary-recipes/data/gen1-weapons.json:271-282 (Eternity), :163-186 (Sunrise/Twilight recipes).

F6 — Under 006's data, a Gen 1 legendary tree is SHALLOW; the deep station chains aren't reached yet ​

The curated set (006) contains only the top assembly + Gift of Fortune + Gift of Mastery. The mid-tree forge gifts — Gift of Might (19672), Gift of Magic (19673), and every weapon-specific gift (Gift of The Bifrost 19654, …) — are not curated and absent from /v2/recipes (recipes/search?output= → [] for all three). All three are AccountBound. So the resolver reaches them, finds no recipe from either source, and terminates them as gated leaves.

Consequently resolve(30698) (The Bifrost) is ~13 distinct nodes, depth 3: Bifrost → {The Legend, Gift of the Bifrost (leaf), Gift of Fortune → {Gift of Magic (leaf), Gift of Might (leaf), Mystic Clover (leaf), Ecto (leaf)}, Gift of Mastery → {Bloodstone Shard, Obsidian Shard, Gift of Exploration, Gift of Battle — all gated leaves}}. Ecto (19721) and every gated input confirmed recipe-less (recipes/search?output=19721|19675|20797|19925|19677|19678 all []).

Why it matters. (1) The deep station expansion in SC5 (Damask→silk→raw) is exercised only by a standalone resolve(46741/46742), not inside a current legendary tree — SC5 tests should call the item directly. (2) SC7's warm-cache resolve is trivially fast now (tiny tree), reinforcing V2's "assert 0 calls, don't promise ms." (3) 008's profit numbers will be incomplete until the forge gifts are curated — the T6-mat cost inside Gift of Might/Magic and the weapon gift is currently invisible (treated as free, player-supplied). That deepening is a future curation spec, not 007; 007 resolves available data faithfully. Recommend the spec state this limitation explicitly.

F7 — Precursors HAVE recipes in the API (type: "LegendaryComponent") — they are not leaves ⚠️ decision needed ​

The Legend (29180) returns recipes/search?output=29180 → [11134], and recipe 11134 is { type: "LegendaryComponent", disciplines: ["Artificer"], min_rating: 500, flags: ["LearnedFromItem"], ingredients: [74378, 76891, 73809, 73875] } — the deterministic precursor-crafting (Hero's-collection) path. Every precursor has one: Dawn (29166)→10795, Dusk (29185)→11180, The Lover (29167)→11227.

So StationDataService.getRecipes(precursor) returns non-empty, and under R5 ("expand while any recipe exists") the resolver would expand the entire precursor-crafting tree — which 006 explicitly put out of scope ("Gen 1 precursors are treated as buyable leaves"). This refutes the spec's SC4 / P1 #4 / Assumption that a precursor is a recipe-less leaf.

Resolution — Option A (human, 2026-07-29). Keep precursors as buyable leaves: the resolver filters out type: "LegendaryComponent" recipes. Aligns with 006's out-of-scope, matches the realistic persona (precursors are bought on the TP), and avoids pulling in collection-/account-gated precursor components (74378, …). Recorded in the spec as R12, with SC4 / P1 #4 / Assumptions kept as written.

  • (Rejected — Option B: expand the precursor-crafting path. Genuinely useful buy-vs-craft, but drags in the precursor-crafting collection subtree, contradicts 006's scope line, and is arguably its own spec.)

Evidence. recipes?ids=11134; recipes/search?output=29180|29166|29185|29167 (2026-07-29).

F8 — Classification is independent of leaf-ness (a gated item can be craftable) ​

Lump of Mithrillium (46742) is AccountBound → classified gated, yet it has two recipes → not a leaf. The ascended time-gated layer generally splits this way: buyable finals (Deldrimor Steel Ingot 46738, Elonian Leather Square 46739, Bolt of Damask 46741, Xunlai Electrum Ingot 46743 — all NoSalvage-only) built from AccountBound daily intermediates (Lump of Mithrillium 46742, Glob of Elder Spirit Residue 46744, Spool of Thick Elonian Cord 46745).

Why it matters. Validates R5 and the design's node model: classification (buyable/gated, a static flag fact) and leaf (recipes.length===0, a graph fact) are orthogonal — the resolver must never shortcut "gated ⇒ leaf." Worth an explicit test: a gated-but-craftable node is expanded, not truncated.

F9 — F12 (Node won't type-strip under node_modules) governs where projectTree and Zod may live ​

Surfaced during Step-4 plan-mode exploration. The API builds with swc src -d dist (apps/api/package.json) — only apps/api/src is compiled — and the built API (node dist/main.js) cannot load a runtime value from a .ts workspace package (research 006 F12, cited in curated-recipe.service.ts:1-6). This applies to projectTree and the endpoint's Zod schemas: putting them in @gw2priory/recipe-graph and importing them at runtime would pass Vitest (Vite transforms TS) but break the built API.

Why it matters. Drove the Option-A design (R11): the package is types-only (type-only imports, like domain/legendary-recipes); projectTree and the boundary Zod live in apps/api (the health.schema.ts pattern). (Human decision, 2026-07-29.) Graduation candidate: docs/architecture/ should state "workspace packages consumed by the API export types/JSON only — never runtime values."

Refuted claims ​

Per the workflow, a refuted premise is surfaced to the human rather than silently patched.

  1. "A Gen 1 precursor resolves to a recipe-less leaf" (SC4, P1 #4, Assumptions) — refuted by F7, then restored by decision. The API supplies LegendaryComponent precursor-crafting recipes, so a precursor is not naturally recipe-less. The human chose Option A (2026-07-29): the resolver filters LegendaryComponent recipes (R12), which makes "precursor is a buyable leaf" true again by construction. SC4 / P1 #4 / Assumptions stand as written, now backed by R12 rather than an incorrect premise.
  2. "Bolt of Damask, id 46742, has two station recipes" (SC5, P1 #3 — inherited from 006's spec) — refuted by V1. 46742 is Lump of Mithrillium (2 recipes, but AccountBound); Bolt of Damask is 46741 (buyable, 1 recipe). Factual example correction: split the two SC5 sub-claims across the two correct ids. (Also flags a stray error in 006's committed spec/domain references — graduation candidate.)

Graduation ​

Candidates for docs/architecture/ at step 6 (so the next spec doesn't re-discover them):

  • gw2-api.md — "Mystic Forge legendary/gift recipes are absent from /v2/recipes, but precursors carry type: \"LegendaryComponent\" recipes (the precursor-crafting path) that recipes/search surfaces." The LegendaryComponent recipe type is the clean filter key for Option A.
  • domain.md — the ascended time-gated layer (F8): buyable finals vs AccountBound craftable intermediates; "gated ≠ leaf."
  • Correction — fix the "Bolt of Damask = 46742" mislabel in 006's spec/domain (it is Lump of Mithrillium; Damask is 46741).