{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://rulewright.dev/schema/rule-schema.json",
  "title": "RuleWright rule document",
  "description": "A RuleWright rule document: either a single rule or a rule set. Logic lives in 'condition' and 'actions'; the 'layout' key is presentation metadata for canvas tools and is ignored entirely by the engine.",
  "oneOf": [
    {
      "$ref": "#/$defs/rule"
    },
    {
      "$ref": "#/$defs/ruleSet"
    },
    {
      "$ref": "#/$defs/decisionTableDocument"
    }
  ],
  "$defs": {
    "decisionTableDocument": {
      "type": "object",
      "properties": {
        "decisionTable": {
          "$ref": "#/$defs/decisionTable"
        }
      },
      "required": [
        "decisionTable"
      ],
      "additionalProperties": false
    },
    "decisionTable": {
      "type": "object",
      "description": "A tabular authoring form that expands into ordinary rules \u2014 one rule per row. Each input column contributes a condition (a null cell is a wildcard); a row with all-wildcard inputs is a catch-all. Each output column contributes an action.",
      "properties": {
        "id": {
          "type": "string",
          "description": "Base for generated row rule ids ('<id>-<rowIndex>'); defaults to 'row'."
        },
        "name": {
          "type": "string",
          "description": "Optional display name for the expanded rule set."
        },
        "description": {
          "type": "string",
          "description": "Free-text documentation; ignored by the engine."
        },
        "hitPolicy": {
          "enum": [
            "collect",
            "first"
          ],
          "default": "collect",
          "description": "'collect' (default): every matching row applies its actions in row order. 'first': only the first matching row (in row order) applies."
        },
        "params": {
          "$ref": "#/$defs/params"
        },
        "inputs": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/decisionInput"
          },
          "description": "Input columns, left to right; each maps a 'when' cell to a condition."
        },
        "outputs": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/decisionOutput"
          },
          "description": "Output columns, left to right; each maps a 'then' cell to an action."
        },
        "rows": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/decisionRow"
          }
        }
      },
      "required": [
        "inputs",
        "outputs",
        "rows"
      ],
      "additionalProperties": false
    },
    "decisionInput": {
      "type": "object",
      "properties": {
        "field": {
          "type": "string",
          "minLength": 1,
          "description": "Dotted fact path this column compares against."
        },
        "operator": {
          "enum": [
            "Equals",
            "NotEquals",
            "GreaterThan",
            "GreaterThanOrEqual",
            "LessThan",
            "LessThanOrEqual",
            "Contains",
            "StartsWith",
            "EndsWith",
            "MatchesRegex",
            "In",
            "NotIn"
          ],
          "default": "Equals",
          "description": "Comparison applied to non-wildcard cells in this column."
        }
      },
      "required": [
        "field"
      ],
      "additionalProperties": false
    },
    "decisionOutput": {
      "type": "object",
      "properties": {
        "target": {
          "type": "string",
          "minLength": 1,
          "description": "Output key this column writes."
        },
        "type": {
          "enum": [
            "setOutput",
            "addToOutput",
            "appendToOutput"
          ],
          "default": "setOutput",
          "description": "How a non-null 'then' cell combines into the outputs."
        }
      },
      "required": [
        "target"
      ],
      "additionalProperties": false
    },
    "decisionRow": {
      "type": "object",
      "properties": {
        "when": {
          "type": "array",
          "description": "One cell per input column. A null cell is a wildcard (no condition). For In/NotIn columns a cell is an array; otherwise a scalar."
        },
        "then": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/expression"
          },
          "description": "One cell per output column. A null cell skips that output for this row; any other value is an expression."
        }
      },
      "required": [
        "when",
        "then"
      ],
      "additionalProperties": false
    },
    "ruleSet": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Optional display name for the rule set."
        },
        "description": {
          "type": "string",
          "description": "Free-text documentation; ignored by the engine."
        },
        "stopAfterFirstMatch": {
          "type": "boolean",
          "default": false,
          "description": "When true, evaluation stops at the first rule whose condition passes; the default applies every matching rule. It is ORed with the caller's 'stopOnFirstMatch' evaluation option, and a 'first' hit-policy decision table expands into a rule set with it set."
        },
        "params": {
          "$ref": "#/$defs/params"
        },
        "rules": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/rule"
          },
          "minItems": 1,
          "description": "The rules in this set. Evaluation order is by descending 'priority', then document order."
        }
      },
      "required": [
        "rules"
      ],
      "additionalProperties": false
    },
    "rule": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "description": "Unique identifier for the rule within its set."
        },
        "description": {
          "type": "string",
          "description": "Human-readable summary of what the rule does."
        },
        "priority": {
          "type": "integer",
          "default": 0,
          "description": "Higher-priority rules are evaluated first; ties keep document order."
        },
        "enabled": {
          "type": "boolean",
          "default": true,
          "description": "Disabled rules are skipped entirely during evaluation."
        },
        "condition": {
          "$ref": "#/$defs/condition"
        },
        "actions": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/action"
          },
          "description": "Actions applied when the condition evaluates to true."
        },
        "else": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/action"
          },
          "description": "Actions applied when the condition evaluates to false \u2014 the rule's 'else' branch. Same shape as 'actions'."
        },
        "failureMessage": {
          "type": "string",
          "minLength": 1,
          "description": "Optional authored explanation surfaced on the evaluation result (as a RuleFailure) when this rule is evaluated and its condition does not pass. Skipped rules (disabled, or unreached after a stop-on-first-match) report nothing. Like 'description', it never changes what the rule computes and is excluded from the rule content hash."
        },
        "params": {
          "$ref": "#/$defs/params"
        },
        "layout": {
          "type": "object",
          "description": "Presentation/layout metadata for visual rule builders (node positions, canvas ids). The engine never reads this key, and it is excluded from the rule content hash used for compilation caching."
        }
      },
      "required": [
        "id",
        "condition"
      ],
      "not": {
        "required": [
          "type"
        ]
      },
      "additionalProperties": false
    },
    "condition": {
      "oneOf": [
        {
          "$ref": "#/$defs/conditionGroup"
        },
        {
          "$ref": "#/$defs/conditionLeaf"
        }
      ]
    },
    "conditionGroup": {
      "type": "object",
      "properties": {
        "type": {
          "const": "group"
        },
        "operator": {
          "enum": [
            "AND",
            "OR",
            "NOT"
          ],
          "description": "Logical combinator. AND/OR take one or more child conditions and short-circuit left to right; NOT takes exactly one child."
        },
        "rules": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/condition"
          },
          "minItems": 1,
          "description": "Child conditions."
        }
      },
      "required": [
        "type",
        "operator",
        "rules"
      ],
      "if": {
        "properties": {
          "operator": {
            "const": "NOT"
          }
        }
      },
      "then": {
        "properties": {
          "rules": {
            "maxItems": 1
          }
        }
      },
      "additionalProperties": false
    },
    "conditionLeaf": {
      "type": "object",
      "properties": {
        "field": {
          "type": "string",
          "minLength": 1,
          "description": "Dotted path resolved against the fact, e.g. 'Customer.Age'. Use this or 'expression' as the left-hand side, not both."
        },
        "expression": {
          "$ref": "#/$defs/expression",
          "description": "A computed left-hand side, e.g. Order.Total * 0.9. An alternative to 'field'; not allowed with operator 'custom'."
        },
        "operator": {
          "enum": [
            "Equals",
            "NotEquals",
            "GreaterThan",
            "GreaterThanOrEqual",
            "LessThan",
            "LessThanOrEqual",
            "Contains",
            "StartsWith",
            "EndsWith",
            "MatchesRegex",
            "In",
            "NotIn",
            "IsNull",
            "IsNotNull",
            "custom",
            "Any",
            "All",
            "None"
          ]
        },
        "value": {
          "description": "Comparison operand. Type depends on the operator: scalar for comparisons, string for string operators, array for In/NotIn, forbidden for IsNull/IsNotNull."
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "Registered custom function name; required when operator is 'custom'."
        },
        "condition": {
          "$ref": "#/$defs/condition",
          "description": "The per-element condition for a collection quantifier (Any/All/None). Field paths inside it resolve against the element, and the path \"$\" means the element itself, which is how a collection of scalars is tested."
        }
      },
      "required": [
        "operator"
      ],
      "not": {
        "required": [
          "type"
        ]
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "operator": {
                "const": "custom"
              }
            },
            "required": [
              "operator"
            ]
          },
          "then": {
            "required": [
              "name"
            ],
            "not": {
              "required": [
                "expression"
              ]
            }
          },
          "else": {
            "anyOf": [
              {
                "required": [
                  "field"
                ]
              },
              {
                "required": [
                  "expression"
                ]
              }
            ]
          }
        },
        {
          "not": {
            "required": [
              "field",
              "expression"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "operator": {
                "enum": [
                  "In",
                  "NotIn"
                ]
              }
            },
            "required": [
              "operator"
            ]
          },
          "then": {
            "properties": {
              "value": {
                "type": "array",
                "minItems": 1
              }
            },
            "required": [
              "value"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "operator": {
                "enum": [
                  "Contains",
                  "StartsWith",
                  "EndsWith",
                  "MatchesRegex"
                ]
              }
            },
            "required": [
              "operator"
            ]
          },
          "then": {
            "properties": {
              "value": {
                "type": "string"
              }
            },
            "required": [
              "value"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "operator": {
                "enum": [
                  "IsNull",
                  "IsNotNull"
                ]
              }
            },
            "required": [
              "operator"
            ]
          },
          "then": {
            "not": {
              "required": [
                "value"
              ]
            }
          }
        },
        {
          "if": {
            "properties": {
              "operator": {
                "enum": [
                  "Equals",
                  "NotEquals",
                  "GreaterThan",
                  "GreaterThanOrEqual",
                  "LessThan",
                  "LessThanOrEqual"
                ]
              }
            },
            "required": [
              "operator"
            ]
          },
          "then": {
            "required": [
              "value"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "operator": {
                "enum": [
                  "Any",
                  "All",
                  "None"
                ]
              }
            },
            "required": [
              "operator"
            ]
          },
          "then": {
            "required": [
              "field",
              "condition"
            ],
            "not": {
              "anyOf": [
                {
                  "required": [
                    "value"
                  ]
                },
                {
                  "required": [
                    "expression"
                  ]
                }
              ]
            }
          }
        }
      ],
      "additionalProperties": false
    },
    "action": {
      "type": "object",
      "properties": {
        "type": {
          "enum": [
            "setOutput",
            "addToOutput",
            "appendToOutput",
            "removeOutput"
          ],
          "description": "How the action changes the result's outputs at 'target': 'setOutput' replaces, 'addToOutput' adds numerically (a running total across fired rules), 'appendToOutput' appends to a list (collected across fired rules), 'removeOutput' deletes the key. For the accumulators, a null value contributes nothing. Engines may additionally register custom action types (RuleWrightBuilder.RegisterAction); a document using one validates through that engine's Validate/LoadRuleSet, which folds the registered names into this closed set."
        },
        "target": {
          "type": "string",
          "minLength": 1,
          "description": "Output key the action writes (or, for 'removeOutput', deletes)."
        },
        "value": {
          "$ref": "#/$defs/expression",
          "description": "The value written to the output key: a constant scalar, or a value expression computed from the fact at evaluation time. Required for every type except 'removeOutput', which takes no value."
        }
      },
      "required": [
        "type",
        "target"
      ],
      "allOf": [
        {
          "if": {
            "properties": {
              "type": {
                "const": "removeOutput"
              }
            },
            "required": [
              "type"
            ]
          },
          "then": {
            "not": {
              "required": [
                "value"
              ]
            }
          },
          "else": {
            "required": [
              "value"
            ]
          }
        }
      ],
      "additionalProperties": false
    },
    "expression": {
      "description": "An output value. Pure data - a closed operator vocabulary, never embedded code. A bare scalar is a constant; object nodes are { 'literal': <scalar> }, { 'field': '<dotted path>' }, { 'op': '<operator>', 'operands': [ <expression>, ... ] }, { 'call': '<registered value function>', 'operands': [ <expression>, ... ] }, or { 'param': '<name>' } (a reference to a scoped param in scope). Operand order is significant for subtract, divide, and modulo.",
      "oneOf": [
        {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ]
        },
        {
          "$ref": "#/$defs/literalExpression"
        },
        {
          "$ref": "#/$defs/fieldExpression"
        },
        {
          "$ref": "#/$defs/operatorExpression"
        },
        {
          "$ref": "#/$defs/callExpression"
        },
        {
          "$ref": "#/$defs/paramExpression"
        }
      ]
    },
    "literalExpression": {
      "type": "object",
      "properties": {
        "literal": {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ],
          "description": "An explicit constant, equivalent to writing the bare scalar."
        }
      },
      "required": [
        "literal"
      ],
      "additionalProperties": false
    },
    "fieldExpression": {
      "type": "object",
      "properties": {
        "field": {
          "type": "string",
          "minLength": 1,
          "description": "Dotted path read from the fact, e.g. 'Order.Total'. A null anywhere along the path yields null."
        }
      },
      "required": [
        "field"
      ],
      "additionalProperties": false
    },
    "operatorExpression": {
      "type": "object",
      "properties": {
        "op": {
          "enum": [
            "add",
            "subtract",
            "multiply",
            "divide",
            "modulo",
            "negate",
            "concat",
            "coalesce",
            "count"
          ],
          "description": "Arithmetic (add/subtract/multiply/divide/modulo/negate), string concatenation (concat), or first-non-null selection (coalesce). Any null operand propagates to null (except coalesce); division or modulo by zero yields null."
        },
        "operands": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/expression"
          },
          "minItems": 1,
          "description": "Operand expressions. negate takes exactly 1; subtract/divide/modulo take exactly 2; add/multiply/concat/coalesce take 2 or more."
        }
      },
      "required": [
        "op",
        "operands"
      ],
      "additionalProperties": false,
      "allOf": [
        {
          "if": {
            "properties": {
              "op": {
                "const": "negate"
              }
            },
            "required": [
              "op"
            ]
          },
          "then": {
            "properties": {
              "operands": {
                "minItems": 1,
                "maxItems": 1
              }
            }
          }
        },
        {
          "if": {
            "properties": {
              "op": {
                "enum": [
                  "subtract",
                  "divide",
                  "modulo"
                ]
              }
            },
            "required": [
              "op"
            ]
          },
          "then": {
            "properties": {
              "operands": {
                "minItems": 2,
                "maxItems": 2
              }
            }
          }
        },
        {
          "if": {
            "properties": {
              "op": {
                "enum": [
                  "add",
                  "multiply",
                  "concat",
                  "coalesce"
                ]
              }
            },
            "required": [
              "op"
            ]
          },
          "then": {
            "properties": {
              "operands": {
                "minItems": 2
              }
            }
          }
        },
        {
          "if": {
            "properties": {
              "op": {
                "enum": [
                  "count"
                ]
              }
            },
            "required": [
              "op"
            ]
          },
          "then": {
            "properties": {
              "operands": {
                "minItems": 1,
                "maxItems": 1
              }
            }
          }
        }
      ]
    },
    "callExpression": {
      "type": "object",
      "properties": {
        "call": {
          "type": "string",
          "minLength": 1,
          "description": "The case-sensitive name of a value function registered on the engine (RuleWrightBuilder.RegisterValueFunction). The document names the function; the behaviour stays registered C# code, and an unregistered name fails at load."
        },
        "operands": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/expression"
          },
          "description": "Argument expressions, in document order. Optional: omitted means a no-argument call. A function that declares a required operand count gets calls arity-checked at load."
        }
      },
      "required": [
        "call"
      ],
      "additionalProperties": false
    },
    "paramExpression": {
      "type": "object",
      "properties": {
        "param": {
          "type": "string",
          "minLength": 1,
          "description": "A reference to a scoped param defined in the rule's 'params' or the rule set's 'params' (local shadows global). Not valid inside a quantifier's per-element condition, where field paths resolve against the element rather than the root fact."
        }
      },
      "required": [
        "param"
      ],
      "additionalProperties": false
    }
  }
}
