# Phase 3 — the recipe layer

Phase 2 produced *evidence dossiers*: what a product is, what it's made of,
what it declares nutritionally, and — for the benchmark — mass-balanced
formulation **hypotheses**. Phase 3 turns a dossier into a **reconstructed
recipe**: gram-precise ingredients, a cooking-yield model, beginner
instructions, computed nutrition, and a match score against the real product.

This layer **extends** Phase 1/2. It never edits a dossier. A recipe is a new
record in `data/recipes/<id>.json`, keyed by the same `id` as its dossier, and
it carries a pointer back to the evidence it was built from. Delete every
recipe and the evidence base is untouched.

The one rule that governs everything here is inherited from Phase 2 and
sharpened by spec §92:

> **A reconstruction is never "the recipe" and never "tested".**
> Every recipe is `Modeled / Evidence-supported reconstruction / Awaiting
> physical validation` until a human actually cooks it and compares it to the
> real product. The schema makes "we tested this" impossible to assert without
> a `userTests[]` entry that records who, when, and against which product.

## Where a recipe's numbers come from (and where they don't)

| Block | Source | Provenance |
|---|---|---|
| Ingredient **identity** & **order** | the dossier's verbatim declaration | `official_declaration` |
| Ingredient **grams** | best-fit formulation % × batch weight | `estimated_from_ingredients` |
| Cooking **yield** | generic food-science yield factors | `culinary_inference` |
| Recipe **nutrition** | computed from the gram list + a labeled generic food-composition table | `estimated_from_ingredients` |
| **Match score** | computed recipe nutrition vs the dossier's declared panel | computed |
| Steps, cues, timings | culinary reasoning from the component roles | `culinary_inference` |

The food-composition reference table (`js/nutrition-ref.js`) is **generic
public per-100g data, not Spinneys data** — it exists only to test whether a
candidate gram list could plausibly produce the declared panel, exactly as the
Phase-2 formulation reference did. It is labeled as such on every record.

## The three confidences (spec §3)

One overall score would hide exactly the thing this project refuses to hide.
So a recipe carries three, each computed, never asserted:

| Confidence | Question it answers | Computed from |
|---|---|---|
| `productConfidence` | How sure are we the *product facts* are right? | the dossier's `evidenceScore` (0–100) |
| `recipeConfidence` | How sure are we this *formulation* approximates the real thing? | evidence completeness × nutrition fit × formulation determinacy |
| `nutritionMatch` | How closely does computed nutrition match the declared panel? | 100 − mean(|deviation%| across calories/protein/fat/carbs) |

`recipeConfidence` is deliberately capped: with a declaration + nutrition panel
but no photo and no serving weight, a salad reconstruction tops out around
**82** (the Phase-2 benchmark figure). It can only exceed that with physical
user-test evidence, which no recipe has yet.

## Status pipeline (spec §88)

```
EVIDENCE_READY → FORMULATION_TESTING → BEST_FORMULA_SELECTED
  → INSTRUCTION_DEVELOPMENT → NUTRITION_VALIDATION → CULINARY_VALIDATION
  → READY_FOR_USER_TEST → USER_TESTED → VALIDATED → PUBLISHED
```

The generator can carry a recipe automatically up to **READY_FOR_USER_TEST**.
It **cannot** move a recipe past that line — `USER_TESTED` and beyond require a
`userTests[]` entry, which only a real person cooking the real comparison can
create (§92, §93). This boundary is enforced in `js/recipe-qa.js`, not by
convention.

## Publish / user-test gate (spec §89)

A recipe may reach `READY_FOR_USER_TEST` only if ALL hold:

- ingredient grams total to the stated batch weight (±2%)
- final yield is plausible (0.6–1.05 × raw weight for these cold/lightly-cooked dishes)
- every ingredient in `ingredients[]` is used in a step, and every step ingredient is listed
- allergens are mapped (or explicitly `unknown` with a gap)
- nutrition is computed with `estimated_from_ingredients` provenance and a per-100g + per-serving basis
- `nutritionMatch` ≥ 55 against at least one declared version
- storage + food-safety guidance present
- every uncertainty is disclosed in `gaps[]` or a `conflicts` pointer

A recipe failing any of these stays at its computed pre-gate status and renders
with a `RESEARCH ONLY` / `FORMULATION IN DEVELOPMENT` banner (§131) — never as
final instructions.

## Automated QA (spec §90)

`js/recipe-qa.js` raises a warning (and, for the starred ones, blocks the
gate) for:

- ★ ingredient listed but never used in a step
- ★ ingredient used in a step but not in the ingredient list
- ★ macros inconsistent with calories (Atwater > ±20%)
- ★ ingredient percentages summing outside 98–102%
- ★ missing serving weight
- ★ unknown/unmapped allergen token
- ★ timer referenced but no duration
- impossible cooking temperature (e.g. chicken target < 74 °C)
- unsafe storage claim (fridge life > 4 days for a mayo-bound or seafood salad)
- a household measure that rounds to an awkward fraction (display-quality only)

## Record shape

```jsonc
{
  "id": "spk-chicken-salad",              // same id as the dossier
  "slug": "spinneys-chicken-salad",
  "dossierRef": "data/dossiers/spk-chicken-salad.json",
  "name": "Chicken Salad",
  "category": "salads",
  "evidenceType": "VERIFIED_PRODUCT",     // RECONSTRUCTED recipe on a verified product
  "reconstructionLabel": "REVERSE_ENGINEERED",  // VERIFIED_RECIPE|VERIFIED_PRODUCT_RECONSTRUCTED|REVERSE_ENGINEERED|INSPIRED

  "status": "READY_FOR_USER_TEST",
  "confidence": { "product": 80, "recipe": 74, "nutritionMatch": 88, "basisVersion": "portal-2026" },

  "batch": { "targetTotalG": 500, "servings": 5, "servingWeightG": 100, "servingWeightProvenance": "culinary_inference" },
  "formulation": { "modelKey": "model-d", "provenance": "estimated_from_ingredients", "note": "..." },

  "yield": { "rawTotalG": 560, "cookedTotalG": 500, "overallYieldPct": 89.3, "perComponent": [ /* raw→cooked */ ] },

  "ingredients": [ /* see below */ ],
  "masterComponents": ["cmp-poached-chicken", "cmp-citrus-mayo"],   // §74
  "equipment": [ { "name": "Mixing bowl", "essential": true, "ifYouDontHave": null } ],
  "miseEnPlace": [ "Dice the capsicum into 5 mm pieces", /* ... */ ],
  "timeline": [ { "atMin": 0, "action": "Start the chicken" }, /* ... */ ],
  "steps": [ /* cue-rich step shape, see below */ ],

  "nutrition": {
    "provenance": "estimated_from_ingredients",
    "per100g": { "calories": 0, "protein": 0, "carbs": 0, "fat": 0, "saturatedFat": 0, "sodium": 0 },
    "perServing": { /* same keys */ },
    "matchVsDeclared": { "version": "portal-2026", "deviations": { "calories": "+2%" }, "score": 88 }
  },
  "allergens": { "provenance": "official_declaration", "contains": ["egg","gluten"], "unknown": false },
  "storage": { "fridgeDays": 2, "freezable": false, "foodSafety": "..." },

  "sensory": { /* §55 predicted 0–10 scores, provenance culinary_inference */ },
  "variations": [],                        // never mutate the base reconstruction
  "userTests": [],                          // empty until a real person cooks + compares (§92)
  "qa": { "passedGate": true, "warnings": [] },
  "gaps": ["serving_weight","official_photo"],
  "engineVersion": 1,
  "generatedAt": "2026-08-19"
}
```

### Ingredient shape (spec §10, §20)

```jsonc
{
  "name": "Mayonnaise",
  "grams": 65, "display": "65 g", "household": "≈ 4 tbsp",
  "prep": null, "purpose": "Binds the salad; provides fat and creamy mouthfeel",
  "essential": true, "role": "binder",
  "substitutes": [ { "name": "Greek yogurt + 1 tsp oil", "rating": "acceptable", "impact": "Lighter, tangier, less rich" } ],
  "allergens": ["egg"], "provenance": "official_declaration"
}
```

### Step shape (spec §76 — cooking-mode ready)

```jsonc
{
  "n": 3, "title": "Cook the chicken", "instruction": "...", "beginnerInstruction": "...",
  "durationMin": 12, "tempC": null, "internalTempC": 74, "timerLabel": "Chicken poach",
  "visualCue": "...", "soundCue": "...", "aromaCue": "...", "textureCue": "...",
  "whyItMatters": "...", "commonMistake": "...", "howToFix": "...",
  "ingredientsUsed": ["Chicken breast"], "equipmentUsed": ["Saucepan"],
  "glossary": [ { "term": "poach", "explains": "cook gently in water below a boil" } ]  // §145
}
```

## What this layer deliberately does NOT do

- **No bakery formulas.** The 25 reconstructable products are all cold
  salads/sandwiches. Bakery has 0% ingredient-declaration evidence in Phase 2
  (`docs/PHASE2_REPORT.md`), so there is nothing to reverse-engineer *from*.
  Inventing a croissant formula and labeling it "Spinneys" is the exact
  fabrication this project forbids. Bakery stays out until real declaration
  evidence exists.
- **No claimed tasting.** Every `sensory` score is `culinary_inference`
  (predicted), and `userTests` is empty on every recipe. The word "tested"
  appears nowhere on a generated recipe.
