Spec 008 — Buy-vs-craft pricing
Status: implemented Branch: 008-buy-vs-craft-pricing
Status is set by the human, never by the agent. It moves draft → approved → implemented. Transcribed to implemented on the human's explicit instruction (2026-08-02), part of the feature-branch PR diff.
Problem
Spec 007 resolves a legendary's full dependency graph and projects it to a nested display tree at GET /recipe-graph/:itemId, but it is purely structural: it computes no gold cost, reads no price, and chooses no recipe — it keeps every RecipeOption and leaves the buy-vs-craft min to 008 (007 R4, R5). So nothing yet answers the question the planner exists for: at each node, is it cheaper to buy the item on the Trading Post or craft it — what does the cheapest whole plan cost, and would selling the finished legendary profit?
There is also a data gap that makes naïve pricing lie. The curated forge dataset today contains recipes only for the 21 Gen 1 weapons, Gift of Fortune (19626) and Gift of Mastery (19674). The Mystic-Forge intermediates on the path — Gift of Might (19672), Gift of Magic (19673), Mystic Clover (19675, ×77 for The Bifrost), Gift of The Bifrost (19654) — have no curated recipe and (being Mystic-Forge output, absent from /v2/recipes) resolve as gated leaves, which also strands everything below them (the sub-gifts Gift of Energy/Color, though they do have discipline recipes, are never reached). Those "leaves" are not free: a Mystic Clover consumes buyable Mystic Coins + Globs of Ectoplasm; the Might/Magic gifts consume 1000+ buyable T6 materials. Priced at 0, a Bifrost's cost — and therefore its profit — is garbage.
008 makes the flagship number real and trustworthy: it prices the graph under a stated market convention, and curates the Bifrost's complete forge path down to buyable raw materials so no gold-bearing node bottoms out at 0. Truly-earned tokens (Obsidian Shards, Bloodstone Shard, Spirit Shards, Gift of Exploration/Battle) stay 0 — you grind them, you don't buy them. Other Gen 1 weapons stay priced-but-partial (a documented, known limitation) until a later curation pass.
A framing note (research F1). Discovery found that on the Gen 1 path the buy-vs-craft min changes no decision: every craftable intermediate is AccountBound (can't be bought → force-crafted) and every buyable node is a raw-material leaf (can't be crafted → force-bought). A genuine buy-vs-craft choice needs a node that is both buyable and craftable — a Gen 2/3 / armor case, absent from Gen 1. So for the flagship, 008 is in practice a cost roll-up + profit calculator; the min(buy, craft) engine is still built and correct because it is forward-compatible (it will bite on later generations), but this spec does not oversell it as an "optimizer" for Gen 1. (Human decision, 2026-08-01: keep the engine, soften the framing.)
User stories
Ordered by priority. Each story must be independently testable and shippable — if only P1 ships, there is still something usable.
P1 — Price a resolved graph into an enriched, cost-annotated plan
As the planner, I want a pure function that folds Trading Post prices into a resolved graph and enriches every node with its buy-vs-craft decision and cost — plus a root-level cost/net-sell/profit summary — so a player sees, at each node, what to buy, what to craft, what it costs, and whether crafting to sell pays.
Independent test: priceGraph(graph, priceMap) is a pure function fed a hand-built ResolvedGraph (007's shape) and a plain id → { buys, sells } map; asserted on the returned enriched tree + summary with no I/O, no services, no live GW2 API. Every cost, decision, and summary field is verifiable from the inputs.
Acceptance scenarios
- Given a buyable-and-craftable node, when the graph is priced, then the node carries both
unitBuyPrice(itssells.unit_price, the buy option) andcraftCost(the summed per-unit cost of its cheapest recipe),unitCost= theminof the two, anddecision= which won ('buy'or'craft'); abuy/crafttie resolves to'buy'. - Given a node whose
decisionis'buy'(buying is cheaper), when the graph is priced, then its crafted subtree is still present (recipeandchildrenkept, fully expanded) — the decision is a marker, not a prune — so the UI can show "you'd craft this like so, but buying is cheaper." - Given a node with more than one
RecipeOption, whencraftCostis computed, then it is the minimum over the options, and therecipe/childrenshown are that cheapest option's expansion. - Given a gated leaf (
classification: 'gated',recipes: []— e.g. Gift of Exploration), when the graph is priced, thenunitCostis0,decisionis'gated', and it contributes 0 to the roll-up (player-supplied, perdomain.md). - Given a gated-and-craftable node (
classification: 'gated'with recipes — e.g. Mystic Clover), when the graph is priced, then it has no buy option (not on the TP), sodecisionis'craft', its children expand, and itscraftCostrolls up from its buyable ingredients (Mystic Coins, Ectos) — a gated node is 0-gold only when it is a true leaf. - Given the root's chosen path is fully priced, when the graph is priced, then
summarycarriestotalCraftCost(the root's rolled-up cheapest cost — each node contributing itsunitCost=min(buy, craft), so abuy-decision node's shown children do not inflate the total),rootBuyPrice(the root'ssells.unit_price),netSell=netSellPrice(root.buys.unit_price), andprofit=netSell − totalCraftCost. - Given the root is always the product being crafted, when the graph is priced, then the root node's
decisionis'craft'(never'buy') even ifrootBuyPrice < totalCraftCost; the buy price is surfaced only viasummary.rootBuyPricefor comparison. - Given a root with no buy order on the TP (not sellable), when the graph is priced, then
summary.netSellandsummary.profitarenull(sellability read from prices, never inferred from generation —domain.md), whiletotalCraftCostis still produced.
P2 — Serve the priced plan by extending the recipe-graph endpoint
As the frontend, I want GET /recipe-graph/:itemId to return the tree already enriched with costs and a summary so that I can render the plan, per-node buy/craft badges, and the profit line without resolving a graph, fetching prices, or running the optimizer myself.
Independent test: a controller/service is exercised with a mocked RecipeGraphService (fixture ResolvedGraph) and a mocked Gw2Service.prices; status codes and the enriched body are asserted with no live server and no GW2 API. The OpenAPI assertion runs against the offline-generated document.
Acceptance scenarios
- Given a valid legendary id, when
GET /recipe-graph/:itemIdis called, then it returns200with the enriched tree — every 007 field (node,count,recipe,children) unchanged and still present, each node additionally carryingdecision,unitBuyPrice,craftCost,unitCost,lineCost, and the root additionally carryingsummary(aPlanSummary). - Given a param that is not a positive integer, when the route is called, then it returns
400; given an id the GW2 API does not know (resolved root metadata isnull), then it returns404— guards unchanged from 007. - Given a resolved graph, when the response is built server-side, then the distinct buyable ids plus the root id are fetched in one batched
Gw2Service.pricescall (never per-node), the price map is handed to the purepriceGraph, and 007'sRecipeGraphService.resolveis left unchanged and price-free (the price fetch and cost fold live in the controller/service layer, not the resolver).
P3 — Curate the Bifrost's forge path so its number is real
As a seller pricing The Bifrost, I want every gold-bearing node on its path expanded to buyable raw materials — not bottomed out as a free gated leaf — so that its totalCraftCost and profit reflect reality.
Independent test: resolve(30698) against the real curated dataset (007's resolver, unchanged code) is asserted to expand Gift of Might, Gift of Magic, Mystic Clover, and Gift of the Bifrost into their buyable ingredients down to raw materials, with only the earned tokens remaining as leaves; and the curated dataset's own guard tests stay green.
Acceptance scenarios
- Given the curated dataset, when it is loaded, then it contains exactly four new Mystic-Forge recipes — Gift of Might (19672), Gift of Magic (19673), Mystic Clover (19675), Gift of The Bifrost (19654) — each ingredient id verified against the wiki (research V2); the sub-gifts Gift of Energy (19623) and Gift of Color (19638) are not curated (they are
/v2/recipesdiscipline recipes 007's station path already resolves). - Given
resolve(30698), when the graph is resolved, then Gift of Might / Gift of Magic expand to their (buyable) T6 materials, Mystic Clover expands to Mystic Coins + Obsidian Shards + Ectoplasm + Mystic Crystal, and Gift of The Bifrost expands to Gift of Energy + Gift of Color (which the station path further expands to buyable dusts / dyes / Opal Orbs) + Icy Runestones + a Sigil — none of these four remains a leaf. - Given the probabilistic Mystic Clover recipe, when it is curated, then it is encoded as an expected-value recipe: its
outputCountis the expected clover yield per Forge batch (a fractional number, verified against the wiki), so per-unit clover cost = batch input cost ÷ expected yield. - Given the earned tokens on the path (Obsidian Shards 19925, Bloodstone Shard 20797, Spirit Shards, Gift of Exploration 19677, Gift of Battle 19678), when the graph is priced, then they remain leaves valued at
0(they are not curated into recipes; they are player-supplied).
Requirements
- R1 — A pure
priceGraph(graph: ResolvedGraph, priceMap): PricedTreelives inapps/apinext toproject-tree.ts(007 R11 — the SWC-built API cannot load a runtime value from a.tsworkspace package). It performs no I/O and calls no service.priceMapis a plainMap<number, { buys: { quantity; unit_price }; sells: { quantity; unit_price } }>— the fields 008 needs fromGw2Price, decoupled from the client type. - R2 — Per-unit chosen cost is computed once per distinct item id (memoised over the DAG, mirroring 007's dedup). For an id, the two options are:
- the buy option (
unitBuyPrice) —sells.unit_priceiffclassification === 'buyable'and the id has a usable price (present inpriceMap,sells.quantity > 0,sells.unit_price > 0); otherwise no buy option. - craftCost — the minimum over the node's
RecipeOptions ofceil( Σ_itemEdges (childChosenCost(edge.itemId) × edge.count) / outputCount ).ceilnever understates; dividing byoutputCountyields a per-unit cost (a recipe yieldsoutputCountitems — possibly fractional, R11). Currency edges contribute 0. If no option is fully priced, no craft option.
- the buy option (
- R3 — Per id:
unitCost = min(unitBuyPrice, craftCost)over the defined options;decision= the winner, with a tie →'buy'(simpler plan; deterministic for golden tests). A gated leaf (classification: 'gated',recipes: []) →unitCost 0,decision 'gated'. If neither option is defined (a rare unpriced, non-craftable node — see Assumptions) →unitCost null,decision 'unknown', and thenullpropagates through the roll-up so no total is silently faked. No special counting or flag is emitted for this case (unpriced items are treated as a non-issue per the human decision). - R4 — Unlike 007, 008 selects a recipe: the tree shows the cheapest
RecipeOption's expansion at each node. But it never prunes — a'buy'-decision node keeps itsrecipeandchildrenfully expanded (decision is a marker, not a cut). The root is always crafted: itsdecisionis'craft'(or'unknown'), regardless of its own buy price. (Human decisions, 2026-08-01: "keep full structure, mark decision"; root is the product.) - R5 — A
PricedTreeNodeis 007'sTreeNodeplus cost fields:{ node: GraphNode; count: number; recipe: RecipeOption|null; children: PricedTreeNode[]; decision: 'buy'|'craft'|'gated'|'unknown'; unitBuyPrice: number|null; craftCost: number|null; unitCost: number|null; lineCost: number|null }.node/count/recipe/childrenare unchanged from 007 (existing consumers keep finding them).unitBuyPrice= the usablesells.unit_priceornull(the buy option, R2);craftCost= the cheapest recipe's per-unit cost (ornull);unitCost=min(R3);lineCost = unitCost × count(ornull). A shared id renders at each usage position (display duplication, as in 007), each carrying its owncount/lineCostbut the same per-idunitCost/decision. - R6 — A
PlanSummaryis{ rootId: number; totalCraftCost: number|null; rootBuyPrice: number|null; netSell: number|null; profit: number|null }.totalCraftCost= the root's rolled-up cost, each node contributing itsunitCost(so'buy'nodes' shown children do not inflate it).netSell=netSellPrice(root.buys.unit_price)when the root has a buy order, elsenull.profit = netSell − totalCraftCostwhen both are non-null, elsenull.rootBuyPrice= the root'ssells.unit_priceornull. Thesummaryis attached to the rootPricedTreeNodeonly (child nodes have nosummary), so the HTTP body stays "the tree" and existing consumers are undisturbed. (Human decision, 2026-08-01: "always priced, extra fields — structure unchanged.") - R7 —
netSellusesnetSellPricefrom@gw2priory/domain(the existing floor-of-85% helper) — 008 does not re-derive the 15% tax. Sellability is a per-item/v2/commerce/pricesfact (the root has a buy order or it does not); 008 never infers it from legendary generation (domain.md). - R8 — The price convention is instant / conservative: inputs are acquired at
sells.unit_price(buy now) and the finished legendary is valued atbuys.unit_pricebefore tax (fill now). No buy-order / optimistic model is built. (Human decision, 2026-08-01.) - R9 —
GET /recipe-graph/:itemIdis extended (not replaced, not siblinged): it nowresolves, fetches prices, foldspriceGraph, and returns the enriched tree + rootsummary. It always prices (no query flag). The path-param guards (400non-positive/non-integer,404unknown root) are unchanged. The price fetch (R2's single batchedGw2Service.pricesover distinct buyable ids ∪ root) and the fold live in the controller/service layer;RecipeGraphService.resolveis not modified. (Human decision, 2026-08-01: "extend 007's endpoint.") - R10 — The response is documented in the generated OpenAPI via the nestjs-zod pattern (
createZodDto+@ZodResponse). 007'sTreeNodeSchema(recursive,z.lazy,: z.ZodType<…>drift guard) is extended with the cost fields into aPricedTreeNodeSchema, and the root DTO addssummary. The deterministic-emit guard test still passes; the OpenAPIGET /recipe-graph/{itemId}entry reflects the enriched shape. - R11 — Fractional
outputCount. The curated recipe schema (packages/legendary-recipes/src/schema.ts) and 007's API responseRecipeOptionSchema(recipe-graph.schema.ts) currently pinoutputCountto a positive integer. 008 relaxes both to a positive number so an expected-value forge recipe (Mystic Clover) can encode a fractional yield. The shared TS type is alreadynumber. Ingredientcounts remain positive integers. (Human-approved recommendation, 2026-08-01.) - R12 — Curation (Bifrost path) — exactly four Forge recipes. Discovery (research V2) pinned the tree: only four items on the Bifrost path have no
/v2/recipesentry and therefore need curated Mystic-Forge recipes — Gift of Might (19672), Gift of Magic (19673), Mystic Clover (19675), and Gift of The Bifrost (19654). New curated recipes for these four are added to the@gw2priory/legendary-recipesdataset (method: 'mystic-forge'), each ingredient id cited from the wiki. The two sub-gifts of Gift of The Bifrost — Gift of Energy (19623 → recipe 4315, Artificer) and Gift of Color (19638 → recipe 3165, Chef) — are ordinary discipline recipes 007's station path already resolves, so they are not curated; once Gift of The Bifrost is curated they expand automatically to their buyable dusts/dyes/orbs. The dataset's own guard tests (packages/legendary-recipes) stay green (research V6). (Human decision, 2026-08-01: "full Bifrost, careful ids" — which discovery showed needs only these four Forge recipes.) - R13 — Earned/vendor tokens are not curated into recipes and remain gated leaves valued at 0: Obsidian Shards (19925), Bloodstone Shard (20797), Spirit Shards (currency), Mystic Crystal / Philosopher's Stone (vendor), Gift of Zhaitan (19669, dungeon-token vendor), Icy Runestone (karma vendor), Gift of Exploration (19677), Gift of Battle (19678). Curation stops at the boundary between buyable materials and earned/vendor items (research V2/V3).
- R14 — 008 adds no new price cache (the 005 client owns the 60 s price TTL and 199-id batching —
gw2-api.md); a hardGw2Servicefailure propagates with no added retry/backoff. 008 writes no file underdocs/superpowers/and adds no persistence (no Postgres, no Redis).
Mark anything unresolved inline rather than assuming an answer. Two markers, split by who can answer:
[NEEDS CLARIFICATION: specific question]— only the human can answer (a product decision, a scope boundary, a preference). Blocks step 1.5.[NEEDS VERIFICATION: specific question]— only reality can answer (whether the codebase works that way, whether an endpoint returns that field, whether a recipe's contents are as stated). Answered inresearch.mdwith cited evidence, never by assumption. Blocks the approval gate.
All six [NEEDS VERIFICATION] markers below were resolved in step 1.5 — every one confirmed; verdicts and cited evidence live in research.md (V1–V6). Kept here as the record of what was checked.
- ✅ [VERIFIED — research V1] exact ingredients of Gift of Might (19672 = 250× Vicious Fang / Armored Scale / Vicious Claw / Ancient Bone) and Gift of Magic (19673 = 250× Vial of Powerful Blood / Powerful Venom Sac / Elaborate Totem / Pile of Crystalline Dust) — all TP-buyable.
- ✅ [VERIFIED — research V2] Gift of The Bifrost (19654) = Gift of Energy (19623) + Gift of Color (19638) + 100 Icy Runestone + 1 Superior Sigil of Nullification; the two sub-gifts are
/v2/recipesdiscipline recipes (4315, 3165) 007 resolves without curation; Gift of Zhaitan (19669) is a vendor leaf. - ✅ [VERIFIED — research V3] Mystic Clover (19675): ~31% success ⇒ ≈3.1 clovers per 10-input batch (10 Obsidian + 10 Mystic Coin + 10 Ecto + 10 Mystic Crystal) → fractional
outputCount ≈ 3.1; only Mystic Coin + Ecto are buyable. - ✅ [VERIFIED — research V4]
Gw2Client.getByIdsomits ids the API doesn't return (gw2-client.ts:197-215); a live 206 probe dropped a non-tradeable id; empty market reads asquantity: 0. - ✅ [VERIFIED — research V5] The Bifrost (30698) has a live
buysorder and noAccountBoundflag ⇒ classifiedbuyable;netSell/profitcompute. - ✅ [VERIFIED — research V6] adding the four curated recipes keeps the real-data guard test and 007's (mock-based) resolver tests green; curated shape carries
method: 'mystic-forge'; the fractional-outputCountrelaxation is isolated to two schema lines.
Any success criterion stating a number carries a [NEEDS VERIFICATION] until a measurement in research.md backs it. 008's success criteria assert behaviour over fixtures (deterministic golden values) plus the verified curation, not live market numbers.
Success criteria
Measurable and technology-agnostic — outcomes, not implementation.
- SC1 — Over a fixture graph, a node carries both
unitBuyPriceandcraftCost;unitCost = minanddecisionis the winner; a tie resolves to'buy'. - SC2 — A
'buy'-decision node keeps itsrecipeand fully-expandedchildren(never pruned); itsunitCostequalsunitBuyPrice, and its children do not contribute tototalCraftCost. - SC3 — Multi-recipe: given two options of different cost, the chosen
recipe/craftCostare the cheaper option's. - SC4 — Gated: a gated leaf →
unitCost 0/decision 'gated', excluded fromtotalCraftCost; a gated craftable node (Mystic Clover) →decision 'craft', children expanded, cost rolled up from buyable ingredients (never a 0-gold leaf). - SC5 — Root economics: for a fixture legendary with a priced path and a sellable root,
totalCraftCostequals the rolled-up cheapest cost,netSell = netSellPrice(root.buys.unit_price),profit = netSell − totalCraftCost, and the root'sdecisionis'craft'even whenrootBuyPrice < totalCraftCost. - SC6 — Non-sellable root: with the root absent from prices (no
buys),netSellandprofitarenullwhiletotalCraftCostis still produced. - SC7 —
priceGraphis pure: same(graph, priceMap)→ identicalPricedTree, no I/O (asserted with a frozen input and no mocked network). - SC8 —
GET /recipe-graph/:itemIdreturns200with the enriched tree + rootsummaryfor a known legendary; every 007 field is still present on each node; a non-integer/non-positive param →400; an unknown item →404. The endpoint issues exactly oneGw2Service.pricescall whose id set = distinct buyable ids ∪{ rootId }, and callsresolveonce (mocked call counts). - SC9 — Fractional
outputCount: the curated schema and the API response schema accept a positive non-integeroutputCount; a Mystic Clover recipe with a fractional expected yield validates and its per-unit cost = batch input cost ÷ yield. - SC10 —
resolve(30698)against the real dataset expands Gift of Might, Gift of Magic, Mystic Clover, and Gift of the Bifrost to buyable raw materials; the earned tokens (R13) remain leaves; the resulting priced Bifrost has a non-zero, completetotalCraftCost(no gold-bearing node valued at 0). - SC11 — A
PricedTreeserializes to plain JSON and round-trips unchanged (noMapon the wire;summaryand nestedchildrenintact). - SC12 — The generated OpenAPI document (and committed
openapi.json) documents the enrichedGET /recipe-graph/{itemId}response (PricedTreeNode+ rootsummary); the deterministic-emit guard test still passes. - SC13 — Zero files are written under
docs/superpowers/(the existing repo invariant test still passes; all 008 artifacts live underspecs/008-buy-vs-craft-pricing/).
Out of scope
- Aggregated shopping list — a deduped "buy N of item X across all branches" roll-up is its own later spec. 008 annotates the tree and summarises totals; it emits no flattened buy-list.
- Curating the other 20 Gen 1 weapons' paths — 008 curates The Bifrost end-to-end (P3); other weapons are priced-but-partial (their uncurated weapon-gifts stay gated leaves = 0) — a documented, known limitation.
- Account state / own-vs-need — "you already own 40 ectos" personalisation is a later account spec. 008 prices as if nothing is owned.
- Opportunity-costing gated inputs — earned tokens are 0 gold; valuing them (gold-per-Gift, time) is out of scope.
- The patient / optimistic (buy-order) price convention — 008 uses instant/conservative only (R8).
- Ranking legendaries by margin — running the priced engine across all Gen 1 weapons and sorting is a later spec.
- Robust unpriced-item handling — treated as a non-issue (R3); no counting, flagging, or "incomplete" UI is built (human decision, 2026-08-01).
- Frontend rendering — 008 ships the engine, the extended endpoint, and the curation; the tree/profit UI is a frontend spec.
Assumptions
- Currency edges cost 0 gold —
RecipeEdgeofkind: 'currency'(Spirit Shards, tokens) is player- supplied, like a gated input. A recipe requiring Coin as a currency is a known gap, recorded not handled. - Buy/craft ties resolve to
'buy'— arbitrary but deterministic, so golden tests are stable. - Every buyable leaf has a usable TP price — in practice all real items trade on the TP; unpriced is a non-issue (R3). A genuinely missing price yields a
nullthat propagates without special handling. - 007's
RecipeGraphService.resolveis available with its documented shape and is not modified by 008; the 005Gw2Service.pricescaching (60 s TTL) / 206–404 / batching behaviour holds (gw2-api.md). - The Mystic Clover recipe is probabilistic; modelling it by expected yield (a fractional
outputCount) is an accepted approximation of its real gold cost, not an exact per-craft figure. - Adding R12's curated recipes does not require any change to 007's resolver code — the resolver already unions curated recipes into the graph; only data (and the R11 schema relaxation) change.
summaryis modeled as an optional property on the recursive node schema in the generated OpenAPI, not required-at-root (implementation note, R6/R10/SC12). A required-at-rootsummarywould need a separate root component, which breaks SC12's recursive$refcycle (children must$refthe same named component — thegenerate-openapi.test.tsguard asserts this). The "summary on root only" guarantee therefore holds at the type level (PricedRoot = PricedTreeNode & { summary }) and at runtime (the controller always populates it, only at the root), but the recursive OpenAPI schema documents it as optional-on-any-node. This is a documentation-fidelity trade-off, not a runtime gap.
Traceability
Each acceptance scenario and success criterion maps to a named test. Filled during implementation — titles below are transcribed verbatim from the shipped tests. Paths: eng = apps/api/src/recipe-graph/pricing.test.ts (pure priceGraph), ctl = apps/api/src/recipe-graph/recipe-graph.controller.test.ts (extended), svc = apps/api/src/recipe-graph/recipe-graph.service.bifrost.test.ts (real-data resolve of the Bifrost path), sch = apps/api/src/recipe-graph/recipe-graph.schema.test.ts (response schema) + packages/legendary-recipes/src/index.test.ts (fractional outputCount, curated dataset guards), oapi = apps/api/src/generate-openapi.test.ts, inv = the contract no-drift guard + the existing repo-invariant test. Amended by spec 012 — the contract half was tests/contract/no-drift.test.ts, deleted there as a duplicate of CI's own pnpm verify:contract step (see spec 004's P3 #2 row); the guarantee is unchanged, its owner is the CI step rather than a test.
| Criterion | Test |
|---|---|
| P1 #1 | eng — "P1 #1 / SC1: node carries unitBuyPrice+craftCost, unitCost=min, decision, tie→buy" |
| P1 #2 | eng — "P1 #2 / SC2: a buy-decision node keeps its expanded subtree and its children do not inflate the total" |
| P1 #3 | eng — "P1 #3 / SC3: cheapest recipe drives craftCost and the shown children" |
| P1 #4 | eng — "P1 #4 / SC4: a gated leaf is 0-gold, decision gated, excluded from totalCraftCost" |
| P1 #5 | eng — "P1 #5 / SC4: a gated-craftable node (Mystic Clover) is crafted, rolled up from buyable mats" |
| P1 #6 | eng — "P1 #6 / SC5: summary totalCraftCost/netSell/profit rolled up via unitCost" |
| P1 #7 | eng — "P1 #7 / SC5: root decision is always craft; rootBuyPrice surfaced even when cheaper" |
| P1 #8 | eng — "P1 #8 / SC6: a non-sellable root yields null netSell and profit, total still produced" |
| P2 #1 | ctl — "P2 #1 / SC8: GET returns 200, enriched tree + root summary, 007 fields intact" |
| P2 #2 | ctl — "P2 #2 / SC8: a non-integer param returns 400" / "… a non-positive param returns 400" / "… an unknown item (null root metadata) returns 404" |
| P2 #3 | ctl — "P2 #3 / SC8: one batched prices call over buyable ids ∪ root; resolve once" |
| P3 #1 | sch (legendary-recipes) — "P3 #1: the dataset curates exactly the four Bifrost forge intermediates (Might/Magic/Clover/Bifrost-gift)", "P3 #1: Gift of Might (19672) = 250× each of the four T6 fine materials …", "P3 #1: Gift of The Bifrost (19654) = 1 Gift of Energy + 1 Gift of Color + 100 Icy Runestone + 1 Sigil" |
| P3 #2 | svc — "P3 #2 / SC10: resolve(30698) expands the forge path to buyable raw materials" |
| P3 #3 | sch (legendary-recipes) — "P3 #3 / SC9: Mystic Clover (19675) is an expected-value forge recipe with a fractional outputCount" |
| P3 #4 | svc — "P3 #4: earned/vendor tokens remain leaves valued at 0" |
| SC1 | eng — "P1 #1 / SC1" |
| SC2 | eng — "P1 #2 / SC2" |
| SC3 | eng — "P1 #3 / SC3" |
| SC4 | eng — "P1 #4 / SC4" and "P1 #5 / SC4" |
| SC5 | eng — "P1 #6 / SC5", "P1 #7 / SC5", and "R3/SC5: a priced root whose craft path is poisoned yields null totalCraftCost and profit (no faked total)" |
| SC6 | eng — "P1 #8 / SC6" |
| SC7 | eng — "SC7: priceGraph is pure and deterministic over a frozen input" |
| SC8 | ctl — "P2 #1 / SC8", "P2 #2 / SC8" (×3), "P2 #3 / SC8"; sch — "SC8: the schema validates an enriched node with cost fields", "SC8: the root DTO accepts a summary", "SC8: a node missing unitCost is rejected (cost fields are required)" |
| SC9 | sch (legendary-recipes) — "SC9: a curated recipe with a fractional outputCount (expected-value yield) validates" (+ "… outputCount 0 is still rejected", "… ingredient count stays integer — 2.5 is rejected"); sch (schema) — "SC9: the response schema accepts a fractional recipe outputCount"; and P3 #3 above |
| SC10 | svc — "P3 #2 / SC10: the priced Bifrost has a non-zero, complete totalCraftCost" |
| SC11 | eng — "SC11: a PricedTree round-trips as plain JSON (no Map on the wire, summary + children intact)" |
| SC12 | oapi — "SC12: GET /recipe-graph/{itemId} documents the enriched priced shape (decision + summary)" + the deterministic no-drift guard (inv) |
| SC13 | inv — the existing docs/superpowers/ zero-write invariant (008 artifacts under specs/008-buy-vs-craft-pricing/) |
Also shipped beyond the table: eng — "R3: neither option defined → unitCost null, decision unknown, propagates" (R3 unknown propagation), and eng — "collectPricedIds returns the distinct buyable ids plus the root id, and nothing else" (the SC8 id set, unit-level).