{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://runbooks.directory/spec/v1/schema/run-report.json",
  "title": "Run report v1",
  "description": "What a supervised run leaves behind (RUNBOOK.md 14). An optional extension of the specification: the catalog depends on receiving none. Every field here is either justified as actionable or excluded as a deanonymization risk, and the exclusions are the point - the pressure to add 'just an error message field' is constant, and each such field is a secret leak waiting to happen. Nothing in this schema accepts free text: that is enforced by enum, pattern or format on every string, not by a promise to redact before publishing.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "report_schema",
    "schema_version",
    "runbook",
    "version",
    "content_hash",
    "profile",
    "outcome",
    "executor_kind",
    "runtime_profile",
    "deviation_count",
    "duration_bucket",
    "env_class",
    "reported_at"
  ],
  "properties": {
    "report_schema": {
      "const": "runbook-run-report/v1"
    },
    "schema_version": {
      "enum": ["v1"],
      "description": "The runbook format the report is about. Without it, reports are incomparable across format versions."
    },
    "runbook": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]{0,31}/[a-z0-9][a-z0-9-]{0,63}$",
      "description": "publisher/slug. A name, not a description: the pattern is what keeps it from becoming a place to write a sentence."
    },
    "version": {
      "type": "string",
      "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
    },
    "content_hash": {
      "type": "string",
      "pattern": "^sha256:[0-9a-f]{64}$",
      "description": "What actually ran, not just the version id. A report tied only to a version lives until the first update drift. A hash the catalog has never seen is still valid here: a reporter cannot be required to have run an indexed version, and refusing those reports would silently exclude exactly the runs worth hearing about."
    },
    "profile": {
      "enum": ["P0", "P1"]
    },
    "outcome": {
      "enum": ["success", "failed", "aborted", "partial"]
    },
    "failed_step_id": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9_]{0,31}$",
      "description": "Where exactly it broke. '5 successes, 3 failures' produces no action; '3 failures at s4' does."
    },
    "failure_class": {
      "enum": [
        "precondition_unmet",
        "tool_missing",
        "permission_denied",
        "expectation_mismatch",
        "timeout",
        "human_abort",
        "upstream_changed"
      ],
      "description": "Closed on purpose. An open class would be a free-text field with extra steps."
    },
    "executor_kind": {
      "enum": ["human", "agent-assisted", "autonomous"],
      "description": "Without it you cannot say whether the procedure is fit for an agent, which is half the point of the catalog."
    },
    "runtime_profile": {
      "enum": ["R0", "R1", "R2"],
      "description": "A failure under an advisory client and a failure under a supervised one are different facts; aggregating them together destroys both."
    },
    "deviation_count": {
      "type": "integer",
      "minimum": 0,
      "description": "How many times the supervisor blocked something the runbook did not authorize. A procedure that constantly provokes deviations is one whose graph does not match reality, and no other field surfaces that."
    },
    "duration_bucket": {
      "enum": ["<1m", "1-5m", "5-30m", ">30m"],
      "description": "Bucketed, never exact. Second-level precision combined with submission time and a rare runbook deanonymizes a small organization."
    },
    "env_class": {
      "enum": ["local", "ci", "staging", "prod"],
      "description": "A closed enum and nothing finer. No cloud, no region, no cluster version: that is already an infrastructure fingerprint."
    },
    "reported_at": {
      "type": "string",
      "format": "date",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
      "description": "A date, not a timestamp, enforced by the pattern rather than by convention. A timestamp is a movement log of one organization's incidents."
    }
  },
  "allOf": [
    {
      "description": "A success has nothing to attribute a failure to. Carrying a failed step anyway would put a step id into the aggregate for runs that did not fail there.",
      "if": {
        "properties": { "outcome": { "const": "success" } },
        "required": ["outcome"]
      },
      "then": {
        "properties": {
          "failed_step_id": { "not": {} },
          "failure_class": { "not": {} }
        }
      }
    },
    {
      "description": "A failure that names no class is a report nobody can act on: the aggregate would say only that something went wrong somewhere.",
      "if": {
        "properties": { "outcome": { "const": "failed" } },
        "required": ["outcome"]
      },
      "then": {
        "required": ["failure_class"]
      }
    }
  ]
}
