{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sous.pras.ai/schema/v0/common/condition.schema.json",
  "title": "Sous termination conditions",
  "description": "How an operation knows it is finished. Real recipes say 'until golden brown', 'until shrimp turn pink', 'for 30 minutes, or until done'. An orchestrator needs these to be observable AND bounded: every 'until' carries a max_duration so a condition that never becomes true cannot hang the kitchen.",

  "$defs": {
    "Duration": {
      "title": "Duration",
      "type": "object",
      "properties": {
        "min":    { "type": "number", "exclusiveMinimum": 0 },
        "target": { "type": "number", "exclusiveMinimum": 0 },
        "max":    { "type": "number", "exclusiveMinimum": 0 },
        "unit":   { "type": "string", "enum": ["s", "min", "h", "d"] },
        "display": { "type": "string" }
      },
      "required": ["unit"],
      "anyOf": [ { "required": ["target"] }, { "required": ["max"] }, { "required": ["min"] } ],
      "additionalProperties": false
    },

    "ElapsedCondition": {
      "title": "Elapsed time",
      "type": "object",
      "properties": {
        "observable": { "const": "elapsed" },
        "duration": { "$ref": "#/$defs/Duration" }
      },
      "required": ["observable", "duration"],
      "additionalProperties": false,
      "$comment": "Machine-checkable. Counts as a bound."
    },

    "TemperatureCondition": {
      "title": "Temperature",
      "description": "The only fully reliable doneness signal for hazardous foods, and the basis of every food-safety critical control point.",
      "type": "object",
      "properties": {
        "observable": { "type": "string", "enum": ["internal_temperature", "equipment_temperature", "oil_temperature"] },
        "comparison": { "type": "string", "enum": ["at_least", "at_most", "approximately"], "default": "at_least" },
        "value": { "type": "number" },
        "unit": { "type": "string", "enum": ["Cel", "[degF]", "K"] },
        "site": { "type": "string", "description": "Where the probe goes: 'thickest part', 'centre'." },
        "hold": { "$ref": "#/$defs/Duration", "description": "Time the temperature must be sustained. Required for some control points." }
      },
      "required": ["observable", "value", "unit"],
      "additionalProperties": false,
      "$comment": "Machine-checkable with a probe. Counts as a bound."
    },

    "FacetCondition": {
      "title": "Facet target",
      "description": "Cook until a state facet reaches a value. 'Until golden brown' is {facet: browning, value: golden_brown}; 'until tender' is {facet: texture, value: tender}.",
      "type": "object",
      "properties": {
        "observable": { "const": "facet" },
        "facet": { "type": "string", "enum": ["form", "thermal", "doneness", "browning", "hydration", "texture", "structure"] },
        "value": { "type": "string" },
        "comparison": { "type": "string", "enum": ["at_least", "exactly"], "default": "at_least" }
      },
      "required": ["observable", "facet", "value"],
      "additionalProperties": false,
      "$comment": "Machine-checkability depends on the facet: browning and form are camera-observable, texture is not. See vocab/equipment-classes.json observables. NOT a bound."
    },

    "ReductionCondition": {
      "title": "Reduction",
      "description": "'Reduce by half' is {to_fraction: 0.5}.",
      "type": "object",
      "properties": {
        "observable": { "const": "volume" },
        "to_fraction": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1 }
      },
      "required": ["observable", "to_fraction"],
      "additionalProperties": false
    },

    "ExpansionCondition": {
      "title": "Expansion",
      "description": "'Until doubled in size' is {factor: 2.0}.",
      "type": "object",
      "properties": {
        "observable": { "const": "volume" },
        "factor": { "type": "number", "exclusiveMinimum": 1 }
      },
      "required": ["observable", "factor"],
      "additionalProperties": false
    },

    "SensoryCondition": {
      "title": "Sensory",
      "description": "Escape hatch for conditions with no machine equivalent: 'until it smells nutty', 'season to taste'. Carries a machine_hint so an executor can approximate, and never counts as a bound.",
      "type": "object",
      "properties": {
        "observable": { "type": "string", "enum": ["taste", "aroma", "viscosity", "sound", "appearance"] },
        "description": { "type": "string", "minLength": 1 },
        "machine_hint": { "type": "string", "description": "Best available proxy, e.g. an elapsed time or a temperature that usually coincides." },
        "requires_human": { "type": "boolean", "default": true }
      },
      "required": ["observable", "description"],
      "additionalProperties": false,
      "$comment": "NOT a bound. The executable profile requires a sibling max_duration."
    },

    "Condition": {
      "oneOf": [
        { "$ref": "#/$defs/ElapsedCondition" }, { "$ref": "#/$defs/TemperatureCondition" },
        { "$ref": "#/$defs/FacetCondition" }, { "$ref": "#/$defs/ReductionCondition" },
        { "$ref": "#/$defs/ExpansionCondition" }, { "$ref": "#/$defs/SensoryCondition" }
      ]
    },

    "Until": {
      "title": "Termination",
      "description": "any_of stops at the first satisfied condition; all_of requires every condition. max_duration is a hard bound applied regardless, and is what makes 'until golden brown' safe to hand to a machine.",
      "type": "object",
      "properties": {
        "any_of": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Condition" } },
        "all_of": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Condition" } },
        "max_duration": { "$ref": "#/$defs/Duration", "description": "Hard upper bound. Required by the executable conformance profile (rule SR-005)." },
        "on_timeout": {
          "type": "string",
          "enum": ["fail", "proceed", "escalate"],
          "default": "escalate",
          "description": "fail aborts; proceed continues anyway; escalate hands control to a human."
        }
      },
      "oneOf": [ { "required": ["any_of"] }, { "required": ["all_of"] } ],
      "additionalProperties": false
    },

    "TimingConstraint": {
      "title": "Timing constraint",
      "description": "A partial order gives precedence but cannot say 'serve immediately', 'do not over-proof', or 'the pasta must finish within 60 s of the sauce'. These are the constraints that make a schedule correct rather than merely feasible.",
      "type": "object",
      "properties": {
        "max_hold": { "$ref": "#/$defs/Duration", "description": "Longest the output may wait before its successor starts. Over-proofing and 'serve immediately' are both max_hold." },
        "min_hold": { "$ref": "#/$defs/Duration", "description": "Shortest it must wait, e.g. resting meat or chilling dough." },
        "sync_with": {
          "type": "array", "items": { "$ref": "identifier.schema.json#/$defs/LocalId" },
          "description": "Task ids that must complete at approximately the same time."
        },
        "sync_tolerance": { "$ref": "#/$defs/Duration" },
        "hold_at": { "type": "string", "description": "Thermal facet value to hold at while waiting." }
      },
      "additionalProperties": false
    }
  }
}
