{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/round-boundary.schema.json",
  "title": "SGA-OIS round boundary",
  "description": "The records that say where a round starts and stops (06 section 9.6). A round is the unit everything else is measured against -- a card, a player rating, a tournament result -- and these are the only things in the journal that state its extent. A round whose end is derived from the documents inside it cannot be closed when there are none: a power cut, a crash or a group walking out leaves the simulator that would have composed a card gone, and a round with no closing record never ends.",
  "oneOf": [
    {
      "$ref": "#/$defs/roundStarted"
    },
    {
      "$ref": "#/$defs/roundEnded"
    }
  ],
  "$defs": {
    "roundStarted": {
      "type": "object",
      "required": [
        "sga_kind",
        "round_ref",
        "at"
      ],
      "additionalProperties": false,
      "properties": {
        "sga_kind": {
          "const": "round_started",
          "description": "Document type discriminator."
        },
        "round_ref": {
          "$ref": "#/$defs/roundRef"
        },
        "course": {
          "type": "object",
          "description": "Which course is about to be played, by reference. The document itself is sent separately and once (06 section 9.5); this names it so a consumer reading only the boundary knows what it is looking at.",
          "required": [
            "course_id",
            "digest"
          ],
          "additionalProperties": false,
          "properties": {
            "course_id": {
              "type": "string",
              "minLength": 1
            },
            "digest": {
              "type": "string",
              "pattern": "^sha256:[0-9a-f]{64}$"
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          }
        },
        "at": {
          "$ref": "#/$defs/at"
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "roundEnded": {
      "type": "object",
      "required": [
        "sga_kind",
        "round_ref",
        "reason",
        "at"
      ],
      "additionalProperties": false,
      "properties": {
        "sga_kind": {
          "const": "round_ended",
          "description": "Document type discriminator."
        },
        "round_ref": {
          "$ref": "#/$defs/roundRef"
        },
        "reason": {
          "enum": [
            "completed",
            "operator_ended",
            "abandoned",
            "session_closed"
          ],
          "description": "How the round ended. `completed` is every hole played out; `operator_ended` is somebody at the front desk; `abandoned` is a group stopping mid-round, which is a first-class record and not a failure. `session_closed` is the one nobody chose: the engine found a round still open as its session ended, and closed it because the simulator that would have composed a card was already gone. A consumer MUST read that one as 'the round stopped here', never as 'the round finished here'."
        },
        "card_hash": {
          "type": "string",
          "pattern": "^sha256:[0-9a-f]{64}$",
          "description": "The final_score card that closes this round, by hash. Required for every reason except session_closed, and forbidden for that one -- there is no card, because the simulator is gone, and an engine composing one would be inventing a score. Carrying the hash is what makes the pairing verifiable rather than conventional: a consumer can prove the card it is scoring is the card that closed the round."
        },
        "discovery": {
          "type": "object",
          "description": "How the devices in this session found the engine, as they said in `descriptor.discovered_via` (03 §2.1), counted at the moment the round ended. Optional. A round played entirely over a fallback is a fact about that round, not only about the bay: it says the local network carried none of it.",
          "required": [
            "local_discovery"
          ],
          "additionalProperties": false,
          "properties": {
            "local_discovery": {
              "type": "boolean",
              "description": "Whether any device found the engine by a local mechanism (mDNS, unicast DNS-SD, BLE or DHCP). `configured` and `cached` are not local discovery: they are how a device reaches a bay somebody set up by hand."
            },
            "by_path": {
              "type": "object",
              "description": "Devices counted by the value they sent, including values this engine does not recognize, which are recorded as given.",
              "additionalProperties": {
                "type": "integer",
                "minimum": 1
              }
            },
            "silent": {
              "type": "integer",
              "minimum": 1,
              "description": "Devices that did not say. Absent when none."
            },
            "unknown": {
              "type": "integer",
              "minimum": 1,
              "description": "Devices that sent a value not in 03 §5. Absent when none."
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          }
        },
        "at": {
          "$ref": "#/$defs/at"
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "reason": {
                "const": "session_closed"
              }
            },
            "required": [
              "reason"
            ]
          },
          "then": {
            "description": "Nobody was there to compose a card.",
            "not": {
              "required": [
                "card_hash"
              ]
            }
          },
          "else": {
            "description": "A round that finished by any deliberate route closes with a score.",
            "required": [
              "card_hash"
            ]
          }
        }
      ],
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "roundRef": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "description": "Opaque, and unique WITHIN THE SESSION. It does not need to be globally unique because a session already is, and a short local reference keeps the journal readable. A consumer joins the two boundary records, and every record between them, by this."
    },
    "at": {
      "type": "string",
      "format": "date-time",
      "description": "When it happened, RFC 3339. The journal's own ordering is authoritative; this is what a human reads."
    }
  }
}
