{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://aisa.ablera.com/contracts/realtime-events.schema.json",
  "title": "Realtime event catalogue (SignalR hub /ai-support/hub)",
  "description": "Software Architecture § 7 Realtime: SignalR on the estate's proven topology — hub + one-use Redis ticket + HMAC group alias + Rabbit fan-out + transactional outbox (OUTBOX_MESSAGES). The UI subscribes per case (group 'case:<id>') and per role inbox (group 'gates:<role>'). Every event is a projection of ledger records already committed — a client that reconnects re-reads from /cases/{id}/cards?after=<seq_no>; events carry the cursor. Transcript lag target < 5 s; at most 10 notifications/s per client (ui.transcript). Payloads are references, never bodies.",
  "type": "object",
  "additionalProperties": false,
  "required": ["event", "version", "occurred_on", "seq_no"],
  "properties": {
    "event": { "type": "string", "enum": [
      "case.opened", "case.state.changed", "case.parked", "case.resumed", "case.controller.changed", "case.viewers.changed",
      "card.appended",
      "gate.opened", "gate.decided", "gate.escalated", "gate.expired",
      "held_batch.changed",
      "shape.state.changed",
      "arrival.received", "arrival.decided",
      "connector.health.changed",
      "budget.warning", "budget.stop",
      "kill_switch.changed",
      "ledger.export.lag",
      "eval_run.finished", "governance.changed",
      "client.request.changed" ] },
    "version": { "type": "integer", "const": 1 },
    "occurred_on": { "type": "string", "format": "date-time" },
    "seq_no": { "type": "integer", "description": "the ledger SEQ_NO of the record this event projects — the reconnect cursor" },
    "case_id": { "type": "string" },
    "group": { "type": "string", "description": "the SignalR group it was fanned out to: case:<id> · gates:<role> · control · client:<customer>" },
    "ref": {
      "type": "object", "additionalProperties": false,
      "description": "references only; the client fetches detail over HTTP",
      "properties": {
        "card_seq_no": { "type": "integer" }, "gate_id": { "type": "string" }, "arrival_id": { "type": "string" },
        "batch_id": { "type": "string" }, "shape_id": { "type": "string" }, "eval_run_id": { "type": "string" },
        "connector": { "type": "string" }, "environment": { "type": "string" }, "subject": { "type": "string" }, "key": { "type": "string" }, "version": { "type": "integer" }
      }
    },
    "state": { "type": "string", "description": "the new state, for *.state.changed / parked / health events" },
    "reason": { "type": "string" },
    "actor": { "type": "object", "properties": { "kind": { "type": "string", "enum": ["agent", "person", "policy", "system"] }, "id": { "type": "string" } } },
    "where": { "type": "object", "properties": { "environment": { "type": "string" }, "system": { "type": "string" }, "object": { "type": "string" }, "mode": { "type": "string" } } }
  },
  "allOf": [
    { "if": { "properties": { "event": { "const": "card.appended" } } }, "then": { "required": ["case_id", "ref"], "properties": { "ref": { "required": ["card_seq_no"] } } } },
    { "if": { "properties": { "event": { "pattern": "^gate\\." } } }, "then": { "required": ["case_id", "ref"], "properties": { "ref": { "required": ["gate_id"] } } } },
    { "if": { "properties": { "event": { "pattern": "^case\\." } } }, "then": { "required": ["case_id"] } },
    { "if": { "properties": { "event": { "const": "connector.health.changed" } } }, "then": { "required": ["ref", "state"], "properties": { "ref": { "required": ["connector", "environment"] } } } }
  ],
  "x-hub": {
    "url": "/ai-support/hub",
    "auth": "one-use ticket from POST /ai-support/api/v1/realtime/ticket (Authority JWT) — case-keyed; the ticket carries the role set",
    "client_methods": ["Subscribe(caseId)", "Unsubscribe(caseId)", "SubscribeGates()", "SubscribeControl()"],
    "server_methods": ["Event(payload)"],
    "reconnect": "re-read /cases/{id}/cards?after=<last seq_no>; the hub never replays"
  }
}
