{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://standards.rootblocks.com/schemas/decisions/decision-event.v1.schema.json",
  "title": "RootBlocks DecisionEvent v1",
  "description": "Normalized, source-agnostic envelope for a decision signal observed in an external system (Jira, Slack, Linear, GitHub). Adapters (D2 Jira / D3 Slack / D4 Linear, plus GitHub) map their native events INTO this shape so the control plane can JOIN a decision to the merge/PR that carries it out — the cross-system decision↔merge correlation. The join key is `workItem` (system + native id); the load-bearing field is `ratification` (was the decision proposed / ratified / rejected / superseded, and what native signal backs it). Sibling wire envelope to evidence.v1 and to the merge-side event; distinct from decision-record.v1 (the SliceOps DEC frontmatter, an in-repo governance record — this is the observed external event that may reference it via `decisionRefs`).",
  "$comment": "Wire-contract discipline mirrors evidence.v1: this is a CLOSED schema (additionalProperties:false) whose MAJOR version is the wire contract — `schemaVersion` stays const 'rootblocks.decision-event/v1' across every v1.x minor, so adapters and consumers pin the major and existing events keep validating. Minors evolve ONLY additively (new OPTIONAL properties, WIDENED enums — never a new required field or a narrowed constraint); because each new field is declared, the closed shape is preserved. The `source` and `workItem.system` enums are expected to widen (e.g. gitlab, notion, azure-devops) under exactly this rule.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schemaVersion",
    "eventId",
    "source",
    "actor",
    "workItem",
    "occurredAt",
    "ratification"
  ],
  "properties": {
    "schemaVersion": {
      "const": "rootblocks.decision-event/v1"
    },
    "eventId": {
      "description": "Stable identifier for this decision event, unique within its source. Adapters SHOULD derive it deterministically from the native event so re-ingestion is idempotent.",
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9.-]{2,127}$"
    },
    "source": {
      "description": "The system the event was observed in. Widened additively as adapters land.",
      "enum": ["jira", "slack", "linear", "github"]
    },
    "actor": {
      "$ref": "#/$defs/actor"
    },
    "workItem": {
      "description": "The work item this decision is about — the JOIN KEY to the merge-side event. A DecisionEvent and a MergeEvent correlate when they name the same (system, id) work item, so the native id MUST be the source-stable identifier (e.g. Jira PROJ-123, Linear ENG-45, a GitHub issue/PR number). `workItem.system` MAY differ from `source` (e.g. a Slack decision about a GitHub issue).",
      "$ref": "#/$defs/workItemRef"
    },
    "occurredAt": {
      "description": "When the event was observed/emitted. For a signal an adapter discovers by polling, this is the observation time; `ratification.ratifiedAt` carries the native transition time when the two differ.",
      "type": "string",
      "format": "date-time"
    },
    "ratification": {
      "$ref": "#/$defs/ratification"
    },
    "summary": {
      "description": "Optional short human-readable description of the decision (e.g. the issue title or the message text). Kept short; never carries secret material.",
      "type": "string",
      "maxLength": 500
    },
    "sourceUrl": {
      "description": "Optional canonical URL of the event itself in the source system (e.g. the Slack message permalink, the GitHub review URL). `workItem.url` points at the item; this points at the event.",
      "type": "string",
      "maxLength": 2048
    },
    "decisionRefs": {
      "description": "Optional linkage to in-repo governance records (DEC/INS ids) this event proposes, ratifies, or supersedes. Closes the loop between an observed external decision and the DecisionRecord (decision-record.v1) that captures it.",
      "type": "array",
      "items": {
        "$ref": "#/$defs/decisionRef"
      }
    }
  },
  "$defs": {
    "actor": {
      "description": "Who produced the decision signal (the approver / requester / automation). Same shape as evidence.v1 actor.",
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id"],
      "properties": {
        "type": {
          "enum": ["human", "agent", "tool", "system"]
        },
        "id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 160
        }
      }
    },
    "workItemRef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["system", "id"],
      "properties": {
        "system": {
          "description": "Namespace the work item lives in — the first half of the cross-system join tuple.",
          "enum": ["jira", "slack", "linear", "github", "other"]
        },
        "id": {
          "description": "Source-native stable identifier of the work item (the second half of the join tuple). Format is source-specific and intentionally unconstrained beyond bounds; normalization is an adapter concern.",
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        },
        "url": {
          "description": "Optional canonical URL of the work item.",
          "type": "string",
          "maxLength": 2048
        }
      }
    },
    "ratification": {
      "description": "The normalized decision signal — the reason this seam exists. `signal` is the source-agnostic decision state; `method` and `sourceSignal` record HOW it was expressed natively, keeping the ratification auditable back to the source. A `ratified` signal MUST carry a `sourceSignal` (its native backing) so the strongest claim is never unfalsifiable.",
      "type": "object",
      "additionalProperties": false,
      "required": ["signal"],
      "properties": {
        "signal": {
          "description": "Normalized decision state. Aligns with decision-record.v1 status where they overlap (proposed/ratified/superseded) and adds `rejected` for observed declines.",
          "enum": ["proposed", "ratified", "rejected", "superseded"]
        },
        "method": {
          "description": "How the signal was expressed in the source system.",
          "enum": ["approval", "review", "reaction", "status-transition", "comment", "label", "automation"]
        },
        "ratifiedAt": {
          "description": "Optional native transition time of the ratification, when it differs from the top-level observation time `occurredAt`.",
          "type": "string",
          "format": "date-time"
        },
        "sourceSignal": {
          "description": "The raw native token that produced the signal — the audit anchor (e.g. 'status:In Review→Approved', 'review_state:approved', 'reaction::white_check_mark:'). Required when signal is 'ratified'.",
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        }
      },
      "allOf": [
        {
          "if": {
            "required": ["signal"],
            "properties": {
              "signal": { "const": "ratified" }
            }
          },
          "then": {
            "required": ["sourceSignal"]
          }
        }
      ]
    },
    "decisionRef": {
      "type": "string",
      "pattern": "^(DR-[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9][a-z0-9-]*|[A-Z]{2,6}-[0-9]{1,6})$"
    }
  }
}
