{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/shot-record.schema.json",
  "title": "SGA-OIS shot record",
  "description": "A shot is a transition between two states with a path between them. All distances are ground distances in meters unless the field name says otherwise; all units and sign conventions are fixed by spec/00-conventions.md. Provenance resolves through the session manifest, not inline.",
  "type": "object",
  "required": [
    "shot_id",
    "session_id",
    "manifest",
    "bundle_version",
    "slot",
    "t_ms"
  ],
  "additionalProperties": false,
  "properties": {
    "sga_kind": {
      "const": "shot-record",
      "description": "Document type discriminator. Always shot-record. 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."
    },
    "shot_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "session_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "round_id": {
      "$ref": "common.schema.json#/$defs/sgaId",
      "description": "The round this stroke belongs to. A round may span several sessions -- nine holes tonight, nine on Thursday -- so this, not the session clock, is what holds a round together across a gap."
    },
    "manifest": {
      "$ref": "common.schema.json#/$defs/digest",
      "description": "Hash of the session manifest in force. Resolves every field's provenance and the full device configuration."
    },
    "bundle_version": {
      "$ref": "common.schema.json#/$defs/bundleVersion"
    },
    "player_up": {
      "$ref": "common.schema.json#/$defs/playerUp",
      "description": "Who was at the ball when this stroke was struck, resolved by the engine at the moment of the shot rather than when a contributing device happened to finish. Late-arriving kinematics or video are attributed by this, not by whoever is up when they land."
    },
    "slot": {
      "$ref": "common.schema.json#/$defs/slot"
    },
    "t_ms": {
      "$ref": "common.schema.json#/$defs/monotonicMs"
    },
    "session_epoch": {
      "$ref": "common.schema.json#/$defs/sessionEpoch"
    },
    "device_t_ms": {
      "$ref": "common.schema.json#/$defs/deviceMs"
    },
    "struck_at": {
      "$ref": "common.schema.json#/$defs/timestamp"
    },
    "hole": {
      "type": "integer",
      "minimum": 1,
      "maximum": 36,
      "description": "Which hole this stroke was played on."
    },
    "stroke_index": {
      "type": "integer",
      "minimum": 1,
      "description": "The stroke's position within the hole for this slot, starting at 1. Ordering within a hole comes from this, never from wall-clock time."
    },
    "course": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "course_id": {
          "$ref": "common.schema.json#/$defs/sgaId"
        },
        "name": {
          "type": "string",
          "maxLength": 200,
          "description": "Course name as the simulator presents it. A label for humans; identity rests on the digest."
        },
        "source": {
          "type": "string",
          "maxLength": 100,
          "description": "Where the course file came from -- the simulator's own library, a marketplace, a community pack. Bears on whether two bays playing the same name are playing the same thing."
        },
        "vendor_ref": {
          "type": "string",
          "maxLength": 100,
          "description": "The vendor's own identifier for the course, so a support conversation can name the same file the vendor names."
        },
        "version": {
          "type": "string",
          "maxLength": 50,
          "description": "Course file version. A layout can be revised without being renamed."
        },
        "digest": {
          "$ref": "common.schema.json#/$defs/digest",
          "description": "The identity that matters. Name and version are labels; the digest is the course."
        },
        "digest_verified": {
          "type": "boolean",
          "description": "true only if the engine computed the hash itself. false means it is relaying the simulator's claim."
        },
        "tee_set": {
          "type": "string",
          "maxLength": 50,
          "description": "Which tees were played, which sets both the yardage and the rating the score is judged against."
        },
        "hole_par": {
          "type": "integer",
          "minimum": 3,
          "maximum": 6,
          "description": "Par for this hole, as the course file states it."
        },
        "hole_length_m": {
          "type": "number",
          "minimum": 0,
          "description": "Length of this hole from the tees in play."
        },
        "georeference": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "datum",
            "origin",
            "bearing_deg"
          ],
          "description": "How this course file maps onto the real world. Declared ONCE here rather than repeated on every position: one transform cannot disagree with itself, and a course without this simply cannot be overlaid, which is the honest failure. Absent means the course is not georeferenced.",
          "properties": {
            "datum": {
              "type": "string",
              "maxLength": 32,
              "description": "Reference frame the origin is expressed in."
            },
            "epoch": {
              "type": "string",
              "maxLength": 16,
              "description": "Epoch of that frame."
            },
            "origin": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "lat_deg",
                "lon_deg"
              ],
              "description": "Real-world location of local (0, 0).",
              "properties": {
                "lat_deg": {
                  "type": "number",
                  "minimum": -90,
                  "maximum": 90,
                  "description": "Latitude of local (0, 0), decimal degrees."
                },
                "lon_deg": {
                  "type": "number",
                  "minimum": -180,
                  "maximum": 180,
                  "description": "Longitude of local (0, 0), decimal degrees."
                },
                "height_m": {
                  "type": "number",
                  "description": "Height of the local reference surface, meters."
                },
                "height_ref": {
                  "enum": [
                    "ellipsoidal",
                    "orthometric"
                  ],
                  "description": "Which height convention the origin uses. The whole transform inherits it, so an omission here propagates to every derived world position."
                }
              },
              "patternProperties": {
                "^x-[a-z0-9_-]+\\.": {}
              }
            },
            "bearing_deg": {
              "type": "number",
              "minimum": 0,
              "exclusiveMaximum": 360,
              "description": "Compass bearing of the local +y axis, degrees clockwise from true north. This is what turns a launch azimuth into a real-world direction."
            },
            "accuracy_m": {
              "type": "number",
              "minimum": 0,
              "description": "How well the transform itself is known. A course digitized from aerial imagery is not surveyed, and saying so prevents a derived world position from claiming more than the map it came from."
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          }
        },
        "pin_position": {
          "$ref": "#/$defs/position",
          "description": "Where the hole was actually cut for this round. distance_to_pin_m is the input to nearly everything the standard computes, and nothing else records where the pin was -- so two rounds on the same course file are not comparable without it, and a pro's approach to a Sunday pin is not the same shot as yours to a fixed one."
        },
        "pin_sheet_ref": {
          "type": "string",
          "maxLength": 64,
          "description": "The day's pin sheet designation as the facility labels it, for example 'Thu back-right'. A human-readable companion to pin_position, not a substitute for it."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "The course, layout and hole this stroke was played on."
    },
    "player": {
      "type": "object",
      "additionalProperties": false,
      "description": "The player's handedness, from the two places it is known, unmerged. A simulator configured for a right-hander while a left-hander plays mirrors every number after it, and that is only visible if both answers survive -- so the record carries both and resolves neither (00 section 7). Where they differ, the difference IS the finding; no separate disagreement flag is needed to assert something a reader can see. Where only one is present, that is the only answer this bay had.",
      "properties": {
        "sim_handedness": {
          "$ref": "common.schema.json#/$defs/handedness",
          "description": "What the simulator was CONFIGURED for -- a setting, which decides how the shot was rendered and where the ball was teed. An inference about the player made from a menu, and it carries `inferred` provenance to say so."
        },
        "lm_handedness": {
          "$ref": "common.schema.json#/$defs/handedness",
          "description": "What a sensor in the room OBSERVED -- a launch monitor (which side of the ball armed, 10), or a camera watching the swing (11 section 4.2a). Measured, because it comes from watching rather than from a menu."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "origin": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "lie_surface": {
          "$ref": "common.schema.json#/$defs/lieSurface"
        },
        "lie_slope_along_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Positive is uphill toward the target."
        },
        "lie_slope_across_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Positive means the ground rises to the player's right, regardless of handedness."
        },
        "distance_to_pin_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance from the ball to the pin at address."
        },
        "elevation_change_m": {
          "type": "number",
          "description": "Target relative to the ball; positive means the pin is above you. Part of the state, not a modifier of it."
        },
        "origin_elevation_m": {
          "type": "number",
          "description": "Absolute elevation of the ball's position, where the course model supplies one."
        },
        "position": {
          "$ref": "#/$defs/position"
        },
        "stance_obstructed": {
          "type": "boolean",
          "description": "The player could not take a normal stance -- against a tree, in a bunker lip, on a cart path. Recorded because it explains poor contact that would otherwise look like a bad swing."
        },
        "recommended_club": {
          "type": "string",
          "maxLength": 16,
          "description": "The club the simulator suggested. A suggestion, not a measurement, and never evidence of what was actually swung."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "Where the ball was, and what it was sitting on, before the stroke. Part of the state of the shot rather than a modifier applied to it."
    },
    "environment": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "wind_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Wind speed the simulator applied."
        },
        "wind_direction_deg": {
          "type": "number",
          "minimum": 0,
          "exclusiveMaximum": 360,
          "description": "Bearing the wind blows TOWARD, 0 = north. Not the meteorological from-convention."
        },
        "wind_gust_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Peak gust the simulator applied, where the model varies the wind."
        },
        "temperature_c": {
          "type": "number",
          "minimum": -50,
          "maximum": 60,
          "description": "Air temperature the flight model used."
        },
        "pressure_hpa": {
          "type": "number",
          "minimum": 800,
          "maximum": 1100,
          "description": "Barometric pressure the flight model used."
        },
        "humidity_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Relative humidity the flight model used."
        },
        "altitude_m": {
          "type": "number",
          "minimum": -500,
          "maximum": 6000,
          "description": "Course altitude the flight model used. Materially changes carry."
        },
        "green_speed_stimp": {
          "type": "number",
          "minimum": 0,
          "maximum": 20,
          "description": "Green speed the simulator applied, on the stimpmeter scale."
        },
        "firmness": {
          "$ref": "common.schema.json#/$defs/firmness"
        },
        "conditions_source": {
          "enum": [
            "simulator_configured",
            "measured",
            "unknown"
          ],
          "description": "A wind value that is a game setting and a wind value that is the weather are not the same fact."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "The conditions the simulator applied to this shot. What the flight model believed, which is not necessarily what a sensor in the room measured -- see ambient for that."
    },
    "ambient": {
      "type": "object",
      "additionalProperties": false,
      "description": "Conditions PHYSICALLY MEASURED where the player is standing, always by a sensor and never a setting. Distinct from `environment`, which is the air the shot was played in -- indoors those are different: the sim simulates a seaside links in a light breeze while the player stands in a 6 degree garage. Both are true at once, which is why they are separate blocks. Outdoors they converge, and the same values may legitimately appear in both.",
      "properties": {
        "temperature_c": {
          "type": "number",
          "minimum": -30,
          "maximum": 60,
          "description": "The reason this block exists indoors. A golf ball at 5 C leaves the face measurably slower than the same ball at 20 C, so a player in an unheated garage in January scores worse for reasons that are not golf -- and a rating model with no view of the room attributes it to them."
        },
        "ball_temperature_c": {
          "type": "number",
          "minimum": -30,
          "maximum": 60,
          "description": "Temperature of the ball itself, where a sensor reports it. A cold ball travels measurably shorter, which is a real effect the simulator's air temperature does not capture."
        },
        "humidity_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Relative humidity measured in the bay."
        },
        "pressure_hpa": {
          "type": "number",
          "minimum": 800,
          "maximum": 1100,
          "description": "Barometric pressure measured in the bay."
        },
        "altitude_m": {
          "type": "number",
          "minimum": -500,
          "maximum": 6000,
          "description": "Altitude of the bay itself, as measured or configured. Distinct from the course altitude the simulator applied."
        },
        "air_density_kgm3": {
          "type": "number",
          "minimum": 0,
          "maximum": 2,
          "description": "Air density measured in the bay, or derived from the measured pressure, temperature and humidity."
        },
        "wind_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Outdoor and hybrid play only. Indoors this should be absent, not zero."
        },
        "wind_direction_deg": {
          "type": "number",
          "minimum": 0,
          "exclusiveMaximum": 360,
          "description": "Direction of any air movement measured in the bay. Compass bearing the air moves toward; 0 is north."
        },
        "illuminance_lux": {
          "type": "number",
          "minimum": 0,
          "description": "Camera-based systems declare requires_lighting; this is how a bay evidences it."
        },
        "measured_by": {
          "$ref": "common.schema.json#/$defs/sgaId"
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "club": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "club_id": {
          "type": "string",
          "maxLength": 16,
          "description": "Which club was used. How that was determined, and how much to trust it, is in club.identification -- the id alone carries no confidence."
        },
        "tag_id": {
          "type": "string",
          "maxLength": 64,
          "description": "Which PHYSICAL club this is, verbatim from whatever carries the identity: usually an RFID or NFC tag, but any marking a device can read and that is unique to the one club. Opaque, never parsed, never assumed to encode the club type. club_id says an 8-iron was swung; tag_id says WHICH 8-iron, which is what lets a specification recorded once -- in the member's registered bag, or by a fitter -- be attached to every later shot without anybody re-entering it. A device that can read a tag but knows nothing else about the club publishes the id and stops there; a device that does know the specification sends club.spec as well (06 5.0a)."
        },
        "speed_mps": {
          "type": "number",
          "minimum": 0,
          "maximum": 80,
          "description": "Club head speed."
        },
        "speed_at_impact_mps": {
          "type": "number",
          "minimum": 0,
          "maximum": 80,
          "description": "Club head speed at the moment of impact specifically, for devices that distinguish it from peak speed. The two differ on a decelerating stroke."
        },
        "path_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Positive is in-to-out."
        },
        "face_angle_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Positive is open."
        },
        "face_to_path_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Face angle relative to the swing path. Positive is the face open to the path. The single strongest predictor of curvature, which is why it is carried directly rather than left to be derived from face and path separately."
        },
        "angle_of_attack_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Positive is upward."
        },
        "dynamic_loft_deg": {
          "type": "number",
          "minimum": -10,
          "maximum": 90,
          "description": "Loft actually delivered at impact, as opposed to the loft stamped on the club."
        },
        "dynamic_lie_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Positive is toe up."
        },
        "spin_loft_deg": {
          "type": "number",
          "minimum": -10,
          "maximum": 90,
          "description": "Angle between the dynamic loft and the angle of attack. Largely determines how much spin the stroke generates."
        },
        "swing_plane_vertical_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Vertical angle of the club head path relative to the ground. Launch monitors that report swing plane report the vertical and horizontal components as distinct quantities; they are not interchangeable."
        },
        "swing_plane_horizontal_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Horizontal orientation of the swing plane."
        },
        "swing_direction_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Direction of the club head's travel through impact, measured on the swing plane. Positive is in-to-out."
        },
        "closure_rate_dps": {
          "type": "number",
          "description": "How fast the face was rotating through impact, in degrees per second. Explains why two swings with the same path produce different curvature."
        },
        "low_point_m": {
          "type": "number",
          "description": "Where the club reached the bottom of its arc, relative to the ball. Positive is after the ball, which is the ball-then-turf contact an iron wants."
        },
        "strike_horizontal_mm": {
          "type": "number",
          "minimum": -80,
          "maximum": 80,
          "description": "Impact position across the face, from the center. Positive is toward the toe."
        },
        "strike_vertical_mm": {
          "type": "number",
          "minimum": -80,
          "maximum": 80,
          "description": "Impact position up the face, from the center. Positive is above center."
        },
        "vendor_club_code": {
          "description": "The vendor's own opaque identifier, verbatim and unmapped. Some simulators emit an uninterpreted integer club id; it passes through rather than being guessed at, and can be decoded later without recapturing anything."
        },
        "spec": {
          "$ref": "#/$defs/clubSpec"
        },
        "identification": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "method"
          ],
          "description": "How club_id was arrived at. Never required for a valid shot record and never used in rating -- the ball goes where the launch data says regardless of what was swung. It gates exactly one thing: whether per-club statistics can honestly be shown to a player.",
          "properties": {
            "method": {
              "$ref": "common.schema.json#/$defs/clubIdMethod"
            },
            "confidence": {
              "type": "number",
              "minimum": 0,
              "maximum": 1,
              "description": "For imaged, marker and inferred methods. A ui_selected club has no meaningful confidence -- it is a claim, and its reliability is a property of the human, not the system."
            },
            "source": {
              "$ref": "common.schema.json#/$defs/sgaId",
              "description": "Which participant asserted the club. Often the simulator rather than the launch monitor."
            },
            "player_confirmed": {
              "type": "boolean",
              "description": "The player was shown the detected club and accepted it. Raises an inferred identification toward, but not to, the standard of a measured one."
            },
            "disagreement": {
              "type": "object",
              "additionalProperties": false,
              "description": "Two sources named different clubs. Recorded rather than resolved: a UI selection contradicting a detection is the single most useful signal that per-club statistics for this player are unreliable.",
              "properties": {
                "ui_selected": {
                  "type": "string",
                  "maxLength": 16,
                  "description": "The club the player or simulator said was in hand."
                },
                "detected": {
                  "type": "string",
                  "maxLength": 16,
                  "description": "The club the bay actually detected. Present only when it differs from what was selected -- the disagreement is recorded rather than resolved, because which one is right depends on evidence the record cannot settle on its own."
                }
              },
              "patternProperties": {
                "^x-[a-z0-9_-]+\\.": {}
              }
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          }
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "What the club was doing through impact. Measured by the launch monitor where it can see the club, and absent rather than estimated where it cannot."
    },
    "ball": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "speed_mps": {
          "type": "number",
          "minimum": 0,
          "maximum": 130,
          "description": "Ball speed immediately after impact."
        },
        "tag_id": {
          "type": "string",
          "maxLength": 64,
          "description": "Which physical ball this is, verbatim, from an embedded marker a device can read. Nothing on the market reports this today; the field exists because a ball that identifies itself is the missing half of equipment data, and a bay that can read one should have somewhere to put it rather than dropping it. Same rules as club.tag_id: opaque, never parsed, resolved to a specification elsewhere."
        },
        "launch_angle_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 90,
          "description": "Vertical launch angle relative to the ground."
        },
        "launch_direction_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Positive is right of the target line."
        },
        "spin_rpm": {
          "type": "number",
          "minimum": 0,
          "maximum": 20000,
          "description": "Total spin rate. Where a device reports total spin and the axis separately, this is the total."
        },
        "back_spin_rpm": {
          "type": "number",
          "minimum": -5000,
          "maximum": 20000,
          "description": "Backspin component."
        },
        "side_spin_rpm": {
          "type": "number",
          "minimum": -10000,
          "maximum": 10000,
          "description": "Sidespin component. Carried where a device measures the components directly; do not derive it from total spin and axis and present the result as measured."
        },
        "spin_axis_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Positive is tilted right. A geometric quantity, not a shot shape: 'draw' and 'fade' are handedness-dependent readings of the same number and MUST NOT appear as values."
        },
        "smash_ratio": {
          "type": "number",
          "minimum": 0,
          "maximum": 2.5,
          "description": "Ball speed divided by club head speed. Derived, not measured -- a device that reports it without measuring both inputs is reporting an assumption."
        },
        "carry_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance the ball flew before first contact with the ground."
        },
        "total_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance from the ball's start to where it came to rest, carry and roll together."
        },
        "spec": {
          "$ref": "#/$defs/ballSpec"
        },
        "side_carry_m": {
          "type": "number",
          "description": "Lateral offset from the target line at the point of first landing. Positive is right of the target line."
        },
        "side_total_m": {
          "type": "number",
          "description": "Lateral offset from the target line at rest. Positive is right of the target line."
        },
        "roll_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance the ball traveled on the ground after landing."
        },
        "curve_m": {
          "type": "number",
          "description": "Side movement perpendicular to the LAUNCH direction, not to the target line. Distinct from side_total_m and routinely confused with it."
        },
        "landing_angle_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Angle at which the ball descends through its launch elevation. Not the same as trajectory.descent_angle_deg, which is measured where the ball first meets the ground: the two differ whenever the landing area is above or below the tee."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "What the ball did off the face. The core measurement of the stroke, and the one most likely to be present on any device worth connecting."
    },
    "putt": {
      "type": "object",
      "additionalProperties": false,
      "description": "Putting parameters, emitted when origin.lie_surface is green or fringe. A largely separate family from full-swing data -- tour-grade launch monitors document a couple of dozen putting parameters that barely overlap the full-swing set. It matters disproportionately for rating: putting is where amateurs lose the most strokes, so it is where a shot-level player rating has the most to say.",
      "properties": {
        "stroke_length_m": {
          "type": "number",
          "minimum": 0,
          "description": "How far the putter was taken back."
        },
        "backswing_time_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Duration of the backswing, from takeaway to the top of the stroke."
        },
        "forward_swing_time_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Duration of the forward stroke, from the top to impact. The ratio between this and the backswing is what putting coaches call tempo."
        },
        "tempo_ratio": {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "Backswing time divided by forward swing time."
        },
        "launch_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Ball speed as it leaves the putter face."
        },
        "skid_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance the ball skids or bounces before true roll begins."
        },
        "roll_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Speed at which the ball begins rolling."
        },
        "speed_drop_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "How much speed the ball lost before beginning true roll. Large values mean the ball was driven into the green rather than rolled along it."
        },
        "roll_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Share of the putt spent in true roll rather than skid. A putt that rolls sooner holds its line sooner."
        },
        "effective_stimp": {
          "type": "number",
          "minimum": 0,
          "maximum": 20,
          "description": "Green speed inferred from this putt's roll deceleration. Distinct from environment.green_speed_stimp, which is the configured setting -- comparing the two is a useful consistency check."
        },
        "break_m": {
          "type": "number",
          "description": "Total side movement from the launch direction."
        },
        "entry_speed_distance_m": {
          "type": "number",
          "minimum": 0,
          "description": "How far past the hole the ball would have finished at its entry speed. The standard way of expressing whether a putt was struck firmly or died in."
        },
        "slope_pct": {
          "type": "number",
          "description": "Slope across the putt's line, as a percentage grade."
        },
        "slope_rise_pct": {
          "type": "number",
          "description": "Slope along the putt's line, as a percentage grade. Positive is uphill toward the hole."
        },
        "putter_path_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Path of the putter head through impact. Positive is in-to-out."
        },
        "putter_face_angle_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Putter face orientation at impact. Positive is open, pointing right of the target line. Face angle dominates start line on a putt far more than it does on a full swing."
        },
        "putter_loft_deg": {
          "type": "number",
          "minimum": -20,
          "maximum": 20,
          "description": "Loft delivered at impact. Positive lofts the ball; a negative value means the ball was pressed into the green."
        },
        "putter_lie_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Lie angle at impact. Positive is toe up."
        },
        "putter_aoa_deg": {
          "type": "number",
          "minimum": -20,
          "maximum": 20,
          "description": "Angle of attack of the putter head. Positive is upward, which is generally what produces early roll."
        },
        "aim_deg": {
          "type": "number",
          "minimum": -45,
          "maximum": 45,
          "description": "Where the putter was aimed at address, relative to the target line. Distinct from face angle at impact, and the difference between them is most of putting instruction."
        },
        "true_roll_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance at which the ball begins true forward roll."
        },
        "entry_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Ball speed at the hole."
        },
        "impact_spot_mm": {
          "type": "number",
          "description": "Contact position across the putter face, positive toward the toe."
        },
        "rise_angle_deg": {
          "type": "number",
          "minimum": -20,
          "maximum": 20,
          "description": "Launch angle of the ball off the putter face."
        },
        "rotation_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Total face rotation through the stroke."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "swing": {
      "type": "object",
      "additionalProperties": false,
      "description": "Body kinematics. The most fragmented category in the industry -- no two vendors name these the same way, and several disagree on sign. Scalars are reported AT NAMED EVENTS; anything continuous is a series reference on the bulk path. All angles positive TOWARD the target unless stated, and lead/trail rather than left/right, so the vocabulary is handedness-independent.",
      "properties": {
        "events": {
          "type": "object",
          "additionalProperties": false,
          "description": "Offset of each named moment relative to impact. Negative before impact. Every key carries its own unit suffix, per 00-conventions -- putting the unit on the parent object would leave the leaf names bare.",
          "properties": {
            "address_ms": {
              "type": "integer",
              "description": "When the player settled at address, as an offset relative to impact. Negative, like every event before impact."
            },
            "takeaway_ms": {
              "type": "integer",
              "description": "When the club started back, relative to impact."
            },
            "top_ms": {
              "type": "integer",
              "description": "The top of the backswing, relative to impact."
            },
            "transition_ms": {
              "type": "integer",
              "description": "When the downswing began, relative to impact. The transition is where sequencing goes right or wrong."
            },
            "impact_ms": {
              "type": "integer",
              "description": "Impact itself. Zero by definition -- it is the origin every other event in this block is measured from, and it is carried explicitly so a consumer never has to assume that."
            },
            "finish_ms": {
              "type": "integer",
              "description": "When the swing finished, relative to impact. Positive."
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          }
        },
        "tempo_ratio": {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "Backswing time divided by downswing time."
        },
        "at": {
          "type": "object",
          "description": "Segment measurements at each named event. Key is a swingEvent.",
          "propertyNames": {
            "$ref": "common.schema.json#/$defs/swingEvent"
          },
          "additionalProperties": {
            "$ref": "#/$defs/postureFrame"
          }
        },
        "peak": {
          "$ref": "#/$defs/postureFrame",
          "description": "Peak value of each measurement across the swing, whenever it occurred."
        },
        "kinematic_sequence": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "peak_order": {
              "type": "array",
              "items": {
                "$ref": "common.schema.json#/$defs/bodySegment"
              },
              "description": "Segments ordered by when each reached peak angular velocity. The proximal-to-distal ordering is the whole point of the measurement."
            },
            "peaks": {
              "type": "array",
              "description": "One entry per segment. An array rather than a segment-keyed map so every numeric leaf carries its own unit suffix, and so the timing of each peak travels with it -- the ORDER is the measurement, and order without timing is half of it.",
              "items": {
                "type": "object",
                "required": [
                  "segment",
                  "peak_speed_dps"
                ],
                "additionalProperties": false,
                "properties": {
                  "segment": {
                    "$ref": "common.schema.json#/$defs/bodySegment"
                  },
                  "peak_speed_dps": {
                    "type": "number",
                    "description": "Peak angular speed reached by this segment, in degrees per second."
                  },
                  "at_ms": {
                    "type": "integer",
                    "description": "When the peak occurred, relative to impact."
                  }
                },
                "patternProperties": {
                  "^x-[a-z0-9_-]+\\.": {}
                }
              }
            },
            "sequenced": {
              "type": "boolean",
              "description": "Whether peaks occurred in proximal-to-distal order."
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          },
          "description": "The order and timing in which body segments reached peak speed. Efficient swings peak from the ground up, and the sequence is what says whether this one did."
        },
        "series": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/seriesRef"
          },
          "description": "Pointers to the sampled biomechanical traces for this swing. Held on the bulk path rather than inlined -- a 240 Hz trace through a two-second swing is hundreds of samples, and putting that in a latency-critical record would be an error of kind, not of size."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "pressure": {
      "type": "object",
      "additionalProperties": false,
      "description": "Ground reaction force and center of pressure, from a plate or mat. Force is reported both in newtons and as a multiple of body weight: body-weight ratio is the convention in golf biomechanics and does not require the platform to know the player's mass.",
      "properties": {
        "cop_lateral_m": {
          "type": "number",
          "description": "Center of pressure along the target line, positive toward the target, zero at address."
        },
        "cop_longitudinal_m": {
          "type": "number",
          "description": "Center of pressure perpendicular to the target line, positive toward the ball."
        },
        "cop_lead_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Share of the center of pressure carried on the lead foot."
        },
        "cop_trail_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Share of the center of pressure carried on the trail foot. Lead and trail together account for the whole, so a device reporting one can be checked against the other."
        },
        "vertical_force_n": {
          "type": "number",
          "description": "Peak vertical force through the swing."
        },
        "vertical_force_bw_ratio": {
          "type": "number",
          "minimum": 0,
          "description": "Peak vertical force as a multiple of the player's body weight. The comparable form -- newtons alone say more about the player's mass than about their swing."
        },
        "horizontal_force_n": {
          "type": "number",
          "description": "Peak horizontal force, the ground reaction that drives rotation."
        },
        "torque_nm": {
          "type": "number",
          "description": "Peak torque applied against the ground."
        },
        "stance_width_m": {
          "type": "number",
          "minimum": 0,
          "description": "Width of the player's stance, measured between the feet."
        },
        "at": {
          "type": "object",
          "description": "Pressure measurements at each named swing event.",
          "propertyNames": {
            "$ref": "common.schema.json#/$defs/swingEvent"
          },
          "additionalProperties": {
            "$ref": "#/$defs/pressureFrame"
          }
        },
        "series": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/seriesRef"
          },
          "description": "Pointer to the full pressure trace on the bulk path, where the scalars above are the summary."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "platform": {
      "type": "object",
      "additionalProperties": false,
      "description": "A hitting surface that moves or changes. Both directions: what the simulator asked for, and what the hardware actually achieved. The difference matters -- a platform that cannot reach the commanded angle has changed the shot, and the record should say so rather than implying the lie was flat.",
      "properties": {
        "pitch_commanded_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Positive is uphill toward the target."
        },
        "pitch_achieved_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Pitch the lie platform actually reached, as distinct from what it was asked for. Platforms have limits and are sometimes still moving when the player swings, so what was achieved is recorded rather than assumed."
        },
        "roll_commanded_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Positive means the surface rises to the player's right."
        },
        "roll_achieved_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 30,
          "description": "Roll the lie platform actually reached, as distinct from what it was asked for."
        },
        "surface_commanded": {
          "$ref": "common.schema.json#/$defs/lieSurface"
        },
        "surface_achieved": {
          "$ref": "common.schema.json#/$defs/lieSurface"
        },
        "settled": {
          "type": "boolean",
          "description": "The platform reached its commanded state before the stroke."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "trajectory": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "apex_height_m": {
          "type": "number",
          "description": "Peak height above the origin, not above the ground beneath it."
        },
        "net_elevation_m": {
          "type": "number",
          "description": "Where the ball finished relative to where it started. SHOULD agree with outcome.elevation_change_m."
        },
        "flight_time_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Time from launch to first landing."
        },
        "descent_angle_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Angle at which the ball came down where it first met the ground. Determines how much a shot will stop. Equals ball.landing_angle_deg only when the landing area is level with the launch point."
        },
        "events": {
          "type": "array",
          "minItems": 2,
          "items": {
            "$ref": "#/$defs/trajectoryEvent"
          },
          "description": "Minimum conformant sequence is launch -> landing -> rest. Any simulator that renders a ball flight already knows all three."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "The ball's path from launch to rest, as a sequence of phases and events. Where carry and total are the summary, this is what actually happened between them."
    },
    "outcome": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "end_lie_surface": {
          "$ref": "common.schema.json#/$defs/lieSurface"
        },
        "end_distance_to_pin_m": {
          "type": "number",
          "minimum": 0,
          "description": "Distance from the pin where the ball came to rest. Together with the origin distance, this is the whole of what a strokes-gained calculation needs."
        },
        "end_elevation_m": {
          "type": "number",
          "description": "Elevation of the ball's resting position."
        },
        "elevation_change_m": {
          "type": "number",
          "description": "Elevation of the resting position relative to the origin. Positive means the ball finished above where it started."
        },
        "end_position": {
          "$ref": "#/$defs/position"
        },
        "holed": {
          "type": "boolean",
          "description": "The ball went in. Ends the hole regardless of what any other field says."
        },
        "penalty_strokes": {
          "type": "integer",
          "minimum": 0,
          "maximum": 10,
          "description": "Penalty strokes added as a result of this stroke."
        },
        "penalty_reason": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 100,
          "description": "Why the penalty was assessed, coded so scoring and player rating can treat categories differently."
        },
        "replaced_position": {
          "$ref": "#/$defs/position",
          "description": "For a drop: where the ball was before it was moved. The origin block carries where it was played FROM after the drop."
        },
        "counted": {
          "type": "boolean",
          "description": "Whether this entered the score. Everything is recorded; only some things count."
        },
        "strokes_counted": {
          "type": "integer",
          "minimum": 0,
          "maximum": 20,
          "default": 1,
          "description": "How many strokes this record adds to the score. Normally 1. A conceded two-putt may be reported as one gimme record with strokes_counted 2 rather than two records; both are conformant. Reconciliation sums this over counted records."
        },
        "shot_kind": {
          "$ref": "common.schema.json#/$defs/shotKind"
        },
        "added_by": {
          "$ref": "common.schema.json#/$defs/strokeActor"
        },
        "rule_reason": {
          "$ref": "common.schema.json#/$defs/penaltyReason"
        },
        "note": {
          "type": "string",
          "maxLength": 300,
          "description": "Free text about the outcome, for a human reading the record. Never parsed, and never load-bearing."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "How the stroke ended, and whether it counted."
    },
    "downgraded": {
      "type": "object",
      "description": "Exceptional per-shot provenance downgrades. Provenance may only ever be downgraded at runtime, never upgraded.",
      "additionalProperties": {
        "type": "object",
        "required": [
          "provenance",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "provenance": {
            "$ref": "common.schema.json#/$defs/provenance"
          },
          "reason": {
            "$ref": "common.schema.json#/$defs/downgradeReason"
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "integrity": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "unmeasured_stroke": {
          "type": "boolean",
          "description": "No launch monitor measured this stroke -- it was entered, conceded, or reconstructed. The stroke still counts and still records; it simply is not evidence of how the ball was struck, and a player rating calculation must know the difference."
        },
        "device_unbound": {
          "type": "boolean",
          "description": "The contributing device was not bound to this bay when it sent the record. The data may be perfectly good; what is missing is the proof that it came from where it claims to have come from. Integrity is not authenticity, and this is the field that keeps the two apart."
        },
        "attribution_adopted_across_rotation": {
          "type": "boolean",
          "description": "The socket shot was queued against the previous player: the swing lands before any sample says the turn moved. Adopted, and flagged rather than hidden."
        },
        "window_closed": {
          "type": "string",
          "enum": [
            "complete",
            "deadline",
            "declined",
            "forced"
          ],
          "description": "How the correlation window ended. `complete` means every device that could still change a flight-critical answer had answered; the others say what was still outstanding when the engine stopped waiting. A record composed on a deadline is not wrong, but it is a different kind of evidence from one composed because everybody spoke."
        },
        "flight_complete": {
          "type": "boolean",
          "description": "Whether every flight-critical contribution was in when the record was composed. A fact about the COMPOSITION rather than about the ball, which is why it lives here: at the top level, beside the measurements, a reader takes it for a property of the shot."
        },
        "unmet_by": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/sgaId"
          },
          "description": "Devices that were expected to contribute and did not. Named rather than counted: 'the launch monitor went quiet' and 'the camera is slow' are different problems for whoever has to fix the bay."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "What the record itself can and cannot be trusted to prove. Present so a consumer never has to infer trustworthiness from the presence or absence of other fields."
    },
    "assertions": {
      "type": "object",
      "description": "For every STANDING field composed into this record: how old the assertion was when the shot happened, and who made it. A standing value is not wrong because it is old, but its age is evidence a consumer may need -- a club identified four seconds before the swing and one identified four minutes before are different claims.",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "object",
        "required": [
          "age_ms"
        ],
        "additionalProperties": false,
        "properties": {
          "age_ms": {
            "type": "integer",
            "minimum": 0,
            "description": "Time between the assertion and the stroke."
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "How sure the asserting device is that the value is correct, from 0 (no confidence) to 1 (certain)."
          },
          "source": {
            "$ref": "common.schema.json#/$defs/sgaId"
          },
          "renewed": {
            "type": "boolean",
            "description": "The device re-affirmed this value rather than it merely persisting unchallenged."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "vendor_raw": {
      "type": "object",
      "description": "The original vendor payload, verbatim and unmapped. Never discard the source record.",
      "additionalProperties": true
    },
    "sources": {
      "type": "object",
      "description": "Which device each value in this record came from, keyed by the record path the value was filed at. This is the standard's own provenance record (01), and it was carried as an x- extension -- which is the namespace for a VENDOR adding something the standard does not describe. Hiding it there told every consumer that the one thing making a record auditable was somebody else's private business.",
      "additionalProperties": {
        "$ref": "common.schema.json#/$defs/sgaId"
      }
    },
    "x": {
      "$ref": "common.schema.json#/$defs/vendorExtensions"
    },
    "origins": {
      "type": "object",
      "description": "For a value composed by a relaying phone (13 section 4.1), the originating device, keyed by the same record path as sources. sources names who asserted it; origins names which sensor it came from.",
      "additionalProperties": {
        "$ref": "common.schema.json#/$defs/deviceHandle"
      }
    }
  },
  "patternProperties": {
    "^x-[a-z0-9_-]+\\.": {}
  },
  "allOf": [
    {
      "$comment": "A stroke added without a swing carries no measurement blocks, but MUST still say where the ball ended up and who added it. Every stroke that changes a score is reportable.",
      "if": {
        "properties": {
          "outcome": {
            "properties": {
              "shot_kind": {
                "enum": [
                  "gimme",
                  "concession",
                  "drop",
                  "penalty"
                ]
              }
            },
            "required": [
              "shot_kind"
            ]
          }
        },
        "required": [
          "outcome"
        ]
      },
      "then": {
        "properties": {
          "ball": false,
          "club": false,
          "putt": false,
          "swing": false,
          "pressure": false,
          "trajectory": false,
          "outcome": {
            "required": [
              "shot_kind",
              "added_by",
              "end_distance_to_pin_m"
            ]
          }
        }
      }
    }
  ],
  "$defs": {
    "clubSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "What club this physically is. Never required, and absent from most rounds. It is the field that turns shot data into equipment data: 'a 34-degree 7-iron with this shaft produces this spin across fifty thousand shots' is the saleable insight, and it is unobtainable without knowing the club.",
      "properties": {
        "manufacturer": {
          "type": "string",
          "maxLength": 64,
          "description": "Club manufacturer."
        },
        "model": {
          "type": "string",
          "maxLength": 96,
          "description": "Club model."
        },
        "model_year": {
          "type": "integer",
          "minimum": 1900,
          "maximum": 2200,
          "description": "Model year, since manufacturers reuse model names across substantially different clubs."
        },
        "club_type": {
          "type": "string",
          "maxLength": 24,
          "description": "driver, 3w, 5h, 7i, pw, 56w, putter."
        },
        "loft_deg": {
          "type": "number",
          "minimum": -5,
          "maximum": 90,
          "description": "Static loft as specified, which is not the loft delivered at impact -- see club.dynamic_loft_deg for that."
        },
        "lie_deg": {
          "type": "number",
          "minimum": 30,
          "maximum": 90,
          "description": "Static lie angle as specified."
        },
        "length_m": {
          "type": "number",
          "minimum": 0.5,
          "maximum": 1.6,
          "description": "Club length as specified."
        },
        "bounce_deg": {
          "type": "number",
          "minimum": -10,
          "maximum": 30,
          "description": "Sole bounce angle. Governs how the club interacts with turf and sand."
        },
        "head_weight_g": {
          "type": "number",
          "minimum": 100,
          "maximum": 400,
          "description": "Head weight."
        },
        "swing_weight": {
          "type": "string",
          "maxLength": 8,
          "description": "Conventional notation such as D2. A scale, not a measurement, so it is carried verbatim."
        },
        "shaft_manufacturer": {
          "type": "string",
          "maxLength": 64,
          "description": "Shaft manufacturer."
        },
        "shaft_model": {
          "type": "string",
          "maxLength": 96,
          "description": "Shaft model."
        },
        "shaft_material": {
          "enum": [
            "steel",
            "graphite",
            "multi_material",
            "other",
            "unknown"
          ],
          "description": "What the shaft is made of. Coarser than weight and torque and worth carrying anyway: it is the one shaft attribute a player reliably knows about their own clubs, and it separates two populations that behave differently at the same flex label."
        },
        "shaft_flex": {
          "type": "string",
          "maxLength": 16,
          "description": "Shaft flex as the manufacturer labels it. Labels are not comparable between manufacturers, which is why the measured properties below are carried alongside it."
        },
        "shaft_weight_g": {
          "type": "number",
          "minimum": 20,
          "maximum": 200,
          "description": "Shaft weight."
        },
        "shaft_torque_deg": {
          "type": "number",
          "minimum": 0,
          "maximum": 10,
          "description": "Shaft torsional stiffness, as degrees of twist under the manufacturer's test load."
        },
        "shaft_kick_point": {
          "enum": [
            "low",
            "mid",
            "high",
            "unknown"
          ],
          "description": "Where the shaft bends most. Influences launch angle and spin."
        },
        "grip_manufacturer": {
          "type": "string",
          "maxLength": 64,
          "description": "Grip manufacturer."
        },
        "grip_model": {
          "type": "string",
          "maxLength": 96,
          "description": "Grip model."
        },
        "custom_fitted": {
          "type": "boolean",
          "description": "The club was fitted to this player, so its specification may differ from the catalog club of the same name."
        },
        "source": {
          "$ref": "common.schema.json#/$defs/specSource"
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "ballSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "Which ball was struck. At least as valuable to a manufacturer as the club, and currently reported by nothing in the industry -- no surveyed launch monitor or simulator captures it. It has to come from the member's registered bag, a fitting record, or someone typing it in.",
      "properties": {
        "manufacturer": {
          "type": "string",
          "maxLength": 64,
          "description": "Ball manufacturer."
        },
        "model": {
          "type": "string",
          "maxLength": 96,
          "description": "Ball model."
        },
        "model_year": {
          "type": "integer",
          "minimum": 1900,
          "maximum": 2200,
          "description": "Model year. Manufacturers revise balls substantially while keeping the name."
        },
        "color": {
          "enum": [
            "white",
            "yellow",
            "orange",
            "pink",
            "green",
            "red",
            "other",
            "unknown"
          ],
          "description": "Ball color. Not cosmetic detail: an optical launch monitor sees a yellow ball differently from a white one, so a spin or speed difference that looks like the ball's construction may be the camera's."
        },
        "number": {
          "type": "string",
          "maxLength": 3,
          "description": "The number printed on the ball, verbatim -- \"1\", \"00\", or a personalized marking. A string because it is a marking rather than a quantity, and because leading zeros are part of it. With manufacturer, model and color it is how a player tells their ball from an identical one in the same group."
        },
        "compression": {
          "type": "number",
          "minimum": 0,
          "maximum": 150,
          "description": "Compression rating as the manufacturer publishes it."
        },
        "piece_count": {
          "type": "integer",
          "minimum": 1,
          "maximum": 6,
          "description": "Number of construction layers."
        },
        "cover": {
          "enum": [
            "urethane",
            "surlyn",
            "ionomer",
            "other",
            "unknown"
          ],
          "description": "Cover material. Bears directly on spin, particularly around the green."
        },
        "condition": {
          "enum": [
            "new",
            "used",
            "range",
            "unknown"
          ],
          "description": "Condition of the ball. A scuffed or waterlogged ball does not fly like a new one, and a range ball is a different ball entirely."
        },
        "source": {
          "$ref": "common.schema.json#/$defs/specSource"
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "pressureFrame": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "cop_lateral_m": {
          "type": "number",
          "description": "Center of pressure across the stance. Positive is toward the player's right, regardless of handedness."
        },
        "cop_longitudinal_m": {
          "type": "number",
          "description": "Center of pressure from heels to toes. Positive is toward the toes."
        },
        "cop_lead_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Share of pressure on the lead foot at this instant."
        },
        "cop_trail_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Share of pressure on the trail foot at this instant."
        },
        "vertical_force_n": {
          "type": "number",
          "description": "Vertical ground reaction force at this instant."
        },
        "vertical_force_bw_ratio": {
          "type": "number",
          "minimum": 0,
          "description": "Vertical force at this instant as a multiple of body weight."
        },
        "horizontal_force_n": {
          "type": "number",
          "description": "Horizontal ground reaction force at this instant."
        },
        "torque_nm": {
          "type": "number",
          "description": "Torque applied against the ground at this instant."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "One sample of the ground-reaction trace: where the player's weight was and what forces they were applying at a single instant of the swing."
    },
    "postureFrame": {
      "type": "object",
      "additionalProperties": false,
      "description": "Body posture at one instant. Every angle positive TOWARD the target (open) unless noted; lead/trail rather than left/right.",
      "properties": {
        "pelvis_turn_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "Pelvis rotation about the vertical axis. Positive is turning away from the target."
        },
        "pelvis_sway_m": {
          "type": "number",
          "description": "Horizontal translation, positive toward the target, zero at address."
        },
        "pelvis_lift_m": {
          "type": "number",
          "description": "Vertical translation, positive up, zero at address."
        },
        "pelvis_side_bend_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Pelvis lateral tilt. Positive is the right side high, regardless of handedness."
        },
        "thorax_turn_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "Upper-body rotation about the vertical axis. The difference between this and the pelvis turn is what coaches call the X-factor."
        },
        "thorax_bend_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Forward flexion; positive is bent toward the ball."
        },
        "thorax_side_bend_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Upper-body lateral tilt. Positive is the right side high."
        },
        "thorax_sway_m": {
          "type": "number",
          "description": "Lateral movement of the upper body away from its address position. Positive is toward the player's right."
        },
        "thorax_lift_m": {
          "type": "number",
          "description": "Vertical movement of the upper body relative to address. Positive is up, away from the ball."
        },
        "x_factor_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "Thorax turn minus pelvis turn."
        },
        "lead_wrist_flexion_deg": {
          "type": "number",
          "minimum": -120,
          "maximum": 120,
          "description": "Positive is flexion (bowed); negative is extension (cupped)."
        },
        "lead_wrist_deviation_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Positive is radial deviation (hinged up)."
        },
        "lead_wrist_rotation_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "Positive is supination."
        },
        "trail_wrist_flexion_deg": {
          "type": "number",
          "minimum": -120,
          "maximum": 120,
          "description": "Trail wrist flexion. Positive is flexed, or bowed; negative is extended, or cupped. Strongly tied to the loft delivered at impact."
        },
        "trail_wrist_deviation_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Trail wrist deviation toward thumb or little finger -- the hinge that sets the club on the backswing."
        },
        "trail_wrist_rotation_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "Trail wrist rotation. Positive is supination."
        },
        "lead_arm_rotation_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "Lead arm rotation about its own axis. A principal contributor to face rotation through impact."
        },
        "lead_elbow_flexion_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 180,
          "description": "Lead elbow bend. Zero is straight."
        },
        "trail_elbow_flexion_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 180,
          "description": "Trail elbow bend. Zero is straight."
        },
        "lead_knee_flexion_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 180,
          "description": "Lead knee bend. Zero is straight."
        },
        "trail_knee_flexion_deg": {
          "type": "number",
          "minimum": -30,
          "maximum": 180,
          "description": "Trail knee bend. Zero is straight."
        },
        "head_sway_m": {
          "type": "number",
          "description": "Lateral movement of the head from its address position. Positive is toward the player's right."
        },
        "head_lift_m": {
          "type": "number",
          "description": "Vertical movement of the head relative to address. Positive is up."
        },
        "hand_speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Speed of the hands at this instant."
        },
        "shaft_lean_deg": {
          "type": "number",
          "minimum": -90,
          "maximum": 90,
          "description": "Positive is forward lean, toward the target."
        },
        "shaft_deflection_mm": {
          "type": "number",
          "description": "Shaft bend; positive is toe-down droop."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "position": {
      "type": "object",
      "required": [
        "x_m",
        "y_m"
      ],
      "additionalProperties": false,
      "properties": {
        "x_m": {
          "type": "number",
          "description": "Position across the target line. Positive is right of it."
        },
        "y_m": {
          "type": "number",
          "description": "Position along the target line. Positive is toward the target."
        },
        "z_m": {
          "type": "number",
          "description": "Height above the reference surface."
        },
        "uncertainty_m": {
          "type": "number",
          "minimum": 0,
          "description": "Radius in meters within which the ball is expected to actually be. REQUIRED in practice whenever the position's provenance is inferred: a point with no error bound reads as certainty, and a confident wrong answer sends a player to the wrong trees. Omit it only for a position that was measured, where the sensor's own resolution already bounds it."
        },
        "world": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "lat_deg",
            "lon_deg"
          ],
          "description": "The same point in real-world coordinates, where one is known. OPTIONAL and additive: the local x_m/y_m frame remains authoritative for everything the standard computes, and this is carried so a shot can be placed on a real course, overlaid against outdoor play, or compared with survey data. Outdoors this is the measured value and the local frame is projected from it; indoors the reverse. Provenance says which.",
          "properties": {
            "lat_deg": {
              "type": "number",
              "minimum": -90,
              "maximum": 90,
              "description": "Latitude in decimal degrees. Seven decimal places is about 1 cm; store in a 64-bit float. A 32-bit float holds only about seven significant digits and silently costs a meter or more."
            },
            "lon_deg": {
              "type": "number",
              "minimum": -180,
              "maximum": 180,
              "description": "Longitude in decimal degrees. Same precision warning as lat_deg."
            },
            "height_m": {
              "type": "number",
              "description": "Height in meters. MEANINGLESS without height_ref: the two conventions differ by -20 to -35 m across the continental US, which is more than most holes are long."
            },
            "height_ref": {
              "enum": [
                "ellipsoidal",
                "orthometric"
              ],
              "description": "ellipsoidal is what GNSS reports natively. orthometric is height above the geoid, which is what everyone means by elevation. Mixing them is the single most damaging error in this object."
            },
            "geoid_model": {
              "type": "string",
              "maxLength": 32,
              "description": "Which geoid model converted ellipsoidal to orthometric, for example EGM2008 or GEOID18. REQUIRED in practice when height_ref is orthometric, because the models disagree by decimeters."
            },
            "datum": {
              "type": "string",
              "maxLength": 32,
              "description": "Reference frame of these coordinates, for example WGS84 or NAD83(2011). Omit to inherit the course georeference. WGS84 and NAD83 differ by 1-2 m in the continental US, so an assumed datum is a systematic error, not noise."
            },
            "epoch": {
              "type": "string",
              "maxLength": 16,
              "description": "Epoch of the reference frame, for example 2010.0. North America moves 2-7 cm a year relative to NAD83, so a coordinate with no epoch slowly stops being true."
            },
            "h_accuracy_m": {
              "type": "number",
              "minimum": 0,
              "description": "Horizontal accuracy, meters, at the source's stated confidence. Roughly 3-5 m for a phone, 1-2 m with SBAS, 1-2 cm for RTK-fixed."
            },
            "v_accuracy_m": {
              "type": "number",
              "minimum": 0,
              "description": "Vertical accuracy, meters. Reported separately because it is typically 1.5 to 3 times worse than horizontal, and a single combined figure hides exactly the axis that ruins an elevation calculation."
            },
            "fix_type": {
              "enum": [
                "autonomous",
                "dgps",
                "sbas",
                "rtk_float",
                "rtk_fixed",
                "estimated",
                "unknown"
              ],
              "description": "How the fix was obtained. estimated means no fix at all -- the position came from a model. Same field, very different trust."
            }
          },
          "patternProperties": {
            "^x-[a-z0-9_-]+\\.": {}
          }
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "A point in the bay's coordinate frame, used for the ball's location on the course rather than for anything about the player."
    },
    "trajectoryEvent": {
      "type": "object",
      "required": [
        "at_m",
        "phase"
      ],
      "additionalProperties": false,
      "properties": {
        "at_m": {
          "type": "number",
          "minimum": 0,
          "description": "Cumulative HORIZONTAL distance from the origin. Monotonic across the sequence."
        },
        "height_m": {
          "type": "number",
          "description": "Vertical displacement from the origin, positive up. With at_m this gives the shot's actual vertical profile — the difference between a shot that carried a ridge and one that ran along a valley floor."
        },
        "t_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Milliseconds since launch."
        },
        "phase": {
          "$ref": "common.schema.json#/$defs/trajectoryPhase"
        },
        "surface": {
          "$ref": "common.schema.json#/$defs/eventSurface"
        },
        "object": {
          "type": "string",
          "maxLength": 50,
          "description": "The specific thing the ball met, free text: tree, branch, cart_path, wall, grandstand, rake, water_surface. Its coded counterpart is `surface` -- object names the thing, surface codes what it was made of. A consumer aggregating across vendors uses surface; object is for a human reading one shot."
        },
        "deflection_deg": {
          "type": "number",
          "minimum": -180,
          "maximum": 180,
          "description": "How far the ball's direction changed at this event. Large values are what a tree does to a good drive."
        },
        "speed_loss_ratio": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Fraction of speed lost in this interaction."
        },
        "speed_mps": {
          "type": "number",
          "minimum": 0,
          "description": "Ball speed at this event."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "One thing that happened to the ball in flight or on the ground -- a landing, a bounce, a collision with a tree or a cart path. What turns a pair of distances into an account of the shot."
    }
  }
}
