{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://psigen.github.io/sous/schema/v0/recipe.schema.json",
  "title": "Sous Recipe",
  "description": "A portable, equipment-independent recipe. Declares the material STATES the dish requires and a partially ordered task network over operations, leaving the procedure that reaches those states to be completed by a planner against a specific kitchen. Equipment is referenced by capability CLASS, never by device: that separation is what makes the document portable (ISA-88's general recipe vs control recipe).",
  "type": "object",
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri",
      "description": "Points at this schema so editors offer completion and inline validation. This is how Sous documents become IDE-authorable."
    },
    "sous_version": {
      "type": "string",
      "const": "0.1.0",
      "description": "Sous specification version this document conforms to."
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "description": "Stable identifier for this recipe, unique within its publisher's namespace."
    },
    "title": {
      "type": "string",
      "minLength": 1
    },
    "description": {
      "type": "string"
    },
    "specificity": {
      "type": "string",
      "enum": [
        "complete",
        "partial",
        "goal_only"
      ],
      "default": "partial",
      "description": "How much of the procedure is fixed. 'complete' is a chef's exact method, fully ordered, nothing left to a planner. 'partial' fixes the culinarily essential steps and leaves prep and logistics to be planned. 'goal_only' declares required states and a goal with minimal method. A consumer must know which it holds before deciding whether it may reorder anything."
    },
    "metadata": {
      "type": "object",
      "properties": {
        "author": {
          "type": "string"
        },
        "source": {
          "type": "string",
          "format": "uri"
        },
        "source_format": {
          "type": "string",
          "enum": [
            "sous",
            "schema.org",
            "cooklang",
            "recipeml",
            "mealmaster",
            "manual"
          ],
          "description": "Set by an importer. Anything other than 'sous' means fields may have been inferred."
        },
        "license": {
          "type": "string"
        },
        "cuisine": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "created": {
          "type": "string",
          "format": "date"
        },
        "modified": {
          "type": "string",
          "format": "date"
        },
        "images": {
          "type": "array",
          "items": {
            "type": "string",
            "format": "uri"
          }
        },
        "import_warnings": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "What an importer could not resolve. Never silently dropped."
        }
      },
      "additionalProperties": true
    },
    "yield": {
      "type": "object",
      "description": "Both servings and mass, because neither alone is sufficient: servings are subjective, mass does not tell a diner how many people it feeds.",
      "properties": {
        "servings": {
          "type": "number",
          "exclusiveMinimum": 0
        },
        "mass": {
          "$ref": "common/quantity.schema.json#/$defs/Exact"
        },
        "unit_label": {
          "type": "string",
          "description": "What one serving is: 'bowl', 'slice', 'taco'."
        }
      },
      "anyOf": [
        {
          "required": [
            "servings"
          ]
        },
        {
          "required": [
            "mass"
          ]
        }
      ],
      "additionalProperties": false
    },
    "scaling": {
      "type": "object",
      "description": "Recipes do not scale linearly. Time scales roughly as (factor)^0.3 for oven work, leavening and seasoning scale sublinearly, and pan geometry changes the surface-area-to-volume ratio. Outside valid_range a consumer must not silently scale.",
      "properties": {
        "valid_range": {
          "type": "object",
          "properties": {
            "min": {
              "type": "number",
              "exclusiveMinimum": 0,
              "default": 0.5
            },
            "max": {
              "type": "number",
              "exclusiveMinimum": 0,
              "default": 2
            }
          },
          "additionalProperties": false
        },
        "notes": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "requires": {
      "type": "array",
      "minItems": 1,
      "description": "Input materials in their REQUIRED STATE. This is the core of Sous: 'onion, diced' declares a state, not a procedure. A kitchen holding pre-diced onion satisfies it directly; one holding whole onion must plan a cut.",
      "items": {
        "$ref": "#/$defs/Material"
      }
    },
    "equipment": {
      "type": "array",
      "description": "Equipment requirements by capability CLASS. Instances are bound by a kitchen profile.",
      "items": {
        "$ref": "#/$defs/EquipmentRequirement"
      }
    },
    "method": {
      "type": "array",
      "description": "A PARTIALLY ORDERED task network, not a step list. Array order is presentation order only; the real order is the 'after' relation. Two tasks with no path between them may run concurrently, which is what lets a scheduler find the critical path.",
      "items": {
        "$ref": "#/$defs/Task"
      }
    },
    "goal": {
      "$ref": "#/$defs/Goal"
    },
    "safety": {
      "$ref": "#/$defs/Safety"
    },
    "notes": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "_expect": {
      "type": "object",
      "description": "Test-fixture annotation only: the semantic rule this document is expected to violate. Never present in a real recipe.",
      "properties": {
        "rule": {
          "type": "string"
        },
        "why": {
          "type": "string"
        }
      },
      "required": [
        "rule",
        "why"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "sous_version",
    "title",
    "requires",
    "method",
    "goal"
  ],
  "additionalProperties": false,
  "$defs": {
    "Material": {
      "title": "Material",
      "description": "A named lot of substance in a state. Inputs are declared here; intermediates are declared by a task's .",
      "type": "object",
      "properties": {
        "id": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId"
        },
        "substance": {
          "$ref": "common/identifier.schema.json#/$defs/SubstanceRef"
        },
        "state": {
          "$ref": "common/state.schema.json#/$defs/MaterialState",
          "description": "The state the recipe REQUIRES. Omitted facets are unconstrained."
        },
        "quantity": {
          "$ref": "common/quantity.schema.json#/$defs/Quantity"
        },
        "optional": {
          "type": "boolean",
          "default": false
        },
        "alternatives": {
          "type": "array",
          "description": "Substitutions the recipe explicitly permits: '8 oz shrimp OR 8 oz chicken breast'. Each carries its own quantity because substitution is rarely 1:1 by mass.",
          "items": {
            "type": "object",
            "properties": {
              "substance": {
                "$ref": "common/identifier.schema.json#/$defs/SubstanceRef"
              },
              "state": {
                "$ref": "common/state.schema.json#/$defs/MaterialState"
              },
              "quantity": {
                "$ref": "common/quantity.schema.json#/$defs/Quantity"
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "substance"
            ],
            "additionalProperties": false
          }
        },
        "notes": {
          "type": "string"
        }
      },
      "required": [
        "id",
        "substance"
      ],
      "additionalProperties": false
    },
    "ProducedMaterial": {
      "title": "Produced material",
      "description": "An intermediate. Naming it is what makes 'set the beef aside' and 'add the beef' five steps later unambiguous, and what lets a sauce be shared between recipes.",
      "type": "object",
      "properties": {
        "id": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId"
        },
        "label": {
          "type": "string",
          "description": "Human name: 'the batter', 'seared beef'."
        },
        "state": {
          "$ref": "common/state.schema.json#/$defs/MaterialState",
          "description": "Overrides or supplements the operation's declared effects."
        },
        "role": {
          "type": "string",
          "enum": [
            "primary",
            "byproduct",
            "solids",
            "liquid",
            "trimmings",
            "reserved"
          ],
          "default": "primary",
          "description": "Byproducts are first class: reserved pasta water, rendered fat, bones for stock, the spent aromatics of a bouquet garni."
        },
        "quantity": {
          "$ref": "common/quantity.schema.json#/$defs/Quantity",
          "description": "For a portion split, or where the yield is known."
        },
        "notes": {
          "type": "string",
          "description": "Free-text note for humans. Never parsed."
        }
      },
      "required": [
        "id"
      ],
      "additionalProperties": false
    },
    "EquipmentRequirement": {
      "title": "Equipment requirement",
      "type": "object",
      "properties": {
        "id": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId",
          "description": "Referenced by tasks. Two tasks naming the same id must use the SAME physical instance, which is how 'add the onions to the same pot' becomes explicit."
        },
        "class": {
          "$ref": "common/identifier.schema.json#/$defs/EquipmentClassRef"
        },
        "label": {
          "type": "string"
        },
        "attributes": {
          "type": "object",
          "description": "Constraints on the instance: capacity, surface_area, dimensions, lidded. Dimensions are load-bearing physics, not decoration: the same batter mass in an 8x8 and a 9x13 dish bakes differently.",
          "additionalProperties": true
        },
        "optional": {
          "type": "boolean",
          "default": false
        },
        "substitutable_with": {
          "type": "array",
          "items": {
            "$ref": "common/identifier.schema.json#/$defs/EquipmentClassRef"
          }
        },
        "notes": {
          "type": "string",
          "description": "Free-text note for humans. Never parsed."
        }
      },
      "required": [
        "id",
        "class"
      ],
      "additionalProperties": false
    },
    "Task": {
      "title": "Task",
      "description": "One node of the task network. Exactly one of 'op' (primitive, directly executable) or 'include' (abstract, resolved by another recipe) must be present.",
      "type": "object",
      "properties": {
        "id": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId"
        },
        "op": {
          "$ref": "common/identifier.schema.json#/$defs/OperationRef"
        },
        "include": {
          "type": "object",
          "description": "Compose a sub-recipe as a component. This is the mechanism Cooklang lacks -- its own example corpus carries a TODO where a sauce should be composed.",
          "properties": {
            "recipe": {
              "type": "string",
              "description": "Recipe id or URI."
            },
            "as": {
              "$ref": "common/identifier.schema.json#/$defs/LocalId",
              "description": "Local id bound to the sub-recipe's output."
            },
            "scale": {
              "type": "number",
              "exclusiveMinimum": 0,
              "default": 1
            }
          },
          "required": [
            "recipe",
            "as"
          ],
          "additionalProperties": false
        },
        "consumes": {
          "type": "array",
          "description": "Material ids consumed. Order is significant for order-sensitive operations such as 'layer' and 'plate'.",
          "items": {
            "$ref": "common/identifier.schema.json#/$defs/LocalId"
          }
        },
        "produces": {
          "type": "array",
          "description": "A LIST, because operations genuinely have multiple outputs: strain gives solids and liquid, trim gives the trimmed material and bones worth keeping.",
          "items": {
            "$ref": "#/$defs/ProducedMaterial"
          }
        },
        "in": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId",
          "description": "Equipment id the operation happens in or on. Naming the same vessel across tasks is how vessel state (fond, residual heat, contamination) carries forward."
        },
        "using": {
          "type": "array",
          "items": {
            "$ref": "common/identifier.schema.json#/$defs/LocalId"
          },
          "description": "Additional equipment: tools, probes, wrapping."
        },
        "params": {
          "type": "object",
          "description": "Operation-specific parameters, validated against the operation's schema in vocab/operations.json.",
          "additionalProperties": true
        },
        "heat": {
          "type": "object",
          "description": "Recipes say 'medium-high heat'; devices need setpoints. Both are carried, and a kitchen profile may remap the level.",
          "properties": {
            "level": {
              "type": "string",
              "enum": [
                "low",
                "medium_low",
                "medium",
                "medium_high",
                "high",
                "maximum"
              ]
            },
            "setpoint": {
              "$ref": "common/quantity.schema.json#/$defs/Exact"
            }
          },
          "anyOf": [
            {
              "required": [
                "level"
              ]
            },
            {
              "required": [
                "setpoint"
              ]
            }
          ],
          "additionalProperties": false
        },
        "until": {
          "$ref": "common/condition.schema.json#/$defs/Until"
        },
        "duration": {
          "$ref": "common/condition.schema.json#/$defs/Duration",
          "description": "Shorthand for an until with a single elapsed condition."
        },
        "after": {
          "type": "array",
          "description": "Task ids that must complete first. This relation defines order, not array position. Omit it where order genuinely does not matter, so a scheduler can parallelise.",
          "items": {
            "$ref": "common/identifier.schema.json#/$defs/LocalId"
          }
        },
        "timing": {
          "$ref": "common/condition.schema.json#/$defs/TimingConstraint"
        },
        "batching": {
          "type": "object",
          "description": "'Cook a small amount at a time' is a capacity constraint, not a stylistic note: crowding a pan drops its temperature and the food steams instead of browning. Declaring it lets a planner compute the batch count from the vessel it actually has.",
          "properties": {
            "mode": {
              "type": "string",
              "enum": [
                "single",
                "in_batches"
              ],
              "default": "single"
            },
            "limited_by": {
              "type": "string",
              "enum": [
                "vessel_surface_area",
                "vessel_capacity",
                "oil_volume",
                "oven_shelf_area",
                "operator"
              ]
            },
            "max_per_batch": {
              "$ref": "common/quantity.schema.json#/$defs/Exact"
            },
            "reason": {
              "type": "string"
            }
          },
          "additionalProperties": false
        },
        "concurrent_actions": {
          "type": "array",
          "description": "Periodic actions during a long operation: 'stirring occasionally', 'basting every 20 minutes', 'skim as required'.",
          "items": {
            "type": "object",
            "properties": {
              "op": {
                "$ref": "common/identifier.schema.json#/$defs/OperationRef"
              },
              "frequency": {
                "type": "string",
                "enum": [
                  "constant",
                  "frequent",
                  "occasional",
                  "as_required"
                ]
              },
              "interval": {
                "$ref": "common/condition.schema.json#/$defs/Duration"
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "op"
            ],
            "additionalProperties": false
          }
        },
        "preconditions": {
          "type": "object",
          "description": "Extra requirements beyond the operation's own. Needed for load-bearing negatives: 'place in a COLD oven' asserts equipment thermal=ambient, which is the opposite of the usual preheat.",
          "properties": {
            "material": {
              "type": "object",
              "additionalProperties": {
                "$ref": "common/state.schema.json#/$defs/MaterialState"
              }
            },
            "equipment": {
              "type": "object",
              "additionalProperties": {
                "$ref": "common/state.schema.json#/$defs/EquipmentState"
              }
            }
          },
          "additionalProperties": false
        },
        "optional": {
          "type": "boolean",
          "default": false
        },
        "text": {
          "type": "string",
          "description": "Human-readable rendering. Non-normative: an executor must never parse it. Every noun phrase in it should resolve to a consumes/produces id -- 'sous lint' checks this."
        },
        "notes": {
          "type": "string"
        }
      },
      "required": [
        "id"
      ],
      "oneOf": [
        {
          "required": [
            "op"
          ]
        },
        {
          "required": [
            "include"
          ]
        }
      ],
      "additionalProperties": false
    },
    "Goal": {
      "title": "Goal",
      "description": "The finished dish. Plating is not a special case: it is transfer plus spatial arrangement into a serving vessel.",
      "type": "object",
      "properties": {
        "serve": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "properties": {
              "material": {
                "$ref": "common/identifier.schema.json#/$defs/LocalId"
              },
              "vessel": {
                "$ref": "common/identifier.schema.json#/$defs/EquipmentClassRef",
                "default": "serving_vessel"
              },
              "portions": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "arrangement": {
                "type": "array",
                "description": "Ordered spatial relations. 'Place on a plate, drizzle the sauce over, garnish on top' is three entries.",
                "items": {
                  "type": "object",
                  "properties": {
                    "relation": {
                      "type": "string",
                      "enum": [
                        "on",
                        "over",
                        "under",
                        "beside",
                        "around",
                        "centred",
                        "scattered",
                        "layered"
                      ]
                    },
                    "material": {
                      "$ref": "common/identifier.schema.json#/$defs/LocalId"
                    },
                    "relative_to": {
                      "$ref": "common/identifier.schema.json#/$defs/LocalId"
                    },
                    "distribution": {
                      "type": "string",
                      "enum": [
                        "even",
                        "clustered",
                        "scattered"
                      ]
                    }
                  },
                  "required": [
                    "relation",
                    "material"
                  ],
                  "additionalProperties": false
                }
              },
              "on_the_side": {
                "type": "array",
                "items": {
                  "$ref": "common/identifier.schema.json#/$defs/LocalId"
                },
                "description": "Reserved portions served separately: 'extra sauce on the side'."
              },
              "temperature": {
                "type": "string",
                "enum": [
                  "frozen",
                  "chilled",
                  "cool",
                  "ambient",
                  "warm",
                  "hot"
                ]
              },
              "immediately": {
                "type": "boolean",
                "default": false,
                "description": "Equivalent to timing.max_hold of a few minutes. Set it so a scheduler treats the finish as a hard synchronisation point."
              }
            },
            "required": [
              "material"
            ],
            "additionalProperties": false
          }
        },
        "accompaniments": {
          "type": "array",
          "description": "'Serve with warmed ciabatta.' Prepared outside the main graph and not required for the dish to be complete.",
          "items": {
            "type": "object",
            "properties": {
              "substance": {
                "$ref": "common/identifier.schema.json#/$defs/SubstanceRef"
              },
              "recipe": {
                "type": "string"
              },
              "state": {
                "$ref": "common/state.schema.json#/$defs/MaterialState"
              },
              "optional": {
                "type": "boolean",
                "default": true
              }
            },
            "additionalProperties": false
          }
        },
        "storage": {
          "type": "object",
          "description": "Make-ahead and leftovers. A scheduler needs this to plan components days apart.",
          "properties": {
            "make_ahead": {
              "type": "boolean"
            },
            "keeps_for": {
              "$ref": "common/condition.schema.json#/$defs/Duration"
            },
            "at": {
              "type": "string",
              "enum": [
                "frozen",
                "chilled",
                "cool",
                "ambient"
              ]
            },
            "reheat": {
              "type": "string"
            }
          },
          "additionalProperties": false
        }
      },
      "required": [
        "serve"
      ],
      "additionalProperties": false
    },
    "Safety": {
      "title": "Safety",
      "description": "Normative, not an add-on. A format that orchestrates real heat and real allergens must carry this. Allergens propagate through the operation graph, so a validator derives the effective set rather than trusting a hand-written list.",
      "type": "object",
      "properties": {
        "allergens": {
          "type": "object",
          "properties": {
            "declared": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/Allergen"
              },
              "description": "Author-declared. A validator recomputes this from the graph and reports disagreement."
            },
            "may_contain": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/Allergen"
              },
              "description": "Cross-contact risk from shared equipment."
            },
            "free_from": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/Allergen"
              },
              "description": "Positive claim. Forces the validator to prove absence through every path, including shared equipment."
            }
          },
          "additionalProperties": false
        },
        "control_points": {
          "type": "array",
          "description": "HACCP-style. A 'critical' point must be met or the dish is unsafe; a 'control' point is quality. Modelled on the Open Recipe Format's control_point / critical_control_point fields.",
          "items": {
            "type": "object",
            "properties": {
              "task": {
                "$ref": "common/identifier.schema.json#/$defs/LocalId"
              },
              "type": {
                "type": "string",
                "enum": [
                  "control",
                  "critical"
                ]
              },
              "condition": {
                "$ref": "common/condition.schema.json#/$defs/Condition"
              },
              "rationale": {
                "type": "string"
              },
              "on_failure": {
                "type": "string",
                "enum": [
                  "discard",
                  "continue_cooking",
                  "escalate"
                ]
              }
            },
            "required": [
              "task",
              "type",
              "condition"
            ],
            "additionalProperties": false
          }
        },
        "cross_contamination": {
          "type": "array",
          "description": "Explicit separation requirements: a board used for raw poultry must be washed before it touches anything served raw.",
          "items": {
            "type": "object",
            "properties": {
              "hazard": {
                "type": "string",
                "enum": [
                  "raw_poultry",
                  "raw_meat",
                  "raw_fish",
                  "raw_egg",
                  "allergen"
                ]
              },
              "allergen": {
                "$ref": "#/$defs/Allergen"
              },
              "requires_wash_before": {
                "type": "array",
                "items": {
                  "$ref": "common/identifier.schema.json#/$defs/LocalId"
                }
              }
            },
            "required": [
              "hazard"
            ],
            "additionalProperties": false
          }
        },
        "hazards": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Physical hazards present, drawn from vocab/equipment-classes.json."
        }
      },
      "additionalProperties": false
    },
    "Allergen": {
      "type": "string",
      "enum": [
        "gluten",
        "crustacean",
        "egg",
        "fish",
        "peanut",
        "soy",
        "milk",
        "tree_nut",
        "celery",
        "mustard",
        "sesame",
        "sulphite",
        "lupin",
        "mollusc"
      ],
      "description": "Union of the EU FIC Annex II 14 and the US FALCPA/FASTER 9."
    }
  }
}
