{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sous.pras.ai/schema/v0/common/quantity.schema.json",
  "title": "Sous quantity algebra",
  "description": "Quantity is not (value, unit). Real recipes say '1-2 cloves', 'salt to taste', 'water enough to cover', '1 (13.5 oz) package', '3 large onions'. Each is a distinct kind with distinct execution semantics.",

  "$defs": {
    "Unit": {
      "title": "Unit",
      "description": "UCUM code. The enumeration is closed so that 'cups' and 'tbsp' fail validation rather than silently meaning something implementation-defined.",
      "type": "string",
      "enum": [
        "g", "kg", "mg", "[oz_av]", "[lb_av]",
        "mL", "L", "dL", "cL", "[foz_us]", "[cup_us]", "[tbs_us]", "[tsp_us]", "[pt_us]", "[qt_us]", "[gal_us]",
        "[cup_m]", "[tbs_m]", "[tsp_m]",
        "mm", "cm", "m", "[in_i]",
        "Cel", "[degF]", "K",
        "s", "min", "h", "d",
        "{each}", "{clove}", "{slice}", "{sprig}", "{bunch}", "{head}", "{stalk}", "{ear}",
        "{fillet}", "{rasher}", "{sheet}", "{stick}", "{can}", "{package}", "{envelope}", "{pinch}", "{dash}"
      ]
    },

    "Basis": {
      "title": "Measurement basis",
      "description": "Resolves the as-purchased / edible-portion ambiguity. '300 g of chopped carrot' differs by roughly 20% depending on which is meant.",
      "type": "string",
      "enum": ["as_purchased", "as_prepared", "drained", "cooked"],
      "default": "as_prepared"
    },

    "Scaling": {
      "title": "Scaling policy",
      "description": "How this quantity behaves when the recipe is scaled. Recipes do NOT scale linearly: leavening and salt scale sublinearly, and pan-bound quantities do not scale at all.",
      "oneOf": [
        { "type": "string", "enum": ["linear", "fixed", "none"] },
        {
          "type": "object",
          "properties": {
            "mode": { "const": "sublinear" },
            "exponent": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }
          },
          "required": ["mode", "exponent"],
          "additionalProperties": false
        }
      ],
      "default": "linear"
    },

    "Common": {
      "type": "object",
      "properties": {
        "basis":   { "$ref": "#/$defs/Basis" },
        "scaling": { "$ref": "#/$defs/Scaling" },
        "display": { "type": "string", "description": "Original human text. Non-normative; never parsed." },
        "implicit": { "type": "boolean", "default": false, "description": "Not stated in the source recipe and inferred by an importer, e.g. the fat in 'a greased pan'. Rejected by the executable profile." }
      }
    },

    "Exact": {
      "title": "Exact quantity",
      "type": "object",
      "properties": {
        "kind": { "const": "exact" },
        "value": { "type": "number", "exclusiveMinimum": 0 },
        "unit": { "$ref": "#/$defs/Unit" },
        "basis": { "$ref": "#/$defs/Basis" }, "scaling": { "$ref": "#/$defs/Scaling" },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind", "value", "unit"],
      "additionalProperties": false
    },

    "Range": {
      "title": "Range quantity",
      "description": "'1-2 cloves of garlic', '2-4 cups of red wine'.",
      "type": "object",
      "properties": {
        "kind": { "const": "range" },
        "min": { "type": "number", "exclusiveMinimum": 0 },
        "max": { "type": "number", "exclusiveMinimum": 0 },
        "unit": { "$ref": "#/$defs/Unit" },
        "basis": { "$ref": "#/$defs/Basis" }, "scaling": { "$ref": "#/$defs/Scaling" },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind", "min", "max", "unit"],
      "additionalProperties": false
    },

    "Approximate": {
      "title": "Approximate quantity",
      "type": "object",
      "properties": {
        "kind": { "const": "approximate" },
        "value": { "type": "number", "exclusiveMinimum": 0 },
        "unit": { "$ref": "#/$defs/Unit" },
        "tolerance": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "description": "Fractional. 0.1 means +/-10%." },
        "basis": { "$ref": "#/$defs/Basis" }, "scaling": { "$ref": "#/$defs/Scaling" },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind", "value", "unit", "tolerance"],
      "additionalProperties": false
    },

    "Count": {
      "title": "Count quantity",
      "description": "'3 large onions'. A supermarket 'medium onion' spans roughly 90-135 g against a USDA figure of 130-148 g, so a count without a mass equivalent carries real error. Mass is authoritative when both are present.",
      "type": "object",
      "properties": {
        "kind": { "const": "count" },
        "count": { "type": "number", "exclusiveMinimum": 0 },
        "unit": { "$ref": "#/$defs/Unit", "description": "A count unit such as {each} or {clove}." },
        "size_class": { "type": "string", "enum": ["small", "medium", "large", "extra_large"] },
        "mass_equivalent": {
          "type": "object",
          "description": "Total mass for the stated count. Supplied by the author or resolved from vocab/core-ingredients.json.",
          "properties": {
            "value": { "type": "number", "exclusiveMinimum": 0 },
            "unit": { "enum": ["g", "kg"] },
            "tolerance": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
            "source": { "type": "string", "enum": ["author", "vocabulary", "estimated"] }
          },
          "required": ["value", "unit"],
          "additionalProperties": false
        },
        "basis": { "$ref": "#/$defs/Basis" }, "scaling": { "$ref": "#/$defs/Scaling" },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind", "count", "unit"],
      "additionalProperties": false
    },

    "Packaging": {
      "title": "Packaging quantity",
      "description": "'1 (13.5 ounce) package', '2 envelopes yeast'.",
      "type": "object",
      "properties": {
        "kind": { "const": "packaging" },
        "packages": { "type": "number", "exclusiveMinimum": 0 },
        "package_unit": { "$ref": "#/$defs/Unit" },
        "package_size": {
          "type": "object",
          "properties": {
            "value": { "type": "number", "exclusiveMinimum": 0 },
            "unit": { "$ref": "#/$defs/Unit" }
          },
          "required": ["value", "unit"],
          "additionalProperties": false
        },
        "basis": { "$ref": "#/$defs/Basis" }, "scaling": { "$ref": "#/$defs/Scaling" },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind", "packages", "package_unit"],
      "additionalProperties": false
    },

    "ToTaste": {
      "title": "To-taste quantity",
      "description": "Resolved at execution time by an assess/adjust pair, not by the author. 'Salt', 'Pepper' with no amount.",
      "type": "object",
      "properties": {
        "kind": { "const": "to_taste" },
        "seed": { "$ref": "#/$defs/Exact", "description": "Suggested starting amount before the first assessment." },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind"],
      "additionalProperties": false
    },

    "AsNeeded": {
      "title": "As-needed quantity",
      "description": "'water, enough to cover the meat'; 'a little olive oil, enough to create the desired consistency'. The amount is defined by an observable condition, not a number.",
      "type": "object",
      "properties": {
        "kind": { "const": "as_needed" },
        "condition": {
          "type": "string",
          "enum": ["to_cover", "to_submerge", "to_coat", "to_grease", "to_consistency", "to_fill", "for_frying", "for_dusting"],
          "description": "Closed set so that an executor can act on it. Anything outside this set is not machine-resolvable."
        },
        "target": { "": "Optional material or equipment the condition is relative to.", "type": "string" },
        "max": { "$ref": "#/$defs/Exact", "description": "Upper bound, so an executor cannot pour indefinitely." },
        "display": { "type": "string" }, "implicit": { "type": "boolean" }
      },
      "required": ["kind", "condition"],
      "additionalProperties": false
    },

    "Quantity": {
      "title": "Quantity",
      "oneOf": [
        { "$ref": "#/$defs/Exact" }, { "$ref": "#/$defs/Range" },
        { "$ref": "#/$defs/Approximate" }, { "$ref": "#/$defs/Count" },
        { "$ref": "#/$defs/Packaging" }, { "$ref": "#/$defs/ToTaste" },
        { "$ref": "#/$defs/AsNeeded" }
      ]
    }
  }
}
