{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://redalpha.example/schemas/playbook/v1.json",
  "title": "Tradecraft Playbook",
  "description": "A declarative unit of customer tradecraft. The platform interprets this at runtime and never compiles it in. Everything expressible here is customer judgment; everything else is a platform feature.",
  "type": "object",
  "required": ["schema_version", "id", "name", "tenant", "trigger", "detection", "severity", "responses", "evidence"],
  "additionalProperties": false,

  "properties": {
    "schema_version": {
      "description": "Version of this schema the playbook targets. The platform refuses playbooks it cannot validate.",
      "const": "1.0"
    },
    "id": {
      "description": "Stable identifier, unique within the tenant.",
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "name": { "type": "string", "minLength": 1 },
    "description": { "type": "string" },
    "tenant": {
      "description": "Owning tenant. The platform uses this only for isolation and routing; it carries no behaviour.",
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "version": {
      "description": "Playbook revision, incremented by the tradecraft owner on each edit.",
      "type": "integer",
      "minimum": 1
    },
    "time_reference": {
      "description": "Which clock this playbook's time-of-day rules are written against. An off-hours rule means nothing until this is fixed: 02:14 is the middle of the night somewhere and mid-afternoon somewhere else, so the same rule produces a different verdict under each reading. Declared here so the choice is the customer's and is recorded, rather than being an accident of where a server happens to run. Facts carry an explicit timezone; the interpreter resolves local-time fields against this reference.",
      "type": "string",
      "enum": ["site_local", "actor_local", "utc"],
      "default": "site_local"
    },

    "trigger": {
      "description": "What starts this playbook.",
      "type": "object",
      "required": ["event_type"],
      "additionalProperties": false,
      "properties": {
        "event_type": {
          "description": "The class of environment event this playbook responds to.",
          "type": "string"
        },
        "conditions": {
          "description": "Optional pre-filter. If present, all conditions must hold for the playbook to run.",
          "$ref": "#/$defs/condition_list"
        }
      }
    },

    "enrichment": {
      "description": "Read-only facts to gather before judging. Each entry names a tool capability; the platform resolves it to a permissioned tool and refuses anything not granted to this tenant.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["capability"],
        "additionalProperties": false,
        "properties": {
          "capability": { "type": "string" },
          "as": {
            "description": "Name to bind the result to for use in detection rules.",
            "type": "string"
          },
          "required": {
            "description": "If true, the playbook halts and reports when this enrichment is unavailable, rather than judging on partial facts.",
            "type": "boolean",
            "default": false
          }
        }
      }
    },

    "context": {
      "description": "Named sets the customer considers significant: sensitive groups, OT-adjacent accounts, crown-jewel assets. Referenced by detection and severity rules.",
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "description": { "type": "string" },
          "members": { "type": "array", "items": { "type": "string" } },
          "selector": {
            "description": "A declarative selector resolved against the asset inventory, e.g. accounts with a reachable path to an OT-classified asset.",
            "$ref": "#/$defs/condition_list"
          }
        }
      }
    },

    "detection": {
      "description": "The rules that decide whether the event matters. This is the heart of the tradecraft.",
      "type": "object",
      "required": ["rules"],
      "additionalProperties": false,
      "properties": {
        "default_classification": {
          "description": "The verdict when no rule matches. Defaults to `benign`, recorded explicitly rather than left empty — 'nothing matched' and 'we decided it was fine' look identical in an audit log otherwise, and only one of them is a judgment. Set it to `inconclusive` if silence from your rules should mean 'not enough information' rather than 'no concern'.",
          "type": "string",
          "enum": ["benign", "inconclusive", "suspicious", "likely_compromise"],
          "default": "benign"
        },
        "rules": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "required": ["id", "when"],
            "additionalProperties": false,
            "properties": {
              "id": { "type": "string" },
              "description": { "type": "string" },
              "when": { "$ref": "#/$defs/condition_list" },
              "classification": {
                "description": "Verdict this rule asserts when it matches.",
                "type": "string",
                "enum": ["benign", "inconclusive", "suspicious", "likely_compromise"]
              }
            }
          }
        }
      }
    },

    "severity": {
      "description": "How the customer scores what was found.",
      "type": "object",
      "required": ["default"],
      "additionalProperties": false,
      "properties": {
        "default": { "$ref": "#/$defs/severity_level" },
        "modifiers": {
          "description": "Conditional adjustments, evaluated in order. The highest resulting severity wins.",
          "type": "array",
          "items": {
            "type": "object",
            "required": ["when", "set"],
            "additionalProperties": false,
            "properties": {
              "description": { "type": "string" },
              "when": { "$ref": "#/$defs/condition_list" },
              "set": { "$ref": "#/$defs/severity_level" }
            }
          }
        }
      }
    },

    "responses": {
      "description": "What the customer permits to be proposed, and the highest autonomy tier each may run at. A response absent from this list can never be proposed. The platform caps every OT-classified action at T1 regardless of what appears here.",
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["capability", "max_tier"],
        "additionalProperties": false,
        "properties": {
          "capability": {
            "description": "The action capability, resolved by the platform to a typed, permissioned tool.",
            "type": "string"
          },
          "description": { "type": "string" },
          "max_tier": { "$ref": "#/$defs/autonomy_tier" },
          "when": {
            "description": "Optional guard. The response is only offered when these conditions hold.",
            "$ref": "#/$defs/condition_list"
          }
        }
      }
    },

    "evidence": {
      "description": "What a recommendation must carry before a human is asked to approve it. The platform refuses to surface a recommendation missing any required item.",
      "type": "object",
      "required": ["require"],
      "additionalProperties": false,
      "properties": {
        "require": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string" }
        }
      }
    }
  },

  "$defs": {
    "severity_level": {
      "type": "string",
      "enum": ["info", "low", "medium", "high", "critical"]
    },
    "autonomy_tier": {
      "description": "T0 observe, T1 recommend, T2 approve-to-act, T3 autonomous within guardrails.",
      "type": "string",
      "enum": ["T0", "T1", "T2", "T3"]
    },
    "condition_list": {
      "description": "A list of conditions combined with AND. Nest any_of / all_of for other logic.",
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/condition" }
    },
    "condition": {
      "type": "object",
      "minProperties": 1,
      "maxProperties": 1,
      "additionalProperties": false,
      "properties": {
        "fact": {
          "description": "Compare an enriched fact against a value. A fact that could not be gathered does not compare false — it abstains, and the enclosing rule cannot match. See `against` for the operators that need a baseline.",
          "type": "object",
          "required": ["name", "op"],
          "additionalProperties": false,
          "properties": {
            "name": { "type": "string" },
            "op": {
              "type": "string",
              "enum": ["eq", "ne", "gt", "gte", "lt", "lte", "in", "not_in", "is_new", "matches", "within"]
            },
            "value": {},
            "against": {
              "description": "The baseline collection `is_new` compares against, as a dotted fact path — `baseline.countries`, not `baseline`. 'New' is meaningless without saying new compared to what, and naming the exact collection avoids the interpreter guessing which part of a record constitutes 'seen before'. Required for `is_new`; ignored by every other operator.",
              "type": "string"
            }
          },
          "allOf": [
            {
              "if": { "properties": { "op": { "const": "is_new" } }, "required": ["op"] },
              "then": { "required": ["against"] }
            }
          ]
        },
        "in_context": {
          "description": "True when the subject belongs to a named set from `context`.",
          "type": "object",
          "required": ["set"],
          "additionalProperties": false,
          "properties": {
            "set": { "type": "string" },
            "subject": { "type": "string", "default": "actor" }
          }
        },
        "all_of": { "$ref": "#/$defs/condition_list" },
        "any_of": { "$ref": "#/$defs/condition_list" },
        "not": { "$ref": "#/$defs/condition" }
      }
    }
  }
}
