{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/event-rules-package.schema.json",
  "title": "SGA-OIS event rules package",
  "description": "Signed by SGA and verified by the SGA Engine before it is delivered to a simulator (08 section 1.1), distributed to every participating bay before play. Removes every variable that moves a score and is not the player. Mandatory for sanctioned events; ignored entirely for casual play.",
  "type": "object",
  "required": [
    "event_id",
    "ruleset_version",
    "bundle_version",
    "course"
  ],
  "additionalProperties": false,
  "properties": {
    "sga_kind": {
      "const": "event-rules-package",
      "description": "Document type discriminator. Always event-rules-package. Optional on the wire -- a consumer that already knows what it asked for does not need it -- but present in stored and exchanged documents so a file on its own can say what it is."
    },
    "event_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "ruleset_version": {
      "type": "string",
      "description": "Every round records exactly which version applied, so a dispute three years later is re-evaluated under the rules that governed it rather than whatever we believe today."
    },
    "bundle_version": {
      "$ref": "common.schema.json#/$defs/bundleVersion"
    },
    "signature": {
      "$ref": "common.schema.json#/$defs/signature"
    },
    "issued_at": {
      "type": "string",
      "format": "date-time",
      "description": "Start of the validity window. With expires_at, event_id and bundle_version this is what keeps a signed package from being replayed into another event or kept past its time: the engine does not deliver a package outside its window (08 section 1.1)."
    },
    "expires_at": {
      "type": "string",
      "format": "date-time",
      "description": "End of the validity window. The engine does not deliver a package past it (08 section 1.1)."
    },
    "course": {
      "type": "object",
      "required": [
        "digest"
      ],
      "additionalProperties": false,
      "properties": {
        "digest": {
          "$ref": "common.schema.json#/$defs/digest"
        },
        "name": {
          "type": "string",
          "description": "Course name as the simulator presents it, for display."
        },
        "version": {
          "type": "string",
          "description": "Required course file version. A vendor can revise a layout without renaming it, and two competitors on different revisions are not playing the same event."
        },
        "source_url": {
          "type": "string",
          "format": "uri",
          "description": "The rights holder's own URL for this exact version. SGA supplies the pointer and the hash; the bytes come from whoever owns them."
        },
        "tee_set": {
          "type": "string",
          "description": "Which tees the field plays from."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "The course, layout and tees every competitor must play. Fixed by the event rather than chosen at the bay."
    },
    "conditions": {
      "type": "object",
      "required": [
        "mode"
      ],
      "properties": {
        "mode": {
          "enum": [
            "fixed",
            "per_hole"
          ],
          "description": "fixed: one setting for the whole event. per_hole: the same keys nested under hole numbers."
        },
        "wind_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Wind speed the simulator must apply."
        },
        "wind_direction_deg": {
          "type": "number",
          "minimum": 0,
          "exclusiveMaximum": 360,
          "description": "Compass bearing the wind blows toward; 0 is north. See the sign conventions in chapter 00."
        },
        "wind_variability": {
          "enum": [
            "none",
            "low",
            "medium",
            "high"
          ],
          "description": "How much the wind is permitted to vary during play. Zero is a steady wind, which is the fairest and least realistic setting."
        },
        "green_speed_stimp": {
          "type": "number",
          "minimum": 0,
          "maximum": 20,
          "description": "Green speed on the stimpmeter scale."
        },
        "firmness": {
          "$ref": "common.schema.json#/$defs/firmness"
        },
        "temperature_c": {
          "type": "number",
          "description": "Air temperature the flight model must assume."
        },
        "pressure_hpa": {
          "type": "number",
          "description": "Barometric pressure the flight model must assume."
        },
        "altitude_m": {
          "type": "number",
          "description": "Course altitude the flight model must assume. Materially changes carry, so it is fixed by the event rather than taken from the bay's location."
        },
        "pin_positions": {
          "type": "object",
          "description": "The pin placement for each hole, keyed by hole number. A position (preferred: it can be checked against the record's course.pin_position) or a label the course defines; a record echoes a label in course.pin_sheet_ref (08 section 1).",
          "additionalProperties": {
            "oneOf": [
              {
                "type": "string",
                "maxLength": 64
              },
              {
                "$ref": "shot-record.schema.json#/$defs/position"
              }
            ]
          }
        },
        "per_hole": {
          "type": "object",
          "description": "Hole number -> the same condition keys. Present when mode is per_hole.",
          "additionalProperties": {
            "type": "object"
          }
        }
      },
      "description": "The playing conditions the simulator must apply. Fixing these is what makes scores from different bays comparable at all -- a competitor who chooses their own wind is not in the same competition as one who does not."
    },
    "play_rules": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "tee_time_window": {
          "type": "array",
          "minItems": 2,
          "maxItems": 2,
          "items": {
            "$ref": "common.schema.json#/$defs/timestamp"
          },
          "description": "When a round may START."
        },
        "round_deadline": {
          "$ref": "common.schema.json#/$defs/timestamp",
          "description": "UTC instant after which no stroke counts, regardless of when the round began. The engine warns as it approaches (notice code round_ending) and does not end the round when it passes: strokes after it are journaled and do not count (08 section 2.2)."
        },
        "deadline_grace_ms": {
          "type": "integer",
          "minimum": 0,
          "default": 0,
          "description": "Whether the hole in progress may be completed after the deadline. Zero for a hard close."
        },
        "min_time_anchor": {
          "enum": [
            "signed_remote",
            "signed_cached"
          ],
          "default": "signed_cached",
          "description": "The weakest trusted time source acceptable for enforcing round_deadline. The bay system clock is never acceptable: it is settable by whoever is standing at the machine."
        },
        "holes": {
          "type": "array",
          "items": {
            "type": "integer"
          },
          "description": "Which holes make up the event, in order. Present so a nine-hole or a selected-holes event is expressed directly rather than inferred from what happens to get played."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "The rules of play the field competes under: what counts, what is conceded, and when the round must start and finish."
    },
    "assists": {
      "type": "object",
      "additionalProperties": false,
      "description": "The assists the event forbids and the ones it requires, in the vocabulary every round declares its settings in (15 section 9). The engine compares this with the simulator's round.settings (08 section 2). Named explicitly because the honest default in a home simulator is to leave assists on, and an event that does not say otherwise will find half its field using them.",
      "properties": {
        "prohibited": {
          "type": "array",
          "uniqueItems": true,
          "items": {
            "$ref": "common.schema.json#/$defs/assistId"
          },
          "description": "Assists that must not be in force. Proving one is off needs a complete declaration: a simulator whose round.settings is partial cannot show an assist is absent, and the key is unsupported."
        },
        "required": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/assistRequirement"
          },
          "description": "Assists that must be in force, with the parameter they must have -- a gimme distance, a mulligan allowance, the stroke cap that closes out a hole, or auto_putt for a format that resolves putts automatically."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "input_requirements": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "swing_input": {
          "enum": [
            "launch_monitor_only",
            "any"
          ],
          "description": "The kind of physical swing the event requires. What separates a competition from a simulation is that somebody actually swung."
        },
        "prohibited_inputs": {
          "type": "array",
          "items": {
            "enum": [
              "mouse",
              "keyboard",
              "gamepad",
              "touch",
              "simulated",
              "replay"
            ]
          },
          "description": "Input methods that must not be used -- entering a result by hand, replaying a stored shot, driving the simulator from a script."
        },
        "max_unmeasured_strokes": {
          "type": "integer",
          "minimum": 0,
          "description": "Gimmes, penalty drops and concessions are legitimately unmeasured. This bounds them rather than banning them."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "How a stroke must be produced for it to count."
    },
    "bay_requirements": {
      "type": "object",
      "additionalProperties": false,
      "description": "What the event requires of the ROOM, checked against the signed bay certificate SGA issued rather than against connected hardware. Not interchangeable with device_requirements: most of what makes a bay certified cannot be observed from the bay -- calibration records reviewed by a person, the installation inspected, the facility's history considered. An event that needs certification and asks only for attestation will accept a bay nobody ever audited.",
      "properties": {
        "min_certification_tier": {
          "type": "string",
          "maxLength": 32,
          "description": "Minimum SGA certification tier, compared by the declared rank in the bundle's tier catalog. Enforced at ROUND START: a bay that is uncertified, certified below this, or whose certification has lapsed does not host the round. A tier the bundle cannot resolve is refused rather than assumed satisfied."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "device_requirements": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "launch_monitor_required": {
          "type": "boolean",
          "description": "Whether a launch monitor must be present and contributing. Without one the ball data is the simulator's own model rather than a measurement, which is a different provenance and usually a different competition."
        },
        "min_attestation_tier": {
          "$ref": "common.schema.json#/$defs/attestationTier"
        },
        "certified_models_only": {
          "type": "boolean",
          "description": "When true, only device models on SGA's certified list may act as the source of record. Other equipment may still be present and still record."
        },
        "firmware_digest_allowlist": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/digest"
          },
          "description": "Exact firmware digests permitted. The strongest form of the requirement: not a version string a device could claim, but a hash of the build it is actually running."
        },
        "required_fields": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Fields that MUST be present at measured provenance for a stroke to count."
        },
        "calibration_max_age_days": {
          "type": "integer",
          "minimum": 0,
          "description": "How stale a device's calibration may be and still count. Calibration ages quietly, which is exactly why an event states a limit rather than trusting that somebody checked."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "What the equipment in the bay must be and must prove."
    },
    "software_requirements": {
      "type": "object",
      "additionalProperties": false,
      "description": "What the event requires of the SIMULATOR, as device_requirements is what it requires of the hardware (08 section 1).",
      "properties": {
        "min_vendor_assurance": {
          "enum": [
            "none",
            "verified_build",
            "certified"
          ],
          "description": "The lowest vendor assurance accepted (02 section 2.1): whether a named party is accountable for what this software reports. Not interchangeable with min_attestation_tier."
        },
        "settings_lockout_required": {
          "type": "boolean",
          "default": false,
          "description": "The simulator must hold the settings this package pins for the whole round, and declare settings_lockout (08 section 2.1a)."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "identity_requirements": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "assurance_level": {
          "enum": [
            "basic",
            "verified",
            "championship"
          ],
          "description": "How strongly a player's identity must be established. A club night and a national final need very different answers, and stating it here keeps the requirement with the event rather than in a separate rulebook."
        },
        "presence": {
          "type": "array",
          "items": {
            "enum": [
              "qr_checkin",
              "proximity_token",
              "shot_cadence",
              "biometric_unlock",
              "camera"
            ]
          },
          "description": "The evidence required that the player was physically at the bay. Presence is graded, not binary -- see chapter 07."
        },
        "capture_disclosed_at_registration": {
          "type": "boolean",
          "description": "MUST be true wherever camera capture is required. Nobody should meet the camera for the first time when they arrive to play."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "How sure the event needs to be about who actually played."
    }
  },
  "patternProperties": {
    "^x-[a-z0-9_-]+\\.": {}
  },
  "$defs": {
    "conformanceVector": {
      "type": "object",
      "required": [
        "verdict"
      ],
      "additionalProperties": false,
      "description": "The engine's signed comparison of required settings against what the simulator reported actually in force. Distributing settings to a bay does not make them true; APPLIED is a report of live state, not an acknowledgment.",
      "properties": {
        "event_id": {
          "$ref": "common.schema.json#/$defs/sgaId"
        },
        "verdict": {
          "enum": [
            "conforming",
            "divergent",
            "unsupported",
            "unattested"
          ],
          "description": "Whether the bay satisfied the package as a whole. The single answer a scoring system acts on; everything else in the vector explains it."
        },
        "conforming": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The requirements the bay met, listed so a passing verdict is auditable rather than merely asserted."
        },
        "divergent": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "key",
              "required",
              "applied"
            ],
            "additionalProperties": false,
            "properties": {
              "key": {
                "type": "string",
                "description": "Which requirement diverged."
              },
              "required": {
                "description": "What the package asked for."
              },
              "applied": {
                "description": "What the bay actually applied."
              },
              "reason": {
                "type": "string",
                "description": "Why the bay diverged, where it knows -- equipment that cannot express the setting, an operator override, a value out of range."
              }
            },
            "patternProperties": {
              "^x-[a-z0-9_-]+\\.": {}
            }
          },
          "description": "Requirements the bay applied differently from what the package asked. Recorded rather than rejected: a round that diverged is still a round, and whether it counts is a scoring decision made with this evidence in hand."
        },
        "unsupported": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Requirements the bay could not evaluate at all, as distinct from ones it evaluated and failed. Not knowing is not the same as not complying, and collapsing the two would either excuse real failures or fail honest bays."
        },
        "checked_at": {
          "$ref": "common.schema.json#/$defs/timestamp"
        },
        "phase": {
          "enum": [
            "pre_round",
            "mid_round"
          ],
          "description": "Pre-round divergence blocks; mid-round divergence flags. Stopping a round on a settings change punishes the honest case and does not stop the dishonest one, which is already recorded."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    }
  }
}
