{
  "openapi": "3.1.0",
  "info": {
    "title": "Kitchen Pass Chef API",
    "version": "1.0.0",
    "summary": "A head chef's verified answers for AI agents: recipe scaling, cooking fixes, ingredient swaps, yields, how much to buy and food cost.",
    "description": "Answers come from a bank of yes/no/depends questions answered by head chef Cristopher\n(data/questions.json). The live API uses only answered, verified questions: `provenance.label` reads\n\"Chef-verified by Cristopher\" when every source used is a verified question; engine defaults read\n\"Sample / unverified data — not chef-verified\". Every engine response (200, 400, 404, 413, 422) carries\n`dataVersion` and `provenance`; payment and service responses (402, 500, 502, 503) don't. Prices are never\nstored: callers always send their own.\n\n**English only.** Text in other scripts and emoji are rejected with 400: the safety guard can't read them, so they\nare never answered. Common look-alike letters (Cyrillic or Greek homoglyphs, small capitals) are read as their\nLatin equivalents, so the guard still sees them; look-alikes it can't map are rejected with 400.\n\n**Safety.** Questions touching allergens, preserving/shelf life, curing, fermentation safety or\nsafe cooking/holding/reheating temperatures are refused with `422` and a pointer to\nFood Standards Australia New Zealand. So are doneness, food for vulnerable eaters (pregnancy,\nbabies, the elderly, weak immunity) and naturally toxic or contaminated foods, reported as\n`food_safety_temperatures`. Kitchen Pass does not guess. The guard recognises words, not meaning: a question in\nwording it doesn't know isn't refused, and is answered only when it matches a listed wording (otherwise a free `404`). That applies to diagnose and substitute questions and to\nthe scale `method`. Recipe lines (/v1/scale) and ingredient names (/v1/yield-cost, /v1/how-much-to-buy) are\nscreened for what the recipe or purchase is, so finished products (bacon, kimchi, pickled onions) aren't refused:\nallergen-free wording, curing agents, cure / pickle / preserve / ferment recipes and unsafe-temperature wording\n(\"internal temperature\", \"danger zone\", \"undercooked\") are.\n\n**Payments (x402 v2, USDC on Base; test USDC on Base Sepolia while on testnet — see `servers`).** Input checks, the safety guard and the data lookup all run *before* the\npayment challenge, so 400, 404, 413 and 422 are answered free and never charged. Only a 200 is paid for.\n- A request with a valid body and no payment gets `402` with the terms in the `PAYMENT-REQUIRED` header\n  (base64 JSON: scheme `exact`, network, USDC asset, amount, pay-to address). An empty or `{}` body also gets\n  the 402, so directory crawlers can read the price.\n- Retry with the signed payment in `PAYMENT-SIGNATURE`; a 200 carries the receipt in `PAYMENT-RESPONSE`.\n- Any x402 v2 client does this for you (e.g. `@x402/fetch`). The v1 `X-PAYMENT` header isn't supported.\n- Our 400 means the recipe or question couldn't be read. Payment problems (bad signature, wrong amount,\n  reused payment, declined settlement) come back as 402 with a `reason`, never 400, and aren't charged.\n- Only EIP-3009 USDC authorisations (`transferWithAuthorization`, the default for USDC) are accepted.\n- One payment buys one answer: sending the same signed payment to several requests gets one 200.\n- If the payment processor can't say whether a payment settled, the chain is checked for the exact USDC\n  transfer to the pay-to address. Found: you get the answer. Not found in time: the answer is withheld\n  (`502 payment_unconfirmed` with a `reference`), and if the payment lands it is refunded by hand to the paying\n  address.\nStatus mapping lives in src/http.ts.\n"
  },
  "servers": [
    {
      "url": "https://kitchen-pass.netlify.app",
      "description": "Live (USDC on Base)"
    }
  ],
  "tags": [
    {
      "name": "kitchen"
    }
  ],
  "paths": {
    "/v1/scale": {
      "post": {
        "tags": [
          "kitchen"
        ],
        "operationId": "scaleRecipe",
        "x-payment-info": {
          "protocols": [
            "x402"
          ],
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.01"
          }
        },
        "summary": "Scale a recipe (4 to 50 servings, double, halve): each line straight, fixed-ratio or start-low, with the chef's reason.",
        "description": "Units: g, kg, ml, l, tsp, tbsp, lb, oz and cups. Spoons stay spoons. g rolls up to kg and ml to l; oz rolls up\nto lb, with the metric weight in brackets (\"1.5 lb (680 g)\"). oz is always weight: fluid ounces, pints, quarts\nand gallons are rejected. Cups stay cups, rounded to eighths and thirds, and are never converted to grams; with\na cup size (`cupSize`, or \"US cups\" / \"metric cups\" in the line) the exact ml is added. Ingredients are matched to the\nchef's scaling classes (salt, acids, garlic / onion / ginger, thickeners…; GET /v1/coverage lists them with their\nkeywords); each class has its own curve, and a class the chef has given a per-batch equipment limit splits the\nrecipe into batches when it goes over. Some classes apply only to a kind of recipe: salt in a cake or\nbread keeps a fixed ratio; in a soup or sauce it starts lower when scaling up. Each line comes back with a\nplain-English `why`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScaleRequest"
              },
              "examples": {
                "text": {
                  "value": {
                    "recipe": "3 kg bread flour; 2.3 l water; 60 g salt; 20 g yeast; 1 tbsp chilli flakes",
                    "fromPortions": 10,
                    "toPortions": 60
                  }
                },
                "structured": {
                  "value": {
                    "recipe": [
                      {
                        "qty": 2,
                        "unit": "kg",
                        "item": "beef brisket"
                      },
                      {
                        "qty": 1,
                        "unit": "tbsp",
                        "item": "salt"
                      },
                      {
                        "qty": 3,
                        "item": "eggs"
                      }
                    ],
                    "fromPortions": 12,
                    "toPortions": 40
                  }
                },
                "us": {
                  "value": {
                    "recipe": "2 lb beef chuck; 1 1/2 cups beef stock; 8 oz mushrooms; 1 tsp salt",
                    "fromPortions": 4,
                    "toPortions": 6,
                    "cupSize": "us"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scaled recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScaleResult"
                }
              }
            }
          },
          "400": {
            "description": "The request can't be read. Not charged. Kitchen Pass scales ingredients only: a line that names staff\n(\"3 kitchen hands\", \"1 sous chef\"), labour or time (\"10 hours cook labour at $32\", \"2 shifts\"), covers or\nguests (\"40 covers\", \"120 guests\") or money (\"50 dollars\") makes the whole request a 400; nothing is scaled.\nOnly the line's name counts: a note after a comma or in brackets (\"1 kg shoulder (slow cook 8 hours)\") is fine.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invalid"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "413": {
            "$ref": "#/components/responses/Invalid"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/PaymentProblem"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/yield-cost": {
      "post": {
        "tags": [
          "kitchen"
        ],
        "operationId": "yieldCost",
        "x-payment-info": {
          "protocols": [
            "x402"
          ],
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.01"
          }
        },
        "summary": "Raw vs cooked weight and cost per portion from what you paid, from the chef's trim and cook-loss ranges.",
        "description": "`ingredient` must be an exact name from the chef's yield questions or one of its listed aliases (case, punctuation,\nplurals and US / UK names ignored; a name without its bracketed detail also works when unique): \"brisket\", \"beef\nbrisket\", \"whole brisket\" and \"packer brisket\" all match. Anything else — \"chicken thighs\", \"peeled carrots\" — returns 404 with the closest\nnames in `suggestions`. Trim and cook loss come from yes/no thresholds, so results are ranges\n(best / mid / worst); an ingredient whose range isn't pinned down yet returns 404. Portion cost is given two ways,\neach labelled: whole-portion (price ÷ full portions) and proportional (share of the price by weight).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/YieldCostRequest"
              },
              "examples": {
                "metric": {
                  "value": {
                    "ingredient": "beef brisket",
                    "purchased": {
                      "qty": 5,
                      "unit": "kg"
                    },
                    "price": 95,
                    "currency": "AUD",
                    "portionSize": {
                      "qty": 180,
                      "unit": "g"
                    }
                  }
                },
                "us": {
                  "value": {
                    "ingredient": "beef brisket",
                    "purchased": {
                      "qty": 11,
                      "unit": "lb"
                    },
                    "price": 95,
                    "currency": "USD",
                    "portionSize": {
                      "qty": 6,
                      "unit": "oz"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Yield and cost breakdown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YieldCostResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/Invalid"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/PaymentProblem"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/diagnose": {
      "post": {
        "tags": [
          "kitchen"
        ],
        "operationId": "diagnose",
        "x-payment-info": {
          "protocols": [
            "x402"
          ],
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.02"
          }
        },
        "summary": "Why is my bread dense or hollandaise split? Dish + symptom → the likely cause and the chef's fix.",
        "description": "Answers only questions it fully understands: the dish must be a listed dish name, and every meaningful word\nof `symptom` must belong to a whole symptom wording of that dish that the question contains (\"it keeps\nsplitting\" matches \"split\"; \"tin\" alone doesn't match \"shrinking in the tin\"). Filler is read through (\"came out\ndense — what went wrong?\" asks what \"dense\" asks), and US, UK and Australian names and spellings are the same\n(\"roast veggies\", \"gray\"). Extra clauses (\"…split after I added the butter\") return 404 with the valid wordings\nin `suggestions`; negations (\"not split\") and rescue questions (\"split, can I save it?\") don't match. Safety\ntopics (\"…split, still ok to serve?\") return 422. The answer's `dish` is the dish name you matched and `symptom`\nthe first wording of the symptom list it matched.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DiagnoseRequest"
              },
              "example": {
                "dish": "hollandaise",
                "symptom": "it keeps splitting"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ranked causes and fixes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiagnoseResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/Invalid"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/PaymentProblem"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/substitute": {
      "post": {
        "tags": [
          "kitchen"
        ],
        "operationId": "substitute",
        "x-payment-info": {
          "protocols": [
            "x402"
          ],
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.01"
          }
        },
        "summary": "What can I use instead of buttermilk, self-raising flour or sour cream? The chef's answered swaps, each with a verdict (yes, depends or no).",
        "description": "`ingredient` must be an ingredient the chef has answered substitutions for (case, punctuation, plurals and\nUS / UK names ignored: \"self-rising flour\" is self-raising flour, \"cornstarch\" is cornflour); anything else\nreturns 404 with the covered ingredients in `suggestions`. Optionally name the\nreplacement you have in mind in `use` to get just that answer (404, with the answered replacements, if the chef\nhasn't answered it). Every option carries a verdict: yes; depends, with what it depends on in the chef's words;\nor no — a \"no\" is advice too (\"not cornflour for flour 1:1 to thicken\"). Food-safety framings (\"egg for my\nallergic guest\") are refused with 422.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubstituteRequest"
              },
              "examples": {
                "ingredient": {
                  "value": {
                    "ingredient": "buttermilk"
                  }
                },
                "pair": {
                  "value": {
                    "ingredient": "self-raising flour",
                    "use": "plain flour plus baking powder"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answered substitutions for that ingredient.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubstituteResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/Invalid"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/PaymentProblem"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/how-much-to-buy": {
      "post": {
        "tags": [
          "kitchen"
        ],
        "operationId": "howMuchToBuy",
        "x-payment-info": {
          "protocols": [
            "x402"
          ],
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.01"
          }
        },
        "summary": "How much to buy for N portions (best/mid/worst, rounded up), with optional cost per portion and food cost %.",
        "description": "Yield-cost run backwards. `portionSize` is the weight of one portion as served: cooked weight when the chef has\na cook-loss range for the ingredient, otherwise trimmed raw weight (`basis` says which). The weight to buy is\nneeded ÷ ((1 − trim) × (1 − cook loss)) across the chef's answered ranges: best = least loss (least to buy),\nworst = most loss (most to buy), mid = at the midpoint of both ranges. `buyDisplay` rounds up, never down: to\nthe next 0.1 kg from 1 kg, otherwise to the next 10 g, with lb (to the next 0.1 lb) in brackets when the portion\nis given in lb or oz or the price per lb. The worst case is the most loss within the chef's answered ranges, not\na guarantee: where he answered \"it depends\", the ranges are his typical figures and `notes` says what moves them.\n\nWith `pricePerKg` or `pricePerLb` (the price as bought; not both) you also get the cost in all and per portion,\nfrom the exact buy weight before rounding up; add `sellPrice` (the menu price of one portion, ex-tax) for this\ningredient's food-cost %. Ingredient names match as in /v1/yield-cost: an exact name or listed alias, otherwise\n404 with the closest names in `suggestions`; an ingredient whose range isn't pinned down yet also returns 404.\nA price under another name (/v1/yield-cost's `price` or `purchased`, or a misspelling such as `price_per_kg` or\n`sellprice`) gets a free 400 naming the field to use, never an answer without the cost.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HowMuchToBuyRequest"
              },
              "examples": {
                "metric": {
                  "value": {
                    "ingredient": "beef brisket",
                    "portions": 120,
                    "portionSize": {
                      "qty": 180,
                      "unit": "g"
                    },
                    "pricePerKg": 19.5,
                    "sellPrice": 32,
                    "currency": "AUD"
                  }
                },
                "us": {
                  "value": {
                    "ingredient": "beef brisket",
                    "portions": 40,
                    "portionSize": {
                      "qty": 6,
                      "unit": "oz"
                    },
                    "pricePerLb": 8.99,
                    "sellPrice": 24,
                    "currency": "USD"
                  }
                },
                "weightOnly": {
                  "value": {
                    "ingredient": "beef brisket",
                    "portions": 12,
                    "portionSize": {
                      "qty": 200,
                      "unit": "g"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How much to buy, and what it costs when a price is given.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HowMuchToBuyResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/Invalid"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/PaymentProblem"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/sample": {
      "get": {
        "tags": [
          "kitchen"
        ],
        "operationId": "sample",
        "summary": "Free sample — the focaccia scaling example, answered live from the same verified data as the paid routes.",
        "responses": {
          "200": {
            "description": "The sample request and the engine's answer to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SampleResult"
                }
              }
            }
          }
        }
      }
    },
    "/v1/coverage": {
      "get": {
        "tags": [
          "kitchen"
        ],
        "operationId": "coverage",
        "summary": "Free — the names of everything the paid routes answer today, never the answers.",
        "description": "Dishes with their symptom wordings, ingredients with answered swaps, yield ingredients with their aliases, and\nscaling classes with their keywords and tip topics, from the same answered, verified questions the paid routes\nuse; plus the food-safety topics that are always refused. Names only: causes, fixes, verdicts, amounts and\nranges come from the paid routes. A listed swap has an answer, which may be no. `yieldCost` ingredients are\nanswered by both /v1/yield-cost and /v1/how-much-to-buy (only ingredients whose ranges are pinned down, so they\ncan be costed).\n",
        "responses": {
          "200": {
            "description": "What's answered, by name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageResult"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "Invalid": {
        "description": "The request can't be read. Not charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Invalid"
            }
          }
        }
      },
      "Refused": {
        "description": "Safety refusal (allergens, preserving, curing, fermentation safety, safe temperatures and doneness, vulnerable eaters, toxic foods). Not charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Refused"
            }
          }
        }
      },
      "NotFound": {
        "description": "No data for that ingredient / dish / symptom yet; `suggestions` lists what exists. Not charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotFound"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "x402 payment challenge. Also returned, with `reason` set, when a payment is refused (bad signature, wrong amount,\nreused, declined at settlement, already used for another answer); nothing is charged for a refused payment.\nThe answer is withheld. The body is always the JSON summary below; the machine-readable terms are in the\nPAYMENT-REQUIRED header. After a declined settlement the 402 carries PAYMENT-RESPONSE (the failed settlement)\ninstead.\n",
        "headers": {
          "PAYMENT-REQUIRED": {
            "description": "Base64-encoded JSON x402 v2 PaymentRequired (x402Version, resource, accepts, extensions, error).",
            "schema": {
              "type": "string"
            }
          },
          "PAYMENT-RESPONSE": {
            "description": "Base64-encoded JSON x402 v2 SettleResponse; only after a declined settlement.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PaymentRequired"
            }
          }
        }
      },
      "PaymentProblem": {
        "description": "`payment_error`: the payment couldn't be checked (nothing charged; retry). `payment_unconfirmed`: settlement\ncouldn't be confirmed in time; the answer is withheld, and if the payment lands it is refunded by hand to the\npaying address. `reference` identifies the request in our records.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServiceError"
            }
          }
        }
      },
      "Unavailable": {
        "description": "The payment service is unreachable, the payment couldn't be recorded, or the request ran out of time before payment. Nothing charged; retry with a new payment.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServiceError"
            }
          }
        }
      },
      "ServerError": {
        "description": "Server error or misconfiguration. Nothing is charged; if a payment was taken, it is refunded by hand to the paying address.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServiceError"
            }
          }
        }
      }
    },
    "schemas": {
      "Unit": {
        "type": "string",
        "enum": [
          "g",
          "kg",
          "ml",
          "l",
          "tsp",
          "tbsp",
          "lb",
          "oz",
          "cup"
        ]
      },
      "WeightAmount": {
        "type": "object",
        "required": [
          "qty",
          "unit"
        ],
        "description": "Between 1 g and 10 t in total (0.0353 oz to 22,046 lb; lb and oz are converted exactly).",
        "properties": {
          "qty": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000000
          },
          "unit": {
            "type": "string",
            "enum": [
              "g",
              "kg",
              "lb",
              "oz"
            ]
          }
        }
      },
      "RecipeLineInput": {
        "type": "object",
        "required": [
          "qty",
          "item"
        ],
        "properties": {
          "qty": {
            "type": "number",
            "minimum": 0.0001,
            "maximum": 1000000
          },
          "unit": {
            "description": "Omit or null for counted items like eggs. mg is also accepted (converted to g), and cl, dl and hl (converted to ml). lb and oz are weight (never fluid ounces). cup or cups stay cups; \"US cup(s)\" or \"metric cup(s)\" also set the cup size. fl oz, pints, quarts, gallons, \"c\", \"T\" and \"t\" are rejected.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Unit"
              },
              {
                "type": "string",
                "enum": [
                  "cups",
                  "mg",
                  "cl",
                  "dl",
                  "hl",
                  "US cup",
                  "US cups",
                  "metric cup",
                  "metric cups"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "item": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          }
        }
      },
      "ProvenanceRow": {
        "type": "object",
        "required": [
          "table",
          "key",
          "verified"
        ],
        "properties": {
          "table": {
            "type": "string",
            "enum": [
              "questions",
              "engine"
            ],
            "description": "questions = a chef-answered question (key = its id); engine = built-in behaviour no answered question covers (never verified)."
          },
          "key": {
            "type": "string"
          },
          "verified": {
            "type": "boolean"
          }
        }
      },
      "Provenance": {
        "type": "object",
        "required": [
          "dataVersion",
          "verified",
          "label",
          "source",
          "rowsUsed"
        ],
        "properties": {
          "dataVersion": {
            "type": "string",
            "example": "sample-0.1+6aeea187"
          },
          "verified": {
            "type": "boolean",
            "description": "True only if rowsUsed is non-empty and every row is verified."
          },
          "label": {
            "type": "string",
            "enum": [
              "Chef-verified by Cristopher",
              "Sample / unverified data — not chef-verified"
            ]
          },
          "source": {
            "type": "string"
          },
          "rowsUsed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProvenanceRow"
            }
          }
        }
      },
      "Meta": {
        "type": "object",
        "required": [
          "dataVersion",
          "provenance"
        ],
        "properties": {
          "dataVersion": {
            "type": "string"
          },
          "provenance": {
            "$ref": "#/components/schemas/Provenance"
          }
        }
      },
      "ScaleRequest": {
        "type": "object",
        "required": [
          "recipe",
          "fromPortions",
          "toPortions"
        ],
        "properties": {
          "recipe": {
            "description": "Text (\"2 kg brisket; 500 ml stock; 1 tbsp salt; 3 eggs\", ; or newline separated, one amount per line, max 200 characters a line) or structured lines. Max 40 lines. Mixed numbers (\"1 1/2 kg\") are read as one amount; \"1,5 kg\", \"2-3 cloves\" and \"1 kg 500 g\" are rejected.",
            "oneOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 4000
              },
              {
                "type": "array",
                "minItems": 1,
                "maxItems": 40,
                "items": {
                  "$ref": "#/components/schemas/RecipeLineInput"
                }
              }
            ]
          },
          "fromPortions": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 10000
          },
          "toPortions": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 10000
          },
          "method": {
            "type": "string",
            "maxLength": 40,
            "description": "Optional cooking method (\"roast\", \"fry\", \"stir-fry\") — selects method-specific tips. Screened like a question: a cure, ferment, preserve or holding method (\"cold smoke\", \"pickling\", \"sous vide\") is refused."
          },
          "cupSize": {
            "type": "string",
            "enum": [
              "us",
              "us-legal",
              "metric"
            ],
            "description": "Cup size for lines in cups: us = 236.5882365 ml, us-legal = 240 ml (US nutrition label), metric = 250 ml. Lines saying \"US cups\" or \"metric cups\" carry their own (\"US cups\" with us-legal is 240 ml); metric against a US size is rejected. Without a size, cups are scaled as cups with no ml and are not counted toward volume batch limits. Cups are never converted to grams, so they never count as a weight: a recipe with a line in cups gets no baker's percentage, and a rule that goes by the recipe's proportions by weight (salt in a cake or bread) can't be checked. When the recipe could be a cake or bread, its salt then scales straight as an engine default and the whole answer is labelled not chef-verified (the line's undecidedClass names the rule); when the known ingredients already rule that out (no flour, stock in it), the usual salt rule applies. Give weights to get the chef's rule."
          }
        }
      },
      "ScaledLine": {
        "type": "object",
        "required": [
          "input",
          "item",
          "unit",
          "cupSize",
          "ingredientClass",
          "undecidedClass",
          "approach",
          "original",
          "linear",
          "scaled",
          "startWith",
          "perBatch",
          "display",
          "perBatchDisplay",
          "why"
        ],
        "properties": {
          "input": {
            "type": "string"
          },
          "item": {
            "type": "string"
          },
          "unit": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Unit"
              },
              {
                "type": "null"
              }
            ],
            "description": "The line's unit as given, with these changes: mg comes back as g; cl, dl and hl as ml; cups, US cup(s) and metric cup(s) as cup (lb and oz stay lb and oz). original, linear, scaled, startWith and perBatch are in this unit."
          },
          "cupSize": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "us",
              "us-legal",
              "metric",
              null
            ],
            "description": "Cup lines only: the cup size used, or null when none was given. Null for other units."
          },
          "ingredientClass": {
            "type": "string",
            "description": "The chef's rule class that decided the line, or \"default\" (straight maths)."
          },
          "undecidedClass": {
            "type": [
              "string",
              "null"
            ],
            "description": "A \"default\" line that one of the chef's rules may cover but that couldn't be decided: the recipe's weights aren't known (a line in cups, a tin with no size), or it is a kind of recipe he hasn't answered for yet (a batter, a scone, a pizza with meat on it). The line scales straight as an engine default, the why says which, and the answer isn't labelled chef-verified. Null otherwise."
          },
          "approach": {
            "type": "string",
            "enum": [
              "exact",
              "straight",
              "start-low",
              "curve"
            ],
            "description": "exact = fixed ratio; straight = straight maths; start-low = straight maths is the most — start below it and taste up (display reads 'up to …'); curve = a fixed seasoning curve."
          },
          "original": {
            "type": "number"
          },
          "linear": {
            "type": "number",
            "description": "Straight multiplication, for comparison."
          },
          "scaled": {
            "type": "number",
            "description": "Total to use across all batches (for start-low lines: the most, i.e. straight maths)."
          },
          "startWith": {
            "type": [
              "number",
              "null"
            ],
            "description": "Start-low lines where the chef gave a fraction (e.g. ¾ of straight maths): the amount to start with, tasting up to `scaled`."
          },
          "perBatch": {
            "type": [
              "number",
              "null"
            ]
          },
          "display": {
            "type": "string",
            "example": "360 g salt"
          },
          "perBatchDisplay": {
            "type": [
              "string",
              "null"
            ]
          },
          "why": {
            "type": "string",
            "description": "Plain-English account of what changed and why."
          }
        }
      },
      "ScaleResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "fromPortions",
              "toPortions",
              "factor",
              "batches",
              "lines",
              "tips"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "fromPortions": {
                "type": "number"
              },
              "toPortions": {
                "type": "number"
              },
              "factor": {
                "type": "number"
              },
              "batches": {
                "type": "object",
                "required": [
                  "count",
                  "portionsPerBatch",
                  "reason"
                ],
                "properties": {
                  "count": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "portionsPerBatch": {
                    "type": "number"
                  },
                  "reason": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              "lines": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ScaledLine"
                }
              },
              "tips": {
                "type": "array",
                "description": "The chef's technique tips that apply to this scaling (ingredient-based, or matching `method`).",
                "items": {
                  "type": "object",
                  "required": [
                    "topic",
                    "text",
                    "note",
                    "source"
                  ],
                  "properties": {
                    "topic": {
                      "type": "string"
                    },
                    "text": {
                      "type": "string"
                    },
                    "note": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "source": {
                      "type": "string",
                      "description": "Question id."
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "YieldCostRequest": {
        "type": "object",
        "required": [
          "ingredient",
          "purchased",
          "price"
        ],
        "properties": {
          "ingredient": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "purchased": {
            "$ref": "#/components/schemas/WeightAmount"
          },
          "price": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 1000000000,
            "description": "Total paid for the purchased quantity."
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Za-z]{3}$",
            "default": "AUD"
          },
          "portions": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000,
            "description": "Split the yield into this many portions: cooked when the chef has a cook-loss range for the ingredient, otherwise trimmed raw (`basis` says which). Not with portionSize."
          },
          "portionSize": {
            "$ref": "#/components/schemas/WeightAmount",
            "description": "Weight of one portion as served: cooked when the chef has a cook-loss range for the ingredient, otherwise trimmed raw (`basis` says which). Not with portions."
          }
        }
      },
      "Range": {
        "type": "object",
        "description": "best = least loss (most food, lowest cost); worst = most loss; mid = at the midpoint of the answered loss ranges.",
        "required": [
          "best",
          "mid",
          "worst"
        ],
        "properties": {
          "best": {
            "type": "number"
          },
          "mid": {
            "type": "number"
          },
          "worst": {
            "type": "number"
          }
        }
      },
      "LabelledRange": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Range"
          },
          {
            "type": "object",
            "required": [
              "label"
            ],
            "properties": {
              "label": {
                "type": "string"
              }
            }
          }
        ]
      },
      "YieldCostResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "ingredient",
              "purchaseUnit",
              "currency",
              "price",
              "basis",
              "purchasedWeightG",
              "trimPct",
              "cookLossPct",
              "usableWeightG",
              "cookedWeightG",
              "costPerKg",
              "portion",
              "notes",
              "workings"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "ingredient": {
                "type": "string"
              },
              "purchaseUnit": {
                "type": "string"
              },
              "currency": {
                "type": "string"
              },
              "price": {
                "type": "number",
                "description": "Echo of the caller's price. Kitchen Pass never stores prices."
              },
              "basis": {
                "type": "string",
                "enum": [
                  "cooked",
                  "trimmed"
                ],
                "description": "trimmed = the bank asks no cook-loss question for this ingredient; figures use the trimmed raw weight."
              },
              "purchasedWeightG": {
                "type": "number"
              },
              "trimPct": {
                "type": "object",
                "description": "From the chef's yes/no thresholds (\"trim over 10%?\" yes, \"over 20%?\" no → 10–20).",
                "required": [
                  "low",
                  "high"
                ],
                "properties": {
                  "low": {
                    "type": "number"
                  },
                  "high": {
                    "type": "number"
                  }
                }
              },
              "cookLossPct": {
                "type": "object",
                "required": [
                  "low",
                  "high"
                ],
                "properties": {
                  "low": {
                    "type": "number"
                  },
                  "high": {
                    "type": "number"
                  }
                }
              },
              "usableWeightG": {
                "$ref": "#/components/schemas/Range"
              },
              "cookedWeightG": {
                "$ref": "#/components/schemas/Range"
              },
              "costPerKg": {
                "type": "object",
                "required": [
                  "purchased",
                  "usable",
                  "cooked"
                ],
                "properties": {
                  "purchased": {
                    "type": "number"
                  },
                  "usable": {
                    "$ref": "#/components/schemas/Range"
                  },
                  "cooked": {
                    "$ref": "#/components/schemas/Range"
                  }
                }
              },
              "portion": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "required": [
                      "basis",
                      "sizeG",
                      "fullPortions",
                      "costPerPortion"
                    ],
                    "properties": {
                      "basis": {
                        "type": "string",
                        "enum": [
                          "cooked weight",
                          "trimmed weight"
                        ]
                      },
                      "sizeG": {
                        "$ref": "#/components/schemas/Range"
                      },
                      "fullPortions": {
                        "$ref": "#/components/schemas/Range"
                      },
                      "costPerPortion": {
                        "type": "object",
                        "required": [
                          "wholePortion",
                          "proportional"
                        ],
                        "properties": {
                          "wholePortion": {
                            "$ref": "#/components/schemas/LabelledRange",
                            "description": "Price ÷ full portions; the leftover is waste."
                          },
                          "proportional": {
                            "$ref": "#/components/schemas/LabelledRange",
                            "description": "The portion's share of the price by weight."
                          }
                        }
                      }
                    }
                  }
                ]
              },
              "notes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Chef's notes from D (depends) answers behind these numbers."
              },
              "workings": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "DiagnoseRequest": {
        "type": "object",
        "required": [
          "dish",
          "symptom"
        ],
        "properties": {
          "dish": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "symptom": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          }
        }
      },
      "DiagnoseResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "dish",
              "symptom",
              "causes"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "dish": {
                "type": "string",
                "description": "The dish name you matched, as the chef's bank spells it (\"béarnaise sauce\" → bearnaise)."
              },
              "symptom": {
                "type": "string",
                "description": "The first wording of the symptom list you matched (\"it keeps splitting\" → split)."
              },
              "causes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "rank",
                    "cause",
                    "fix",
                    "note",
                    "verified"
                  ],
                  "properties": {
                    "rank": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "cause": {
                      "type": "string"
                    },
                    "fix": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null when the question bank names the cause but not a fix."
                    },
                    "note": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The chef's note when the answer was D (depends)."
                    },
                    "verified": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "SubstituteRequest": {
        "type": "object",
        "required": [
          "ingredient"
        ],
        "properties": {
          "ingredient": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "What you want to replace."
          },
          "use": {
            "type": "string",
            "maxLength": 80,
            "description": "Optional: the replacement you have in mind."
          }
        }
      },
      "SubstituteResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "ingredient",
              "options"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "ingredient": {
                "type": "string"
              },
              "options": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "use",
                    "verdict",
                    "amount",
                    "context",
                    "dependsOn",
                    "source",
                    "verified"
                  ],
                  "properties": {
                    "use": {
                      "type": "string"
                    },
                    "verdict": {
                      "type": "string",
                      "enum": [
                        "yes",
                        "depends",
                        "no"
                      ]
                    },
                    "amount": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "context": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Where the answer applies, e.g. in baking."
                    },
                    "dependsOn": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "On a depends verdict: what it depends on, in the chef's words."
                    },
                    "source": {
                      "type": "string",
                      "description": "The question id behind this answer."
                    },
                    "verified": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "HowMuchToBuyRequest": {
        "type": "object",
        "required": [
          "ingredient",
          "portions",
          "portionSize"
        ],
        "properties": {
          "ingredient": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "portions": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000,
            "description": "How many portions to buy for."
          },
          "portionSize": {
            "$ref": "#/components/schemas/WeightAmount",
            "description": "Weight of one portion as served — cooked when the chef has a cook-loss range for the ingredient, otherwise trimmed raw (the response's `basis` says which). portions × portionSize is at most 10 t."
          },
          "pricePerKg": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 1000000000,
            "description": "Price per kg as bought. Not with pricePerLb."
          },
          "pricePerLb": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 1000000000,
            "description": "Price per lb as bought. Not with pricePerKg."
          },
          "sellPrice": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 1000000000,
            "description": "Menu price of one portion, excluding GST/VAT/sales tax. Needs pricePerKg or pricePerLb (400 without)."
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Za-z]{3}$",
            "default": "AUD",
            "description": "Display only."
          }
        }
      },
      "HowMuchToBuyResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "ingredient",
              "purchaseUnit",
              "basis",
              "portions",
              "portionSizeG",
              "neededG",
              "trimPct",
              "cookLossPct",
              "buyWeightG",
              "buyDisplay",
              "cost",
              "foodCostPct",
              "foodCostLabel",
              "notes",
              "workings"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "ingredient": {
                "type": "string"
              },
              "purchaseUnit": {
                "type": "string"
              },
              "basis": {
                "type": "string",
                "enum": [
                  "cooked",
                  "trimmed"
                ],
                "description": "cooked = portions are cooked weight (trim and cook loss applied). trimmed = the bank asks no cook-loss question for this ingredient, so portions are trimmed raw weight."
              },
              "portions": {
                "type": "integer"
              },
              "portionSizeG": {
                "type": "number"
              },
              "neededG": {
                "type": "number",
                "description": "portions × portionSizeG."
              },
              "trimPct": {
                "type": "object",
                "required": [
                  "low",
                  "high"
                ],
                "properties": {
                  "low": {
                    "type": "number"
                  },
                  "high": {
                    "type": "number"
                  }
                }
              },
              "cookLossPct": {
                "type": "object",
                "description": "0–0 on a trimmed basis.",
                "required": [
                  "low",
                  "high"
                ],
                "properties": {
                  "low": {
                    "type": "number"
                  },
                  "high": {
                    "type": "number"
                  }
                }
              },
              "buyWeightG": {
                "$ref": "#/components/schemas/Range",
                "description": "Grams to buy, as bought. best = least loss (least to buy); worst = most loss (most to buy)."
              },
              "buyDisplay": {
                "type": "object",
                "description": "The exact buy weight (not buyWeightG, which is to the nearest 0.1 g) rounded up — to the next 0.1 kg from 1 kg, otherwise the next 10 g; lb (next 0.1 lb) in brackets when the caller used lb, oz or pricePerLb.",
                "required": [
                  "best",
                  "mid",
                  "worst"
                ],
                "properties": {
                  "best": {
                    "type": "string",
                    "example": "38.6 kg"
                  },
                  "mid": {
                    "type": "string",
                    "example": "41.3 kg"
                  },
                  "worst": {
                    "type": "string",
                    "example": "44.4 kg"
                  }
                }
              },
              "cost": {
                "description": "Only with pricePerKg or pricePerLb. From the exact buy weight, before rounding up.",
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "required": [
                      "currency",
                      "pricePerKg",
                      "total",
                      "perPortion"
                    ],
                    "properties": {
                      "currency": {
                        "type": "string"
                      },
                      "pricePerKg": {
                        "type": "number",
                        "description": "As bought; converted from pricePerLb when that was given."
                      },
                      "total": {
                        "$ref": "#/components/schemas/Range"
                      },
                      "perPortion": {
                        "$ref": "#/components/schemas/Range"
                      }
                    }
                  }
                ]
              },
              "foodCostPct": {
                "description": "Only with a price and sellPrice. Cost per portion ÷ sellPrice × 100.",
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/Range"
                  }
                ]
              },
              "foodCostLabel": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "What foodCostPct is: this ingredient's cost as a % of the menu price (ex-tax), not the whole plate."
              },
              "notes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Chef's notes from D (depends) answers behind these numbers."
              },
              "workings": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "Refused": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "topic",
              "matched",
              "message"
            ],
            "properties": {
              "status": {
                "const": "refused"
              },
              "topic": {
                "type": "string",
                "enum": [
                  "allergens",
                  "preserving",
                  "curing",
                  "fermentation_safety",
                  "food_safety_temperatures"
                ],
                "description": "food_safety_temperatures also covers doneness, vulnerable eaters and naturally toxic or contaminated foods; preserving covers spoilage."
              },
              "matched": {
                "type": "string",
                "description": "The words that triggered the refusal."
              },
              "message": {
                "type": "string"
              }
            }
          }
        ]
      },
      "Invalid": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "message"
            ],
            "properties": {
              "status": {
                "const": "invalid"
              },
              "message": {
                "type": "string"
              }
            }
          }
        ]
      },
      "NotFound": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "required": [
              "status",
              "message",
              "suggestions"
            ],
            "properties": {
              "status": {
                "const": "not_found"
              },
              "message": {
                "type": "string"
              },
              "suggestions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "PaymentRequired": {
        "type": "object",
        "required": [
          "status",
          "price",
          "message"
        ],
        "properties": {
          "status": {
            "const": "payment_required"
          },
          "price": {
            "type": "string",
            "example": "$0.01 USDC"
          },
          "message": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "Only on a refused payment: why (e.g. payment_already_used, insufficient_funds)."
          }
        }
      },
      "ServiceError": {
        "type": "object",
        "required": [
          "status",
          "message"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "payment_error",
              "payment_unconfirmed",
              "unavailable",
              "error"
            ]
          },
          "message": {
            "type": "string"
          },
          "reference": {
            "type": "string",
            "description": "Only on payment_unconfirmed: identifies the request in our records."
          }
        }
      },
      "CoverageResult": {
        "type": "object",
        "required": [
          "dataVersion",
          "note",
          "scale",
          "diagnose",
          "substitute",
          "yieldCost",
          "refused"
        ],
        "properties": {
          "dataVersion": {
            "type": "string",
            "description": "The data version the paid routes answer from."
          },
          "note": {
            "type": "string"
          },
          "scale": {
            "type": "object",
            "required": [
              "rules",
              "tips"
            ],
            "properties": {
              "rules": {
                "type": "array",
                "description": "Scaling classes the chef's answers set, with the bank's keywords for each: a keyword matches as the head of a line (\"table salt\" is salt, \"garlic salt\" isn't). Two classes are scoped to a kind of recipe and apply only there: \"salt in cakes & bread\" when the recipe is a cake or bread by its weighed flour, sugar, eggs and raising agent or yeast (not with meat, seafood or stock), and \"acid with oil (vinaigrette)\" when the recipe has oil. Elsewhere the same keywords fall to the general class (\"salt & salty seasonings\", \"acids\"). A line no class covers scales straight.",
                "items": {
                  "type": "object",
                  "required": [
                    "class",
                    "keywords"
                  ],
                  "properties": {
                    "class": {
                      "type": "string"
                    },
                    "keywords": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "tips": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Topics of the technique tips a scale answer can carry."
              }
            }
          },
          "diagnose": {
            "type": "array",
            "description": "One entry per answered dish and symptom list. Any dish name here with any of its symptom wordings is answered.",
            "items": {
              "type": "object",
              "required": [
                "dish",
                "symptoms"
              ],
              "properties": {
                "dish": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "symptoms": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "substitute": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "ingredient",
                "replacements"
              ],
              "properties": {
                "ingredient": {
                  "type": "string"
                },
                "replacements": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Replacements the chef has answered for it (the verdict — yes, depends or no — is in the paid answer)."
                }
              }
            }
          },
          "yieldCost": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "ingredient",
                "aliases",
                "basis"
              ],
              "properties": {
                "ingredient": {
                  "type": "string"
                },
                "aliases": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "basis": {
                  "type": "string",
                  "enum": [
                    "cooked",
                    "trimmed"
                  ],
                  "description": "cooked = trim and cook loss both answered; trimmed = no cook-loss question, so portions are trimmed raw weight."
                }
              }
            }
          },
          "refused": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Food-safety topics that are never answered: refused with a free 422 when the guard recognises the wording, otherwise a free 404. The guard recognises words, not meaning."
          }
        }
      },
      "SampleResult": {
        "type": "object",
        "required": [
          "sample",
          "note",
          "request",
          "response"
        ],
        "properties": {
          "sample": {
            "const": true
          },
          "note": {
            "type": "string"
          },
          "request": {
            "$ref": "#/components/schemas/ScaleRequest"
          },
          "response": {
            "$ref": "#/components/schemas/ScaleResult"
          }
        }
      }
    }
  }
}
