{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sous.pras.ai/schema/v0/run.schema.json",
  "title": "Sous Run",
  "description": "What happened when a schedule was executed. A schedule is a plan; this is the record, and the two are deliberately separate documents -- a plan that rewrote itself as it ran could not afterwards be compared against what it promised. This is the contract an execution engine drives: every event here is something a worker must be able to report, and nothing here requires the recipe or the kitchen profile to interpret. It is OUTPUT. See docs/spec/EXECUTION.md.",
  "type": "object",
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri"
    },
    "sous_version": {
      "type": "string",
      "const": "0.7.0"
    },
    "run_id": {
      "type": "string",
      "description": "Unique to this execution. Two runs of one schedule are different runs."
    },
    "schedule": {
      "type": "object",
      "description": "Which plan was executed. Enough to find it again, not a copy of it.",
      "properties": {
        "recipe": {
          "type": "string"
        },
        "kitchen": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "makespan_s": {
          "type": "number",
          "minimum": 0
        },
        "digest": {
          "type": "string",
          "description": "Hash of the schedule document. A run and a schedule that disagree about this were not the same plan, which is the sort of thing that is obvious in the kitchen and invisible in a log."
        }
      },
      "required": [
        "recipe",
        "kitchen"
      ],
      "additionalProperties": false
    },
    "executor": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "version": {
          "type": "string"
        },
        "clock": {
          "type": "string",
          "enum": [
            "simulated",
            "wall"
          ],
          "description": "`simulated` says no food was cooked. Worth recording, since a run log is otherwise indistinguishable from a real one."
        }
      },
      "required": [
        "name",
        "clock"
      ],
      "additionalProperties": false
    },
    "started_at": {
      "type": "string",
      "description": "ISO-8601, when the clock is a wall clock."
    },
    "finished_at": {
      "type": "string"
    },
    "status": {
      "type": "string",
      "enum": [
        "running",
        "completed",
        "completed_with_deviations",
        "failed",
        "aborted"
      ],
      "description": "`completed_with_deviations` is the common honest case: the dish was made, and not quite as planned."
    },
    "makespan_s": {
      "type": "number",
      "minimum": 0,
      "description": "Actual elapsed time."
    },
    "steps": {
      "type": "array",
      "description": "One entry per scheduled step, carrying planned against actual. The events are the narrative; this is the summary.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "$ref": "common/identifier.schema.json#/$defs/LocalId"
          },
          "op": {
            "type": "string"
          },
          "status": {
            "$ref": "#/$defs/StepStatus"
          },
          "planned_start_s": {
            "type": "number"
          },
          "planned_end_s": {
            "type": "number"
          },
          "actual_start_s": {
            "type": "number"
          },
          "actual_end_s": {
            "type": "number"
          },
          "ended_by": {
            "type": "string",
            "enum": [
              "until_satisfied",
              "max_duration",
              "planned_duration",
              "operator",
              "escalation"
            ],
            "description": "Why the step stopped. `planned_duration` means nothing was observed and the clock alone ended it -- for a step with an `until`, that is a weaker claim than the plan implies."
          },
          "start_delta_s": {
            "type": "number",
            "description": "Actual minus planned. Negative is early."
          },
          "duration_delta_s": {
            "type": "number"
          }
        },
        "required": [
          "id",
          "status"
        ],
        "additionalProperties": false
      }
    },
    "events": {
      "type": "array",
      "description": "Append-only, ordered by `seq`. This is the audit trail: a control point that was met and one that was never checked must not look the same afterwards.",
      "items": {
        "$ref": "#/$defs/Event"
      }
    },
    "warnings": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": [
    "sous_version",
    "run_id",
    "schedule",
    "executor",
    "status",
    "steps",
    "events"
  ],
  "additionalProperties": false,
  "$defs": {
    "StepStatus": {
      "type": "string",
      "enum": [
        "pending",
        "ready",
        "running",
        "awaiting_observation",
        "done",
        "failed",
        "skipped"
      ],
      "description": "`awaiting_observation` is a state a duration-only executor never enters, and the reason this vocabulary is not just pending/running/done: a step whose `until` is sensory can be finished cooking and not yet known to be."
    },
    "Observation": {
      "title": "Observation",
      "description": "The seam between the plan and the physical world. An `until` says 'when the seeds stop popping'; this is somebody or something reporting that they have.",
      "type": "object",
      "properties": {
        "observable": {
          "type": "string"
        },
        "facet": {
          "type": "string"
        },
        "value": {
          "description": "Whatever was measured or reported: a number, a facet value, free text."
        },
        "unit": {
          "type": "string"
        },
        "satisfied": {
          "type": "boolean",
          "description": "Whether this resolves the step's `until`. An observation may be recorded without ending anything."
        },
        "source": {
          "type": "string",
          "enum": [
            "human",
            "sensor",
            "assumed"
          ],
          "description": "`assumed` means nobody looked and nothing measured -- the executor proceeded on the plan alone. It is recorded rather than omitted so that 'we did not check' can never be mistaken for 'we checked'."
        },
        "note": {
          "type": "string"
        }
      },
      "required": [
        "observable",
        "source"
      ],
      "additionalProperties": false
    },
    "Deviation": {
      "title": "Deviation",
      "description": "Where the run departed from the plan. Recorded even when harmless, because the pattern across runs is what says whether a schedule's durations are honest.",
      "type": "object",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "late_start",
            "early_start",
            "ran_long",
            "ran_short",
            "hold_expired",
            "sync_missed",
            "skipped",
            "substituted"
          ]
        },
        "planned_s": {
          "type": "number"
        },
        "actual_s": {
          "type": "number"
        },
        "delta_s": {
          "type": "number"
        },
        "detail": {
          "type": "string"
        }
      },
      "required": [
        "kind",
        "detail"
      ],
      "additionalProperties": false
    },
    "Escalation": {
      "title": "Escalation",
      "description": "Something the executor may not decide alone. A timeout with `on_timeout: fail`, or a control point that was not met.",
      "type": "object",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "timeout",
            "control_point",
            "missing_observable",
            "resource",
            "operator_request"
          ]
        },
        "control_point_type": {
          "type": "string",
          "enum": [
            "control",
            "critical"
          ]
        },
        "on_failure": {
          "type": "string",
          "enum": [
            "discard",
            "continue_cooking",
            "escalate",
            "fail",
            "proceed"
          ],
          "description": "What the recipe said to do, echoed from the schedule so the log stands alone."
        },
        "action_taken": {
          "type": "string",
          "enum": [
            "discarded",
            "continued",
            "aborted",
            "awaiting_operator",
            "proceeded_anyway"
          ]
        },
        "detail": {
          "type": "string"
        }
      },
      "required": [
        "kind",
        "detail",
        "action_taken"
      ],
      "additionalProperties": false
    },
    "Event": {
      "type": "object",
      "properties": {
        "seq": {
          "type": "integer",
          "minimum": 0,
          "description": "Strictly increasing. Two events can share a timestamp; they cannot share an order."
        },
        "at_s": {
          "type": "number",
          "description": "Offset into the run."
        },
        "at": {
          "type": "string",
          "description": "ISO-8601 wall clock, when there is one."
        },
        "type": {
          "type": "string",
          "enum": [
            "run_started",
            "step_started",
            "observation",
            "step_finished",
            "deviation",
            "escalation",
            "run_finished"
          ]
        },
        "step": {
          "$ref": "common/identifier.schema.json#/$defs/LocalId"
        },
        "observation": {
          "$ref": "#/$defs/Observation"
        },
        "deviation": {
          "$ref": "#/$defs/Deviation"
        },
        "escalation": {
          "$ref": "#/$defs/Escalation"
        },
        "detail": {
          "type": "string"
        }
      },
      "required": [
        "seq",
        "at_s",
        "type"
      ],
      "additionalProperties": false,
      "allOf": [
        {
          "$comment": "Every event about a step must name one, and every payload must match its type -- an `escalation` event carrying only a `deviation` is a log that cannot be read.",
          "if": {
            "properties": {
              "type": {
                "enum": [
                  "step_started",
                  "step_finished",
                  "observation"
                ]
              }
            },
            "required": [
              "type"
            ]
          },
          "then": {
            "required": [
              "step"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "type": {
                "const": "observation"
              }
            },
            "required": [
              "type"
            ]
          },
          "then": {
            "required": [
              "observation"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "type": {
                "const": "deviation"
              }
            },
            "required": [
              "type"
            ]
          },
          "then": {
            "required": [
              "deviation"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "type": {
                "const": "escalation"
              }
            },
            "required": [
              "type"
            ]
          },
          "then": {
            "required": [
              "escalation"
            ]
          }
        }
      ]
    }
  }
}
