{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/request.schema.json",
  "title": "SGA-OIS request",
  "description": "A participant asking a bay to change or produce something, rather than asserting a measurement (03 section 1.1). These are the only messages of that kind, they share one namespace so the complete list is one thing to read, and who may make each is the request table in the bundle rather than anything encoded in the name. Every request is journaled -- asked and answered -- and travels over the protocol; a bay that could be asked things another way would have a second authority that nothing validates and nobody can audit.",
  "oneOf": [
    {
      "$ref": "#/$defs/endRound"
    },
    {
      "$ref": "#/$defs/addPlayer"
    },
    {
      "$ref": "#/$defs/removePlayer"
    },
    {
      "$ref": "#/$defs/updatePlayer"
    },
    {
      "$ref": "#/$defs/scorecard"
    },
    {
      "$ref": "#/$defs/applyRules"
    },
    {
      "$ref": "#/$defs/applyRulesAck"
    }
  ],
  "$defs": {
    "askedBy": {
      "type": "object",
      "required": [
        "role"
      ],
      "additionalProperties": false,
      "description": "On whose authority. A simulator receiving a request can answer 'who asked for this' from the message itself rather than from the fact that it arrived -- which is the question a vendor asks first about anything that can end their round.",
      "properties": {
        "role": {
          "enum": [
            "operator",
            "engine"
          ],
          "description": "Who asked. `operator` is a person, authorized by the cloud against the bay's claim and relayed to the bay; no participant can declare it (03 section 1). `engine` is the hub itself, named here because three of these are part of its ordinary work. The engine writes this; it is never taken from a participant."
        },
        "device_id": {
          "type": "string",
          "maxLength": 128,
          "description": "Which participant sent it, where one did."
        },
        "operator_ref": {
          "$ref": "common.schema.json#/$defs/operatorRef"
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "endRound": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "asked_by",
        "at",
        "release_players"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "request.end_round",
          "description": "End the round in progress. Only an operator may ask, relayed from the cloud: ending a round is a decision about somebody's afternoon, and the engine has no standing to take it."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "Unique within the session. A request is journaled when it is made and again when it is answered, and this is what joins the two -- so 'asked and refused' and 'asked and never answered' are different readings rather than the same silence."
        },
        "asked_by": {
          "$ref": "#/$defs/askedBy"
        },
        "at": {
          "type": "string",
          "format": "date-time",
          "description": "When it was asked, RFC 3339."
        },
        "round_ref": {
          "type": "string",
          "maxLength": 64,
          "description": "Which round it concerns, where that is not obvious from context. A session holds several (06 section 9.6)."
        },
        "release_players": {
          "type": "boolean",
          "description": "Whether the group has gone. False keeps the roster -- a stuck round, a restart, an operator fixing something -- and true clears it for whoever is next. Releasing MUST NOT alter any record: the round that was played is the round that was played."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "addPlayer": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "asked_by",
        "at"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "request.add_player",
          "description": "Seat a player. Any combination of name, tee and check-in reference, and at least one: they answer different questions and a caller may know any subset. A front desk setting a group up before they have walked over knows names and nothing else; a member who scanned in is a check-in reference and no name the bay should trust; an operator who knows the group is on the whites can say so for everybody before anyone touches the screen. There is deliberately no slot: the bay assigns positions, and a caller naming one would be guessing at a roster it cannot see."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "Unique within the session. A request is journaled when it is made and again when it is answered, and this is what joins the two -- so 'asked and refused' and 'asked and never answered' are different readings rather than the same silence."
        },
        "asked_by": {
          "$ref": "#/$defs/askedBy"
        },
        "at": {
          "type": "string",
          "format": "date-time",
          "description": "When it was asked, RFC 3339."
        },
        "round_ref": {
          "type": "string",
          "maxLength": 64,
          "description": "Which round it concerns, where that is not obvious from context. A session holds several (06 section 9.6)."
        },
        "display_label": {
          "type": "string",
          "maxLength": 64,
          "description": "What to call them on the simulator's own screen. For a claimed position it is a fallback: a member's screen name comes from their account, which is better evidence than anything typed at a desk."
        },
        "checkin_ref": {
          "type": "string",
          "maxLength": 128,
          "description": "The check-in this seating is explained by, where there is one. Opaque and engine-scoped: it means 'the person who scanned in at 17:02', and only the cloud can say who that was."
        },
        "tee": {
          "type": "string",
          "minLength": 1,
          "maxLength": 32,
          "description": "Which markers they play from, by name, matching a tee in the course document exactly (06 section 9.5). Omitted means the simulator's default -- a tee nobody chose is not the same as a tee somebody chose badly, and only the first is safe to change later without asking."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "anyOf": [
        {
          "required": [
            "display_label"
          ]
        },
        {
          "required": [
            "checkin_ref"
          ]
        },
        {
          "required": [
            "tee"
          ]
        }
      ],
      "description": "Seat a player. Any combination of name, tee and check-in reference, and at least one: they answer different questions and a caller may know any subset. A front desk setting a group up before they have walked over knows names and nothing else; a member who scanned in is a check-in reference and no name the bay should trust; an operator who knows the group is on the whites can say so for everybody before anyone touches the screen. There is deliberately no slot: the bay assigns positions, and a caller naming one would be guessing at a roster it cannot see."
    },
    "removePlayer": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "asked_by",
        "at",
        "slot"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "request.remove_player",
          "description": "Take a player out of the round. The engine may ask because a check-in lapsed or was ended, which leaves somebody in a round they are no longer at."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "Unique within the session. A request is journaled when it is made and again when it is answered, and this is what joins the two -- so 'asked and refused' and 'asked and never answered' are different readings rather than the same silence."
        },
        "asked_by": {
          "$ref": "#/$defs/askedBy"
        },
        "at": {
          "type": "string",
          "format": "date-time",
          "description": "When it was asked, RFC 3339."
        },
        "round_ref": {
          "type": "string",
          "maxLength": 64,
          "description": "Which round it concerns, where that is not obvious from context. A session holds several (06 section 9.6)."
        },
        "slot": {
          "type": "integer",
          "minimum": 1,
          "maximum": 36
        },
        "reason": {
          "type": "string",
          "maxLength": 64,
          "description": "Why, for the journal and for anybody reading it later. Not a status and not acted on."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "scorecard": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "asked_by",
        "at",
        "card_reason"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "request.scorecard",
          "description": "Produce a card, for a stated reason. The engine asks on its own cadence because the card and the shot stream are produced by different logic and disagreement is the signal (06 section 9.1)."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "Unique within the session. A request is journaled when it is made and again when it is answered, and this is what joins the two -- so 'asked and refused' and 'asked and never answered' are different readings rather than the same silence."
        },
        "asked_by": {
          "$ref": "#/$defs/askedBy"
        },
        "at": {
          "type": "string",
          "format": "date-time",
          "description": "When it was asked, RFC 3339."
        },
        "round_ref": {
          "type": "string",
          "maxLength": 64,
          "description": "Which round it concerns, where that is not obvious from context. A session holds several (06 section 9.6)."
        },
        "card_reason": {
          "enum": [
            "begin_round",
            "hole_complete",
            "interim_verify",
            "final_score"
          ],
          "description": "Which card is wanted. Named card_reason rather than reason because a request already has one kind of reason in it and two fields of that name, one hop apart, meaning different things is how a simulator writes the wrong value into the right field."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "applyRules": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "asked_by",
        "at",
        "package_digest",
        "package"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "request.apply_rules",
          "description": "Put a signed settings package in force. The one request whose authority is not the asker's role: it is SGA's SIGNATURE on the package that permits it. The engine MUST verify that signature against the pinned SGA root key before sending this request, and MUST NOT deliver a package that is unsigned, does not verify, has expired, or names an event or bundle version other than the one running (08 section 1.1). A simulator accepts this request only from the SGA Engine; it MAY verify the signature itself and is not required to."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "Unique within the session, and what the acknowledgment names -- so 'asked and answered', 'asked and refused' and 'asked and never answered' are three distinguishable readings of the journal."
        },
        "asked_by": {
          "$ref": "#/$defs/askedBy"
        },
        "at": {
          "type": "string",
          "format": "date-time"
        },
        "expires_at": {
          "type": "string",
          "format": "date-time",
          "description": "Every request carries a lifetime (03 section 1.1). A package applied long after it was sent puts a competition's conditions in force for a round it was never meant to govern."
        },
        "package_digest": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "description": "Digest of the package's canonical form with its signature member omitted (02 section 7.5); the same value the acknowledgment and the journal name."
        },
        "signature": {
          "type": "object",
          "description": "SGA's signature on the package: the authority. Absent only where a bay is running unsanctioned. The engine verifies it before delivery and never delivers an unsigned package to a sanctioned round; a simulator that chooses to verify it as well refuses a bad one with signature_invalid."
        },
        "package": {
          "$ref": "event-rules-package.schema.json",
          "description": "The Event Rules Package exactly as SGA signed it, signature included, so a simulator can verify what it is being asked to put in force (08 section 1.1)."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "applyRulesAck": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "at",
        "disposition"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "request.apply_rules_ack",
          "description": "The simulator answering request.apply_rules. Mandatory, and the only request that needs an explicit answer: the others are visible in their effects -- a roster changes, a card arrives -- while a simulator that never received a package, one that refused it, one still loading a course and one that quietly applied half of it are indistinguishable from outside."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "The request being answered."
        },
        "at": {
          "type": "string",
          "format": "date-time"
        },
        "disposition": {
          "enum": [
            "accepted",
            "refused"
          ],
          "description": "Whether the package was taken up at all. What an accepted package put in force is not reported here: the simulator publishes round.settings with changed_by rules_package and this request_id, and the engine compares that with the package (08 section 2). The simulator reports; the engine grades."
        },
        "reason": {
          "enum": [
            "signature_invalid",
            "package_unsupported",
            "package_expired",
            "not_ready"
          ],
          "description": "Required when refused. `not_ready` is the only one worth retrying: the package was authentic and understood, but the simulator could not act at that moment. `signature_invalid` is for a simulator that verifies the package itself; verification is required at the engine, before delivery (08 section 1.1)."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "updatePlayer": {
      "type": "object",
      "required": [
        "type",
        "request_id",
        "asked_by",
        "at",
        "slot"
      ],
      "additionalProperties": false,
      "anyOf": [
        {
          "required": [
            "tee"
          ]
        },
        {
          "required": [
            "display_label"
          ]
        }
      ],
      "properties": {
        "type": {
          "const": "request.update_player",
          "description": "Change a seated player's tee, display label or both (15 section 6). Never who the player is: no slot change and no checkin_ref."
        },
        "request_id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "description": "Unique within the session. A request is journaled when it is made and again when it is answered, and this is what joins the two -- so 'asked and refused' and 'asked and never answered' are different readings rather than the same silence."
        },
        "asked_by": {
          "$ref": "#/$defs/askedBy"
        },
        "at": {
          "type": "string",
          "format": "date-time",
          "description": "When it was asked, RFC 3339."
        },
        "round_ref": {
          "type": "string",
          "maxLength": 64
        },
        "slot": {
          "$ref": "common.schema.json#/$defs/slot"
        },
        "tee": {
          "type": "string",
          "minLength": 1,
          "maxLength": 32,
          "description": "Which markers they play from, by name, matching a tee in the course document exactly (06 section 9.5). Omitted means the simulator's default -- a tee nobody chose is not the same as a tee somebody chose badly, and only the first is safe to change later without asking."
        },
        "display_label": {
          "type": "string",
          "maxLength": 64,
          "description": "What to call them on the simulator's own screen. For a claimed position it is a fallback: a member's screen name comes from their account, which is better evidence than anything typed at a desk."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    }
  }
}
