{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sous.pras.ai/schema/v0/schedule.schema.json",
  "title": "Sous Schedule",
  "description": "A recipe bound to one kitchen and resolved: every task given a start and an end that respect precedence, the recipe's timing constraints and the kitchen's finite resources -- and given the materials, heat, endpoint and safety points needed to carry it out. This is the ISA-88 control recipe, the Layer-2 execution artifact ADR 0003 deferred until there was a scheduler to show what one actually contains. It is SELF-CONTAINED: an executor holding this document alone, with no access to the recipe or the kitchen profile, can run every step. It is OUTPUT -- generated by tools/sous_schedule.py, not authored. See docs/spec/EXECUTION.md.",
  "type": "object",
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri"
    },
    "sous_version": {
      "type": "string",
      "const": "0.7.0"
    },
    "recipe": {
      "type": "string",
      "description": "Id of the recipe scheduled."
    },
    "kitchen": {
      "type": "string",
      "description": "Id of the kitchen profile it was scheduled against. A schedule is meaningless without it."
    },
    "feasible": {
      "type": "boolean",
      "description": "False when no schedule exists. `conflicts` then says why, which is the useful half: 'this recipe needs two hobs and you have one' is actionable in a way that a bare failure is not."
    },
    "conflicts": {
      "type": "array",
      "description": "Why no schedule exists. Present only when feasible is false.",
      "items": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "resource",
              "timing",
              "missing_equipment",
              "forbidden_operation"
            ]
          },
          "resource": {
            "type": "string"
          },
          "tasks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "detail": {
            "type": "string"
          }
        },
        "required": [
          "kind",
          "detail"
        ],
        "additionalProperties": false
      }
    },
    "origin": {
      "type": "string",
      "enum": [
        "makespan",
        "serve_at"
      ],
      "description": "Whether times are relative to a zero start, or anchored so the last task lands on a requested serving time."
    },
    "serve_at": {
      "type": "string",
      "description": "ISO-8601 time the dish is served, when origin is serve_at."
    },
    "makespan_s": {
      "type": "number",
      "minimum": 0,
      "description": "Elapsed seconds from first start to last end."
    },
    "serial_s": {
      "type": "number",
      "minimum": 0,
      "description": "Sum of every task duration. The ratio to makespan_s is how much parallelism the kitchen actually permitted."
    },
    "tasks": {
      "type": "array",
      "description": "Every scheduled task, including prep steps synthesised for this kitchen's pantry that appear nowhere in the recipe.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "op": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "recipe",
              "prep"
            ],
            "description": "`prep` marks a task the planner added because this pantry required it -- peeling an onion the recipe assumed was already peeled."
          },
          "start_s": {
            "type": "number",
            "minimum": 0
          },
          "end_s": {
            "type": "number",
            "minimum": 0
          },
          "duration_s": {
            "type": "number",
            "minimum": 0
          },
          "duration_source": {
            "type": "string",
            "enum": [
              "task",
              "until",
              "operation",
              "batched"
            ],
            "description": "Where the duration came from, since most tasks do not state one and fall back to the operation's typical figure. Worth surfacing: a schedule built mostly on defaults deserves less trust."
          },
          "attended": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Resolved fraction of a cook this task consumed."
          },
          "batches": {
            "type": "integer",
            "minimum": 1
          },
          "equipment": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "resources": {
            "type": "object",
            "description": "Pool name to units consumed, including the `cook` pool.",
            "additionalProperties": {
              "type": "number"
            }
          },
          "critical": {
            "type": "boolean",
            "description": "On the critical path: delaying it delays the whole dish."
          },
          "slack_s": {
            "type": "number",
            "minimum": 0,
            "description": "How long this task could be delayed without moving the finish, from a backward pass over precedence alone. Resource contention can consume it -- delaying a task may collide with something else wanting the same hob -- so it is an upper bound on the freedom available, not a promise."
          },
          "text": {
            "type": "string"
          },
          "attention_visits": {
            "type": "object",
            "description": "Suggested times to go back to this task: N visits of `each_s` seconds, one per `every_s` window, at the offsets in `at_s`. ADVISORY ONLY -- feasibility is decided by the fractional `attended` demand, and these times constrain nothing. They exist so a printed schedule can say when to stir.",
            "properties": {
              "count": {
                "type": "integer",
                "minimum": 1
              },
              "each_s": {
                "type": "number",
                "minimum": 0
              },
              "every_s": {
                "type": "number",
                "minimum": 0
              },
              "at_s": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "Offsets, in seconds, at which attention is suggested."
              }
            },
            "required": [
              "count",
              "each_s",
              "every_s"
            ],
            "additionalProperties": false
          },
          "subject": {
            "$ref": "#/$defs/Material",
            "description": "For a `prep` step only: the ingredient it acts on. Such steps exist nowhere in the recipe and so have no materials of their own, but an executor still has to be told what to peel."
          },
          "after": {
            "type": "array",
            "items": {
              "$ref": "common/identifier.schema.json#/$defs/LocalId"
            },
            "description": "Steps that must finish first, including edges the planner synthesised for this pantry. The start times already respect these; they are carried because a run that falls behind has to distinguish a real dependency from the plan's own ordering."
          },
          "timing": {
            "$ref": "recipe.schema.json#/$defs/Task/properties/timing",
            "description": "Hold windows and synchronisation, constraining this step's SUCCESSORS. A `max_hold` is a freshness deadline that survives into execution: tadka held past it is stale whatever the plan said."
          },
          "consumes": {
            "type": "array",
            "description": "What goes in, resolved: the substance, its state, and the declared quantity. 'add 500 g diced onion', not 'add `onion`'.",
            "items": {
              "$ref": "#/$defs/Material"
            }
          },
          "produces": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "common/identifier.schema.json#/$defs/LocalId"
                },
                "label": {
                  "type": "string"
                },
                "state": {
                  "$ref": "common/state.schema.json#/$defs/MaterialState"
                },
                "mass_g": {
                  "$ref": "#/$defs/TracedMass"
                }
              },
              "required": [
                "id"
              ],
              "additionalProperties": false
            }
          },
          "in": {
            "$ref": "common/identifier.schema.json#/$defs/LocalId",
            "description": "The recipe vessel this step acts in; resolve it in top-level `equipment`."
          },
          "params": {
            "type": "object",
            "description": "Operation arguments, carried through unchanged."
          },
          "heat": {
            "type": "object",
            "description": "Heat resolved to a number. Recipes say 'medium-high'; a hob wants a setpoint. The word is kept for a human executor, but a setpoint is always present.",
            "properties": {
              "level": {
                "$ref": "recipe.schema.json#/$defs/Task/properties/heat/properties/level"
              },
              "level_source": {
                "type": "string",
                "enum": [
                  "task",
                  "operation"
                ],
                "description": "`operation` means the recipe never said, and the operation's default was used."
              },
              "setpoint": {
                "$ref": "common/quantity.schema.json#/$defs/Exact"
              },
              "setpoint_band_c": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "minItems": 2,
                "maxItems": 2,
                "description": "The band the setpoint was taken from, when it came from the vocabulary. The midpoint is a guess; the band is the truth."
              },
              "setpoint_source": {
                "type": "string",
                "enum": [
                  "recipe",
                  "kitchen",
                  "vocabulary"
                ],
                "description": "How the number was arrived at, because they are not equally trustworthy: `kitchen` is that appliance's own calibration, `vocabulary` is the midpoint of a generic band. An executor may reasonably refuse to run unattended on the latter."
              }
            },
            "additionalProperties": false
          },
          "until": {
            "$ref": "common/condition.schema.json#/$defs/Until",
            "description": "When the step ends. Without this a worker knows only how long the step was planned to take, which is not the same thing and is why `duration_s` alone cannot execute."
          },
          "observables_required": {
            "type": "array",
            "description": "What must be sensed to resolve `until`. A kitchen that cannot measure these cannot run the step unattended -- worth knowing before it starts, not at the moment it should have stopped.",
            "items": {
              "type": "object",
              "properties": {
                "observable": {
                  "type": "string"
                },
                "facet": {
                  "type": "string"
                },
                "requires_human": {
                  "type": "boolean"
                }
              },
              "required": [
                "observable"
              ],
              "additionalProperties": false
            }
          },
          "concurrent_actions": {
            "$ref": "recipe.schema.json#/$defs/Task/properties/concurrent_actions"
          },
          "control_points": {
            "$ref": "recipe.schema.json#/$defs/Safety/properties/control_points",
            "description": "The safety points that land on this step. In a recipe they sit at the top, keyed by task; a worker holding one step would never find them."
          },
          "hazards": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "From the operation vocabulary -- what this step can do to a person."
          }
        },
        "required": [
          "id",
          "op",
          "source",
          "start_s",
          "end_s",
          "duration_s"
        ],
        "additionalProperties": false
      }
    },
    "utilisation": {
      "type": "object",
      "description": "Per-pool peak and mean usage. This is what says whether the cook or the hob was the binding constraint.",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "capacity": {
            "type": "number"
          },
          "peak": {
            "type": "number"
          },
          "mean": {
            "type": "number"
          }
        },
        "required": [
          "capacity",
          "peak"
        ],
        "additionalProperties": false
      }
    },
    "warnings": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "title": {
      "type": "string",
      "description": "Human name of the dish, so a run can be announced without the recipe."
    },
    "servings": {
      "type": "number",
      "minimum": 0
    },
    "equipment": {
      "type": "array",
      "description": "Which kitchen instance each recipe vessel was bound to. Steps name the recipe role (`in: pot`); this says which pan that actually is, once rather than per step.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "$ref": "common/identifier.schema.json#/$defs/LocalId"
          },
          "class": {
            "$ref": "common/identifier.schema.json#/$defs/EquipmentClassRef"
          },
          "label": {
            "type": "string"
          },
          "instance": {
            "type": "string",
            "description": "Id of the kitchen instance the solver chose."
          },
          "attributes": {
            "type": "object",
            "description": "Carried through from the recipe's requirement."
          }
        },
        "required": [
          "id",
          "class"
        ],
        "additionalProperties": false
      }
    },
    "safety": {
      "type": "object",
      "description": "The dish-level safety envelope. Per-step control points live on the steps; this is what must be known before the first one starts.",
      "properties": {
        "allergens": {
          "$ref": "recipe.schema.json#/$defs/Safety/properties/allergens"
        },
        "hazards": {
          "$ref": "recipe.schema.json#/$defs/Safety/properties/hazards"
        }
      },
      "additionalProperties": false
    },
    "goal": {
      "$ref": "recipe.schema.json#/$defs/Goal",
      "description": "How the dish is served. Carried so plating needs no second document."
    }
  },
  "required": [
    "sous_version",
    "recipe",
    "kitchen",
    "feasible"
  ],
  "additionalProperties": false,
  "$defs": {
    "TracedMass": {
      "type": "number",
      "minimum": 0,
      "description": "Mass in grams, traced back through the producing tasks and their yield factors. Present only where it could be traced: absent means unknown, never zero. This is a sliver of the arithmetic resolution LIMITATIONS calls stage 2, not a general quantity resolver."
    },
    "Material": {
      "title": "Resolved material",
      "type": "object",
      "properties": {
        "id": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId"
        },
        "kind": {
          "type": "string",
          "enum": [
            "ingredient",
            "intermediate",
            "unknown"
          ],
          "description": "`ingredient` comes from the pantry; `intermediate` was made by an earlier step, named in `from_task`."
        },
        "from_task": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId"
        },
        "substance": {
          "$ref": "common/identifier.schema.json#/$defs/SubstanceRef"
        },
        "label": {
          "type": "string"
        },
        "state": {
          "$ref": "common/state.schema.json#/$defs/MaterialState"
        },
        "quantity": {
          "$ref": "common/quantity.schema.json#/$defs/Quantity"
        },
        "mass_g": {
          "$ref": "#/$defs/TracedMass"
        },
        "optional": {
          "type": "boolean"
        }
      },
      "required": [
        "id",
        "kind"
      ],
      "additionalProperties": false
    }
  }
}
