{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://psigen.github.io/sous/schema/v0/domain.schema.json",
  "title": "Sous Domain Vocabulary",
  "description": "Meta-schema for the files in vocab/. Together they form the Sous domain: the state facets, the operations defined as typed state transformers over those facets, the equipment capability classes, and the starter substance set. This is the artifact that compiles to a PDDL/HDDL domain -- an operation without preconditions and effects gives a planner nothing to work with.",
  "type": "object",
  "required": ["vocabulary", "version"],
  "properties": {
    "$schema": { "type": "string" },
    "vocabulary": { "type": "string", "enum": ["state-facets", "operations", "equipment-classes", "core-ingredients"] },
    "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
    "description": { "type": "string" }
  },

  "allOf": [
    { "if": { "properties": { "vocabulary": { "const": "state-facets" } }, "required": ["vocabulary"] },
      "then": { "$ref": "#/$defs/StateFacetsDoc" } },
    { "if": { "properties": { "vocabulary": { "const": "operations" } }, "required": ["vocabulary"] },
      "then": { "$ref": "#/$defs/OperationsDoc" } },
    { "if": { "properties": { "vocabulary": { "const": "equipment-classes" } }, "required": ["vocabulary"] },
      "then": { "$ref": "#/$defs/EquipmentDoc" } },
    { "if": { "properties": { "vocabulary": { "const": "core-ingredients" } }, "required": ["vocabulary"] },
      "then": { "$ref": "#/$defs/IngredientsDoc" } }
  ],

  "$defs": {
    "Facet": {
      "type": "object",
      "properties": {
        "label": { "type": "string" },
        "definition": { "type": "string" },
        "cardinality": { "type": "string", "enum": ["single", "multi"], "description": "single becomes mutually exclusive PDDL predicates; multi becomes independent booleans." },
        "ordered": { "type": "boolean" },
        "monotone": { "type": "boolean", "description": "Irreversible. No operation may lower the rank; the validator enforces this." },
        "monotone_note": { "type": "string" },
        "parametric": { "type": "boolean" },
        "derived": { "type": "boolean" },
        "same_as": { "type": "string" },
        "parameter": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string" } }, "required": ["name", "type"] },
        "default": { "type": "string" },
        "values": {
          "type": "object",
          "additionalProperties": {
            "type": "object",
            "properties": {
              "rank": { "type": "integer", "minimum": 0 },
              "definition": { "type": "string" },
              "typical_dimension_mm": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
              "approx_range_c": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
              "parametric": { "type": "boolean" },
              "parameter": { "type": "object" }
            },
            "additionalProperties": false
          }
        }
      },
      "required": ["cardinality"],
      "allOf": [
        { "if": { "properties": { "ordered": { "const": true } }, "required": ["ordered"] },
          "then": { "properties": { "values": { "additionalProperties": { "required": ["rank"] } } },
                    "$comment": "An ordered facet must rank every value, or 'at least as done as' is undefined." } },
        { "if": { "properties": { "cardinality": { "const": "single" }, "same_as": false } },
          "then": { "required": ["values"] } }
      ]
    },

    "StateFacetsDoc": {
      "type": "object",
      "required": ["material_facets", "equipment_facets"],
      "properties": {
        "material_facets": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/Facet" } },
        "equipment_facets": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/Facet" } },
        "notes": { "type": "array", "items": { "type": "string" } }
      }
    },

    "Operation": {
      "type": "object",
      "required": ["label", "core", "arity", "preconditions", "effects", "duration"],
      "description": "Preconditions and effects are REQUIRED. An operation without them cannot be translated to a PDDL action schema, which is the whole point of the domain layer.",
      "properties": {
        "label": { "type": "string" },
        "core": { "type": "string", "enum": ["cutting", "mixing", "pouring", "pick_and_place", "cook_and_cool", "preparing", "control"],
                  "description": "The six action cores from the robotic meal-preparation literature, plus 'control' for non-manipulative operations." },
        "definition": { "type": "string" },
        "arity": {
          "type": "object",
          "properties": {
            "consumes": { "$ref": "#/$defs/ArityBound" },
            "produces": { "$ref": "#/$defs/ArityBound" }
          },
          "required": ["consumes", "produces"],
          "additionalProperties": false
        },
        "params": { "type": "object" },
        "preconditions": {
          "type": "object",
          "properties": {
            "material": { "type": "object" },
            "equipment": { "type": "object" }
          },
          "additionalProperties": false
        },
        "precondition_note": { "type": "string" },
        "effects": { "type": "object" },
        "effect_note": { "type": "string" },
        "produces_byproduct": { "type": "object" },
        "duration": {
          "type": "object",
          "properties": {
            "typical_s": { "type": ["number", "null"] },
            "bound_s": { "type": ["number", "null"], "description": "Default hard bound when a recipe supplies none." },
            "scales_with": { "type": "string", "enum": ["input_mass", "input_count", "none"] },
            "scale_exponent": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
            "scale_note": { "type": "string" }
          },
          "additionalProperties": false
        },
        "yield_factor": { "type": "number", "exclusiveMinimum": 0 },
        "yield_factor_from_param": { "type": "string" },
        "yield_note": { "type": "string" },
        "batching": { "type": "object" },
        "observables": { "type": "array", "items": { "type": "string" } },
        "hazards": { "type": "array", "items": { "type": "string" } },
        "safety_note": { "type": "string" },
        "note": { "type": "string" },
        "state_preserving": { "type": "boolean" },
        "non_mutating": { "type": "boolean" },
        "order_significant": { "type": "boolean" },
        "terminal": { "type": "boolean" },
        "repeatable": { "type": "boolean" },
        "executable": { "type": "boolean" },
        "preserves": { "type": "array", "items": { "type": "string" } },
        "failure_states": { "type": "array", "items": { "type": "string" } },
        "requires_preceding": { "type": "string" },
        "over_condition": { "type": "object" }
      },
      "additionalProperties": false
    },

    "ArityBound": {
      "type": "array", "minItems": 2, "maxItems": 2,
      "items": { "type": ["integer", "null"], "minimum": 0 },
      "description": "[min, max]; null max means unbounded."
    },

    "OperationsDoc": {
      "type": "object",
      "required": ["operations", "heat_levels"],
      "properties": {
        "condition_language": { "type": "object" },
        "operations": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/Operation" } },
        "heat_levels": { "type": "object" },
        "extension": { "type": "object" }
      }
    },

    "EquipmentDoc": {
      "type": "object",
      "required": ["classes", "observables", "hazards"],
      "properties": {
        "classes": {
          "type": "object", "minProperties": 1,
          "additionalProperties": {
            "type": "object",
            "required": ["label"],
            "properties": {
              "label": { "type": "string" },
              "definition": { "type": "string" },
              "parent": { "type": "string" },
              "parents": { "type": "array", "items": { "type": "string" } },
              "attributes": { "type": "object" }
            },
            "additionalProperties": false
          }
        },
        "observables": {
          "type": "object",
          "required": ["values"],
          "properties": {
            "note": { "type": "string" },
            "values": {
              "type": "object",
              "additionalProperties": {
                "type": "object",
                "required": ["machine_checkable"],
                "properties": {
                  "instrument": { "type": ["string", "null"] },
                  "machine_checkable": { "type": "boolean" },
                  "requires_human": { "type": "boolean" },
                  "note": { "type": "string" }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "hazards": {
          "type": "object",
          "required": ["values"],
          "properties": {
            "values": {
              "type": "object",
              "additionalProperties": {
                "type": "object",
                "required": ["severity"],
                "properties": {
                  "severity": { "type": "string", "enum": ["low", "moderate", "high"] },
                  "note": { "type": "string" }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },

    "IngredientsDoc": {
      "type": "object",
      "required": ["substances", "allergen_vocabulary"],
      "properties": {
        "allergen_vocabulary": { "type": "object" },
        "property_notes": { "type": "object" },
        "mapping_summary": { "type": "object" },
        "substances": {
          "type": "object", "minProperties": 1,
          "additionalProperties": {
            "type": "object",
            "required": ["label", "category", "foodon", "mapping_status", "allergens"],
            "properties": {
              "label": { "type": "string" },
              "category": { "type": "string", "enum": ["vegetable", "fruit", "protein", "dairy_egg", "grain_starch", "legume_nut_seed", "herb_spice", "fat_oil", "condiment_liquid", "baking"] },
              "foodon": { "type": ["string", "null"], "description": "null is legal and honest. Sous never invents an ontology identifier; an unmapped term is better than a wrong CURIE." },
              "fdc": { "type": ["string", "null"] },
              "mapping_status": { "type": "string", "enum": ["resolved", "approximate", "curated", "unmapped", "pending"] },
              "mapping_query": { "type": "string" },
              "mapping_exact_only": { "type": "boolean" },
              "mapping_matched_label": { "type": "string" },
              "mapping_note": { "type": "string" },
              "allergens": { "type": "array", "items": { "type": "string" } },
              "ap_to_ep": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
              "density_g_per_ml": { "type": "number", "exclusiveMinimum": 0 },
              "count_to_mass": {
                "type": "object",
                "required": ["grams", "tolerance"],
                "properties": {
                  "grams": { "type": "object", "additionalProperties": { "type": "number", "exclusiveMinimum": 0 } },
                  "tolerance": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }
                },
                "additionalProperties": false
              }
            },
            "additionalProperties": false
          }
        }
      }
    }
  }
}
