The shot record
Normative. Units and sign conventions per 00-conventions. Provenance per 01-provenance — every numeric field below resolves its provenance through the session manifest, not inline.
A shot is not a scalar outcome. It is a transition between two states, with a path between them. Recording only the endpoints throws away most of what the simulator knows, and most of what makes a simulator player rating better than a paper one.
1. Shape#
{
"shot_id": "sga:shot:01J9X4...",
"session_id": "sga:session:01J9X4...",
"manifest": "sha256:7a3f...",
"bundle_version": "2026.09.1",
"slot": 1,
"t_ms": 842301,
"struck_at": "2026-09-05T14:22:31.482-05:00",
"hole": 7,
"stroke_index": 2,
"course": { },
"origin": { },
"environment": { },
"club": { },
"ball": { },
"trajectory": { },
"outcome": { }
}Every block is optional. A bay with a socket-only connector and no launch monitor emits origin, outcome and a partial course, and that is a conformant, useful, ratable shot record. Absent is honest. See 01 §1.
2. course — identity and version#
Tournament integrity depends on this block more than on anything else in the record. Two players on a course of the same name may be playing measurably different golf courses: community course files are versioned, forked, and revised without notice, and the differences move scores.
"course": {
"course_id": "sga:course:9f2c...",
"name": "Example Links",
"source": "vendor.community",
"vendor_ref": "SGT-1842",
"version": "1.4.2",
"digest": "sha256:c41d...",
"tee_set": "championship",
"hole_par": 4,
"hole_length_m": 396.2,
"digest_verified": true
}digestis the identity that matters. Name and version are labels; the digest is the course. A sanctioned event pins a digest, and a bay whose course file does not hash to it is not playing that event.digest_verifiedrecords whether the engine actually computed the hash or is relaying the simulator's claim. The difference matters and is not hidden.- Course files a vendor treats as opaque may not permit hashing. Then
digestis absent anddigest_verifiedisfalse, the round is recorded, and the competition rules decide whether it can be sanctioned. Same pattern as everywhere else: record, grade, let the rulebook decide.
2.1 On distributing courses#
Verifying a course and shipping a course are very different propositions.
Simulator courses are third-party intellectual property — community-authored, vendor-licensed, or both. SGA redistributing them would put us in the content distribution business, with the licensing exposure that implies, and it contradicts the position that makes partnership conversations easy: we do not build simulators, hardware, or courses.
So the standard specifies verification, and the plumbing for distribution that someone else performs:
- SGA publishes the required
digestfor an event. - The engine checks the installed file, and reports mismatch or absence before the round starts rather than after it is scored.
- On mismatch, the engine can hand the simulator a fetch instruction — the vendor's or the community library's own URL for that exact version. The bytes come from the rights holder. SGA supplies the pointer and the hash.
That gets you equal footing for tournaments without SGA becoming a distributor. If a vendor later grants redistribution rights, it drops into the same slot as an implementation detail.
3. origin — the state the shot started from#
This block, with outcome, is what the player rating is actually computed from.
"origin": {
"lie_surface": "rough",
"lie_slope_along_deg": -3.2,
"lie_slope_across_deg": 1.8,
"distance_to_pin_m": 142.6,
"elevation_change_m": 4.1,
"origin_elevation_m": 88.3,
"position": { "x_m": 1204.3, "y_m": -88.1 },
"stance_obstructed": false,
"recommended_club": "7i"
}lie_surfacefrom the registered vocabulary:tee,fairway,first_cut,rough,deep_rough,fescue,sand_bunker,sand_waste,hardpan,pine_straw,divot,cart_path,rock,water,penalty_area,fringe,green,unknown. Vendor-specific surfaces extend the vocabulary under a namespace and map to the nearest core value.lie_slope_along_deg— positive is uphill toward the target.lie_slope_across_deg— positive means the ground rises to the player's right, regardless of handedness. This is the two-axis lie angle a leveling platform both consumes and can report.elevation_change_mis the target relative to the ball — positive means the pin is above you. This is not decoration: a 100 m shot playing 15 m downhill and a 100 m shot playing 15 m uphill are different shots requiring different clubs, and a rating model that treats them as the same state is measurably wrong. Elevation is part of the state, not a modifier of it.origin_elevation_mis the absolute elevation of the lie in course coordinates, so origin and outcome elevations are directly comparable.positionis course-local coordinates where the simulator exposes them. Optional, and the most likely field for a vendor to withhold — the record works without it, sincedistance_to_pin_mpluslie_surfacecarries almost all the rating signal.
4. environment#
"environment": {
"wind_speed_mps": 4.2,
"wind_direction_deg": 215,
"wind_gust_mps": 6.1,
"temperature_c": 18.5,
"pressure_hpa": 1013.2,
"humidity_pct": 55,
"altitude_m": 512,
"green_speed_stimp": 10.5,
"firmness": "medium",
"conditions_source": "simulator_configured"
}wind_direction_deg is the bearing the wind blows toward, 0 = north — not the meteorological convention of the direction it comes from. Stated because this is the second-most-likely place after spin_axis_deg for two correct implementations to disagree by 180 degrees.
conditions_source distinguishes simulator_configured (a setting, and therefore a tournament-rules matter) from measured (a real sensor in an outdoor or hybrid setting). A wind value that is a game setting and a wind value that is the weather are not the same fact.
4.1 ambient — the room, not the game#
environment is the air the shot was played in. ambient is what was physically measured where the player is standing. Indoors those are different things and both are true at once: the simulator is modeling a seaside links in a light breeze while the player stands in a six-degree garage. One block with a source flag could not express that, so there are two blocks.
Indoors, only one field in ambient earns its place, and it earns it well:
Temperature. A golf ball at 5 °C leaves the face measurably slower than the same ball at 20 °C — enough to matter, and it compounds over a winter. 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 the difference to them. Worse, it is seasonal and systematic, so it does not average out.
That also makes it a competition-fairness variable rather than a data-quality one. A player in a heated facility and a player in a cold garage are not competing under equal conditions, and an event can only require a temperature band if something measures it. That is a certification requirement waiting to be written, and it is the strongest argument for the block.
Outdoors the two blocks converge and the same values may legitimately appear in both.
Wind indoors should be absent, not zero. Zero is a measurement; absent is the truth.
5. club and ball#
Standard launch-monitor telemetry. Every field optional, provenance declared in the manifest.
"club": {
"club_id": "7i",
"speed_mps": 38.1,
"path_deg": 2.4,
"face_angle_deg": -1.1,
"face_to_path_deg": -3.5,
"angle_of_attack_deg": -4.2,
"dynamic_loft_deg": 28.6,
"strike_horizontal_mm": -3.0,
"strike_vertical_mm": 2.5,
"vendor_club_code": 7
}"ball": {
"speed_mps": 53.6,
"launch_angle_deg": 19.2,
"launch_direction_deg": -1.8,
"spin_rpm": 6420,
"spin_axis_deg": -4.1,
"smash_ratio": 1.407,
"carry_m": 131.4,
"total_m": 138.9
}vendor_club_code preserves the vendor's own opaque identifier verbatim. Some simulators emit an uninterpreted integer club id; it passes through unmapped rather than being guessed at, and can be decoded later without recapturing anything.
5.0 What the club and ball physically are#
club.spec and ball.spec carry the equipment itself — manufacturer, model, loft, lie, length, shaft and grip for a club; manufacturer, model, compression, construction and condition for a ball.
Never required, absent from most rounds, and the single field that turns shot data into equipment data. "A 34-degree 7-iron with this shaft produces this spin profile across fifty thousand shots" is the insight a manufacturer would pay for, and it is unobtainable without knowing which club was swung. Ball identity is at least as valuable and is currently reported by nothing in the industry — no surveyed launch monitor or simulator captures it.
When two clubs are out at once, the agreed attributes still stand. Two Callaway 7-irons, same shaft, different model and grip: brand, type and shaft are true whichever one is being swung, and only the model and the grip are in doubt. The engine composes the attributes every candidate agrees on, marks the result partial, and lists what it omitted.
That is more useful than nothing and still entirely honest — nothing reported is in question. A consumer wanting a complete specification checks partial and skips it; a shaft-performance analysis uses it happily.
A scalar has no such half. Two different club_id values compose to nothing, because there is no part of "7i" that is also "5i".
source says where the specification came from, for the same reason identification.method exists: a corpus is only worth money if you can filter to rows where the equipment was actually known.
| source | resolved |
|---|---|
member_bag | in the cloud, on claim, from the member's registered bag |
sim_bag_setup | at the bay — most simulators already have a "my bag" with lofts and lengths |
rfid_tag | at the bay, from a tag on the club |
rack_slot | at the bay, from a rack whose slots were labeled once |
fitting_record | from a club-fitting session |
operator_entered | typed by staff |
Two of those are worth noticing. Simulators and fitting software already collect most of this today and simply have nowhere to send it — a bag setup with lofts and lengths is a standard feature, and it currently dies inside the application. And a club rack whose slots were labeled once supplies both the identification and the specification from the same thirty-dollar device.
The privacy position is straightforward for the specification itself: equipment is not personal data. A member's bag is linked to them, but a shot record saying "34-degree 7-iron, X-flex shaft" says nothing about who swung it. Club and ball specifications therefore survive de-identification and stay in the corpus.
A tag id does not, and the distinction is the whole of it. A specification describes a category of object; a tag identifies one object, and one object usually has one owner. The same tag appearing at four venues links those rounds to each other, and to a person, with every name already removed. So tag_id is equipment data at the bay and an identifier everywhere after it: it is kept (recording is total), it may be resolved inside SGA, and it MUST NOT leave in a data set shared beyond SGA -- to a manufacturer, a researcher or a partner -- unless it is removed or replaced with a value scoped to that data set. What the corpus carries outward is what the club was, never which club it was.
5.0a The tag identifies the club; the specification can arrive later#
Every device in the bay contributes whatever it can observe -- that is what the matrix is for, and a device that knows a club's full specification should publish it. But a specification is often known nowhere near the bay. The player told their app what is in the bag, or a fitter recorded it, or the simulator holds a bag setup. What the bay can see is which club came out, and, where something on the club says so, which club that was.
Two fields carry that:
| field | carries |
|---|---|
club.club_id | which club was swung -- 8i |
club.tag_id | which physical club it was -- the tag's own identifier |
tag_id is opaque. It is whatever the reader read, verbatim: an RFID or NFC serial, or any other marking unique to the one club. An engine MUST NOT parse it, MUST NOT infer the club type from it, and MUST NOT require any particular format -- tag schemes belong to whoever made the tags.
A tag id is enough on its own. A device that can read a tag but knows nothing about the club publishes the id and stops there. The specification is resolved from it later: in the player's app, where they recorded what that club is; in the cloud, against a member's registered bag; or by any consumer holding its own mapping. That resolution is ancillary to the record rather than part of it -- the record says what the bay saw, and what the club turned out to be is an association made afterwards, by somebody who can be asked how they knew.
A device that does know the specification sends it. A fitting system, a simulator with a configured bag or an app on the player's phone joins as sensor.equipment and provides club.spec (5.0), and club.spec.source says which route it came by. Neither path is preferred, and both may happen in the same round for different clubs.
A tag mapping MUST be resolved as of the shot. Tags move: a club is reshafted, a label is peeled off a wedge and stuck on its replacement, a set is sold. A consumer that resolves a two-year-old shot against today's bag will attribute it to whatever that tag means now, silently and wrongly. The mapping is therefore time-versioned, and the timestamp that governs is the shot's, not the lookup's.
The same applies to ball.tag_id, which nothing on the market reports yet. The field exists so that a bay able to read a ball has somewhere to put what it read, rather than dropping the one identifier the industry has never had.
5.1 How the club was identified#
club.identification records how the club was known, on a separate axis from field provenance — because the club is a categorical identity claim, not a measured quantity.
| method | evidence? | typical |
|---|---|---|
imaged | yes | camera array recognizing the head |
marker | yes | sticker, dot or label read at impact |
rfid_nfc | yes | the club announces itself |
holder_absence | yes | inferred from what is missing from a known set — a slotted rack, or a bag that can see its own clubs. Knows what was drawn, not what was swung |
inferred_from_impact | partial | derived from loft, speed and impact — a guess with physics behind it |
ui_selected | no | a human told the simulator |
bag_sequence | no | assumed from order of play |
unknown | no | nothing said |
This is never required and never affects rating. The ball goes where the launch data says regardless of what was swung, and the strokes-gained transition that drives the player rating does not care about club at all. A shot record with no club identification is complete, conformant and fully ratable.
It gates exactly one thing: whether per-club statistics can honestly be shown to a player.
That constraint is worth stating plainly because it is a product decision dressed as a schema field. "Your 7-iron carries 148 m on average" is only true if something knew it was a 7-iron. Where the club came from a UI dropdown, the number is an average over whatever the player said they hit — and a player who selects a 7-iron and swings an 8-iron produces a shot record that is correct in every respect except the one used to group it. Those errors do not cancel out; golfers mis-select in a biased direction, usually toward the club they meant to hit.
So the platform can offer per-club analytics on bays with a detecting monitor, must decline to offer them elsewhere, and needs this field to tell the difference. Without it we would either show numbers we cannot stand behind or withhold a feature from the bays that have earned it.
The club need not be identified by the launch monitor. identification.source names whichever participant asserted it — an RFID reader in the playfield, a separate camera, a bag sensor, or the simulator's UI. The engine composes the record from all of them; see 03 §2.2b.
A club identification is standing state, not shot-synchronous — a rack or a bag sensor knows the club long before the swing. Such a device MUST be able to withdraw its assertion when it can no longer support it (two slots empty, and it cannot tell which club is in play), and the composed record carries assertions["club.club_id"].age_ms so a consumer can see how long before the stroke the club was last known. See 03 §2.2a.
5.2 Club identification is per shot, and it is the exception#
Field provenance is declared once at handshake and referenced thereafter, because a radar that measures ball speed measures it on every shot. Club identification does not work that way and is the one place the standard puts provenance inline on every record.
Whether the club is identified depends on conditions that change stroke to stroke: a sticker that fell off, a head that was out of frame, a bad angle, a player standing between the camera and the ball. A device can be entirely capable of imaged identification and still fail on the third shot of the hole.
So:
- The capability descriptor declares
id_methods— what this device can do, best first. That is a capability, not a promise. - Each shot record carries
club.identificationwith the method that actually worked for that stroke, and its confidence. - When nothing identified the club, omit
club_identirely. Absent is the honest answer and it costs nothing — the shot is still complete, still ratable, still counts.
The rule that matters, and the one worth reading twice:
A device MUST NOT report a declared method it did not actually use on this stroke. If
imagedfailed and the value came from the simulator's UI selection, the method isui_selected. Inheriting the declared method because it is usually true is how a per-club statistic quietly becomes an average over what players said they hit.
This is the same shape as the per-shot downgrade in 01 §2.1 — only ever downwards, never upwards, always stated. Conformance rules R046 and R047 check it, and R048 rejects a shot claiming a method the device never declared it could perform.
disagreement records the case where two sources named different clubs — a UI selection contradicting a detection. It is recorded, not resolved. A recurring disagreement is the clearest available signal that a given player's per-club data should not be trusted, and it costs nothing to capture.
There is a pleasant secondary effect: a monitor that detects clubs has a concrete, demonstrable feature that one which does not cannot offer, and the platform surfaces that difference to the owner. That is a reason for a manufacturer to want this field to exist.
6. trajectory — the path, as events#
The block with no equivalent in any existing simulator protocol, and the one that makes "was that a good shot" answerable.
"trajectory": {
"apex_height_m": 24.1,
"net_elevation_m": -1.5,
"flight_time_ms": 5600,
"descent_angle_deg": 41.3,
"events": [
{ "t_ms": 0, "at_m": 0.0, "height_m": 0.0, "phase": "launch",
"speed_mps": 61.4 },
{ "t_ms": 2400, "at_m": 62.0, "height_m": 24.1, "phase": "apex" },
{ "t_ms": 3900, "at_m": 96.2, "height_m": 11.4, "phase": "interaction",
"object": "tree", "surface": "foliage",
"deflection_deg": 22.0, "speed_loss_ratio": 0.41 },
{ "t_ms": 5600, "at_m": 119.6, "height_m": -1.2, "phase": "landing",
"surface": "rough", "speed_mps": 18.7 },
{ "t_ms": 6100, "at_m": 124.8, "height_m": -1.0, "phase": "bounce",
"surface": "rough" },
{ "t_ms": 7300, "at_m": 131.4, "height_m": -1.3, "phase": "roll",
"surface": "fringe" },
{ "t_ms": 8100, "at_m": 134.0, "height_m": -1.4, "phase": "roll",
"surface": "green" },
{ "t_ms": 9400, "at_m": 135.7, "height_m": -1.5, "phase": "rest",
"surface": "green" }
]
}surfaceon an event iseventSurface, notlieSurface. A ball comes to rest on a lie; in flight it hits things no ball can ever lie on. The example above deflects offfoliage, and foliage is not a lie — it is not somewhere you can play your next shot from.eventSurfaceis a superset: every lie surface is in it, so alanding,bounceorrollevent uses exactly the term it would use anywhere else in this record, and the additions are only ever things struck —foliage,tree_trunk,netting,wall,fence,post,structure,flagstick,cup,equipment,water_surface,player.surfaceandobjectare a pair and both are worth sending:objectnames the thing,surfacecodes what it was made of. "A tree" is an object; "foliage" is what the ball actually touched, and a ball through the canopy behaves nothing like one off the trunk. Aggregate onsurface;objectis for a human reading one shot.at_mis cumulative horizontal distance from the origin, so the sequence is monotonic and directly readable. All_mdistances in this record are ground distances unless the name says otherwise.height_mis vertical displacement from the origin, positive up. Together withat_mthe events describe the shot's actual vertical profile, not just its length — which is the difference between a shot that carried a ridge and one that ran along a valley floor.net_elevation_mis where the ball finished relative to where it started. It should agree withoutcome.elevation_change_m; a disagreement is a device or model inconsistency worth surfacing.apex_height_mis peak height above the origin, not above the ground beneath it. Absolute apex over undulating terrain is recoverable from the course model and is not the launch monitor's business to assert.phasefrom the vocabulary:launch,flight,apex,interaction,landing,bounce,roll,rest,penalty.t_msis milliseconds sincelaunch, andat_malone is not a substitute. Two shots can trace the same ground path and be completely different golf: a low runner that reached the green in four seconds and a towering wedge that took nine. Without the clock nothing downstream can tell them apart, and a shot's character is most of what makes it worth looking at later.speed_mpson an event is the ball's speed at that moment. Worth sending atlandingin particular: the difference between a ball that lands soft and one that arrives hot is the whole reason it ended up where it did.interactioncovers anything the ball met that changed its path — tree, branch, cart path, grandstand, rake, wall, water surface. That it happened, and what was struck, is the point.deflection_degandspeed_loss_ratioare optional: send them where the model knows them, and omit them where it does not. A recorded interaction with neither is still worth far more than a silent one, because the interesting fact is that the ball hit a tree.- A minimal conformant trajectory is
launch→landing→rest. Three events. Any simulator that renders a ball flight already knows all three. - Events MAY be sent incrementally, as the shot happens. They are enrichment (10 §10.1), so a simulator that wants to narrate a flight in progress sends each event as it computes it, carrying the
shot_id, and a consumer that wants live narration subscribes like it would to anything else. Nothing about the finished record changes: a consumer that only cares about the completed series ignores the intermediates, and the engine journals all of it either way.
7. outcome#
"outcome": {
"end_lie_surface": "green",
"end_distance_to_pin_m": 2.0,
"end_elevation_m": 73.1,
"elevation_change_m": -15.2,
"end_position": { "x_m": 1341.8, "y_m": -12.4 },
"holed": false,
"penalty_strokes": 0,
"penalty_reason": null,
"counted": true,
"shot_kind": "stroke"
}7.1 Every stroke emits a record, including the ones with no swing#
shot_kind is one of stroke, whiff, provisional, mulligan, practice, gimme, concession, drop, penalty.
Any event that changes a player's score produces a shot record, whether or not anybody swung a club. A drop, a conceded putt, a penalty stroke, a hole conceded in match play — each emits a record.
A record for a non-swing stroke carries no ball, club or trajectory block, because there was nothing to measure. It MUST carry:
| field | why |
|---|---|
shot_kind | what happened |
added_by | player, simulator_auto, operator or official. A human adjusting a score is exactly what an audit trail exists to capture. |
outcome.end_distance_to_pin_m | otherwise the next shot's origin is unexplained, and a rating model cannot score a transition whose start state it does not know |
rule_reason (penalties) | water, out of bounds and unplayable are different events, and only some are worth reviewing later |
replaced_position (drops) | where the ball was before it moved |
strokes_counted defaults to 1 and may be higher, so a conceded two-putt can be one gimme record with strokes_counted: 2 rather than two records. Both forms are conformant.
Mulligans and practice shots emit records with counted: false. They are excluded from rating and are excellent coaching and equipment data, which is precisely why they are recorded rather than dropped.
A stroke with no swing is not correlated#
§10 exists to assemble one shot from several devices that each saw part of it and finish at different times. A gimme, a drop or a conceded hole has none of that: there is no launch condition, no device still working, and nothing that could arrive later and improve the answer. Everything about it is known at the moment it is reported.
So it does not go through the window:
- The engine MUST compose and emit a non-swing stroke immediately on receipt, rather than opening or joining a correlation window.
- It MUST NOT open a shot context. A non-swing stroke is not evidence of a swing, and treating it as the first sighting of one leaves a window open waiting for a ball that was never hit.
- The reporting device MUST NOT allocate a
shot_id. Shot identity is the engine's, exactly as it is for a swing, and a device that invents one is describing a shot the engine never issued — which an engine correctly reads as late enrichment for something it has never heard of, and files as loose data rather than a stroke. - The engine MUST stamp it with the same authorship it stamps a composed record with:
sga_kind,session_id,manifest,bundle_version, the clocks, and the slot. A record that cannot name the manifest it was taken under cannot be audited against one.
This is the failure worth naming, because it is silent and it costs strokes. Where a non-swing stroke is left to the correlation path, nothing errors: the record is journaled as an ordinary data contribution, the round still ships, and the only symptom is that the cloud's copy of the card is short by exactly the strokes nobody swung at — the hole reads as never finished, and the reconciliation in §9.1 fails against a shot stream that is missing the very records §7.1 requires.
8. Why record fields nothing consumes#
Most of this record has no consumer today. strike_horizontal_mm feeds nothing; no display asks for speed_loss_ratio.
Record it anyway. Per 03 §3, distribution is filtered and recording is total. The marginal cost of journaling a field already crossing the wire is nil; the cost of not having it, when a tournament result is challenged three weeks later or when someone finally asks what a golf ball actually does across a million shots, is unrecoverable.
The fields nobody wants today are the corpus somebody buys in five years.
9. Scorecard checkpoints#
After every player in the round completes a hole, the simulator emits a scorecard checkpoint — the whole card so far, in one standard shape. Schema: scorecard.schema.json.
The engine can also ask for one at any time (§9.4), including before a ball has been struck. Every card says why it was issued, so a consumer never has to infer whether it is looking at a score or at a course.
Cards, and the course document beside them, sit inside a frame: a round says where it starts and stops in records of its own (§9.6), and a card is never the thing that opens or closes one.
It does three jobs that are worth keeping distinct.
9.1 Independent evidence#
The shot stream and the scorecard are produced by different logic. Where they disagree, something is wrong — a shot was dropped on the wire, the simulator counted a stroke nothing measured, a score was edited, or a player's strokes landed on the wrong slot.
Reconciliation is a plain equality:
hole.strokes == sum(strokes_counted over records where counted = true)
No exemptions, no special cases. That is the payoff of §7.1: because a gimme, a drop and a penalty each emit their own record, there is nothing to subtract and nothing to argue about. Checked by conformance rule R062.
Reconciliation never subtracts gimmes or penalties from the stroke count: that would be wrong in both directions, flagging honest rounds and letting a fabricated gimme hide a missing shot. Requiring a record for every stroke removes the arithmetic entirely.
The checkpoint cannot tell you which side is wrong — only that they disagree, at a specific hole, at a specific time, before the round ended and while the evidence is still fresh. That is exactly what an audit trail is for, and it is far more useful than discovering a discrepancy from a total three weeks later.
checkpoint_index is monotonic and previous_checkpoint_hash chains them, so a checkpoint cannot be removed or reordered after the fact without the chain breaking. Gaps are detectable, and a gap is itself evidence.
9.2 Canonical state for late joiners#
A display, a spectator board, or a phone that connects at hole 12 needs the score, not a shot stream to replay. The most recent checkpoint is the answer — one document, complete, current, and identical for every consumer.
This is why the checkpoint carries the full card rather than a delta. Deltas require every consumer to have seen every prior message, which is precisely what a device joining mid-round has not done.
9.3 Rules#
- A checkpoint MUST be emitted after every player in the round has completed a hole, and MUST NOT skip a hole that was played.
- It MUST carry every hole played so far, not only the one just finished.
player_uprecords who was at the ball when the stroke was struck, resolved by the engine at that moment rather than when a contributing device finished. Late kinematics and video are attributed by this, so a swing cannot be filed under the next player's name because the group rotated while a clip was still encoding (03 §3.4).display_labelis the typed screen name, carried for operator display. It is not an identity and MUST NOT be treated as one — see 00 §6. Rule R063 rejects labels that look like member identifiers.- Per-hole
completenessMUST be recorded. Partial rounds are first-class records; posting eligibility is a query, not an ingest filter. - Every card MUST carry
reason. A card with no results is not self-describing: an unplayed round and a round of zeros look identical otherwise, and one of those readings is a scorecard full of holes in one. courseon a card is a REFERENCE —course_idanddigest— and not a copy of the course. Par, stroke index, the number of holes and every tee's yardages are properties of the COURSE, not of any round, so they travel once in the course document (§9.5). Restating them on every checkpoint would repeat the same table all round and give a consumer two places to disagree about what par was.- A slot MUST carry its own
tee_setwhere the course offers more than one. Players in one group routinely play different tees, and a card that assumed a single set would misstate both the distance every one of them played and the rating their score is judged against. The name matches a tee in the course document, and that match is the whole join. length_mon a slot's hole is for the exception only: what that hole ACTUALLY played, when it differed from the card yardage — a temporary tee, a pin moved to the front. Omitted when the hole played as printed.
9.4 Asking for a card#
A checkpoint after every hole is the right cadence for an audit trail and the wrong one for everything that happens before the golf starts. The shape of the round — which course, how many holes, what each is worth, who is playing off which tee — is settled at the moment the group finishes configuring the simulator, and nothing else in this protocol can carry it. Waiting for the first hole to be completed means a scoreboard, a phone and the cloud all spend the opening hole unable to draw a card at all.
So the engine MAY ask the simulator for one:
{ "type": "request.scorecard",
"round_id": "sga:round:01J9X4...",
"reason": "begin_round" }The simulator answers with a record.scorecard document whose reason echoes the request.
| reason | issued | carries |
|---|---|---|
hole_complete | unsolicited, after every player finishes a hole | the card so far — the checkpoint of §9.1 |
begin_round | on request, once the course and players are settled | who is playing and off which tee, and no results. Sent with the course document (§9.5), which carries the holes themselves |
interim_verify | on request, any time during play | the card so far, taken deliberately rather than on the hole boundary |
final_score | on request, when the round ends | the last word on the round |
begin_round is a card with no scores in it, and that is the point. It states what the round is going to be. hole_completed is absent — no hole has been finished — and a slot's holes carry par and length_m with no strokes. A strokes: 0 would be a score, and a round of nines read as a round of zeros is the exact misreading reason exists to prevent.
Why a pull and not another push. The simulator cannot know when anything downstream needs the card: a spectator board plugs in at hole 12, the cloud reconnects after an outage, an official wants a reconciliation now rather than at the next hole boundary. Pushing on a timer would send the same document to nobody most of the time; pushing only on hole boundaries is what left the opening hole blank. A request costs one frame and is answerable from state the simulator already holds.
Rules:
- A simulator declaring
scorecard_checkpointsMUST answerrequest.scorecard, and MUST setreasonto the value requested. - It MUST answer
begin_roundonce the course is chosen, and MUST NOT answer it with results. A simulator that has not yet settled the course MUST refuse withsession.deniedrather than guess: an invented course is worse than a late one, because it will be believed. - It MUST send the course document with that answer, unless one with a matching digest has already gone out this session. A
begin_roundcard naming a course nobody has been given is a card whose par cannot be resolved. - An engine MUST NOT treat a requested card as a hole boundary.
checkpoint_indexandprevious_checkpoint_hashchain (§9.1) across every card whatever its reason, so a requested card cannot be quietly dropped from the chain either. final_scoredoes not end a round. Around_endedrecord does (§9.6), and an engine acting on anrequest.end_round(03 §1.1) MUST ask for the card first — a simulator that has torn down the round has nothing left to answer with, and the record it then writes has to carry that card's hash.- Requesting a card MUST NOT change it. A consumer asking twice gets the same document, modulo whatever golf happened in between.
9.5 The course, sent once#
A scorecard says what happened. It does not say what the course is, and for most of a round it does not need to — until somebody asks how far a hole played, and the answer turns out to depend on which markers each player teed from.
A tee deck is not a multiplier. The obvious shortcut is to carry one course length per hole and scale it per tee — Championship 1.00, Members 0.90, Forward 0.78 — and it is wrong about every real course. Tees move each hole by a different amount and for different reasons: a forward tee on a par three may give up eighty meters while the one beside it gives up fifteen, because one is carrying a hazard and the other is not. A scaled course is internally consistent and matches no card ever printed.
So the course states its own numbers, per hole, per tee:
{ "sga_kind": "course",
"course_id": "sga:course:ironwood-dunes",
"name": "Ironwood Dunes",
"digest": "sha256:…",
"holes": [ { "number": 1, "par": 4, "stroke_index": 7 } ],
"tees": [
{ "name": "Blue", "rating": 72.4, "slope": 131,
"holes": { "1": { "length_m": 383 } } },
{ "name": "White", "holes": { "1": { "length_m": 351 } } }
] }Why it is a document of its own#
The course is a level that holds, the same way standing state is (02 §5.1), and the same argument applies. Eighteen holes across eight tees is a hundred and forty-four distances that do not change while anybody is playing. Folding them into every checkpoint would repeat the whole table on each one, which is the mistake this standard has already made once and removed.
Separating them also makes the data cacheable where it is read: a cloud holding a thousand courses holds a thousand tables, not one per round. That is what digest is for.
- A course document MUST carry a
digestover its own RFC 8785 canonical form withdigestremoved. - A consumer that already holds a document with that digest MAY ignore the body and keep what it has.
- A course that is edited — a tee re-measured, a green rebuilt — MUST produce a new digest. Rounds scored against the old one stay scored against the old one, which is the entire reason this is content-addressed rather than versioned by a number somebody remembers to increment.
Tee names are freeform, and that is deliberate#
Real courses name their markers by color, by name, by number, and by combinations of all three. Any enumeration this standard invented would be wrong within a week of meeting an actual course.
- A tee's
nameis freeform text, carried verbatim from the course. - It is an identifier for display and for joining a player to their yardages. A consumer MUST NOT parse meaning out of it — "Blue" is not a difficulty, and "3" is not a position.
- A slot's
tee_seton the scorecard MUST match a teenamein the course document exactly. That match is the only thing connecting a player to the distances they actually played.
When both are sent#
- A simulator MUST send the course document at the start of a round, alongside the opening scorecard (§9.4). Two documents, because they answer two questions, and both are wanted at the same moment: what is this course, and who is playing it.
- It MUST send an updated scorecard when the roster changes — a player added or removed mid-round (03 §3.4d). A card that still lists somebody who left, or omits somebody who joined, is a card a display will draw wrongly and a reconciliation will fail against.
- It MUST NOT need to resend the course for a roster change. Nobody joining a round alters what the course measures.
- An engine MAY request either at any time by directive, the same way it requests a card.
The line between the two documents#
It is not a size judgment, it is a question of what a fact is ABOUT. Par is a property of a hole. Stroke index is a property of a hole. How many holes there are is a property of the course. None of them is a property of a round, and none of them changes because somebody teed off — so none of them belongs on a card that is reissued after every hole.
| the course document says | the scorecard says |
|---|---|
| the holes, and each one's par | how many strokes each player took on them |
| stroke index | which strokes were received, and where |
| every tee, and what each hole measures from it | which tee each player used (tee_set) |
| how many holes the course has | how many of them have been completed |
The card keeps exactly one distance of its own: slots[].holes[].length_m, and only when a hole played differently from the card — a temporary tee, a pin moved to the front. That is a fact about the round.
A card is therefore not self-sufficient, deliberately. It carries course_id and digest, and a consumer that cannot resolve them cannot say what par was. That is the honest state of affairs: a consumer in that position genuinely does not know, and the alternative — restating par on every card — buys the appearance of knowing at the cost of two places to disagree about it.
- An engine MUST give a late joiner the course document alongside the canonical card (§9.2). Handing over a card whose course it has not sent leaves the joiner unable to draw a scoreboard, which is the one thing §9.2 exists to prevent.
9.6 Where a round begins and ends#
A round is the unit everything else is measured against — a card, a player rating, a tournament result, a bay's evening. A session may hold several, one after another, so a round states its own extent: two records, one at each end.
The boundary is a record, not something derived from the documents inside it. The deciding case is the round that ends with no document at all. A power cut, a crash, a group that walks out — whatever would have composed a closing card is gone, and a round with nothing to close it never ends: its check-ins stay live, its check-in code stays on the screen, and every display shows it in progress for ever. An engine can write a record; it cannot write a score.
Two smaller things follow from the same choice. Which round a stroke belongs to becomes something a consumer READS rather than a rule it has to be told, and two rounds in one session are told apart by their own references rather than by replaying documents in order and counting.
The two records#
{ "sga_kind": "round_started",
"round_ref": "r2",
"course": { "course_id": "sga:course:ironwood-dunes", "digest": "sha256:…" },
"at": "2026-09-16T01:12:04Z" }
{ "sga_kind": "round_ended",
"round_ref": "r2",
"reason": "completed",
"card_hash": "sha256:…",
"at": "2026-09-16T03:40:18Z" }round_ref is opaque and unique within the session — it does not need to be globally unique, because a session already is. reason is one of completed, operator_ended, abandoned or session_closed.
Rules#
- A simulator MUST emit
round_startedbefore the first shot record of a round, and after the roster and course are settled — those are what make it a round rather than an intention. - A roster asserted while the group is still being set up is not settled: it is published then so a member checking in can find their name, and it may change many times. The round begins when the simulator first asserts
round.player_up— somebody is at the ball — or, from a simulator that never asserts it, at the first shot record. An engine writing the boundary records MUST open the round at that moment and MUST NOT open one on a roster alone. Until then the session is in its lobby: check-ins made there are for the round about to begin. - A simulator MUST emit
round_endedwhen a round finishes by any route: the last hole completed, an operator ending it, or a group abandoning it mid-hole. round_endedMUST be accompanied by afinal_scorecard, and MUST carry that card's hash incard_hash. The pairing is then verifiable rather than conventional — a consumer can prove the card it is scoring is the card that closed the round.- Rounds MUST NOT nest or overlap. A
round_startedwhile a round is open is a fault, and a consumer MUST treat it as one rather than silently closing the first. - A consumer MUST attribute a shot record to the round whose boundaries enclose it, and MUST NOT attribute one that falls outside any round to a round at all.
- A card does not open or close a round, whatever its
reason(§9.4).request.end_round(03 §1.1) is an instruction to end one;round_endedis the record that it happened. An instruction that was refused, or that arrived at a bay with no round running, leaves no such record — which is the distinction being kept.
The engine closes what the simulator could not#
- An engine MUST emit
round_endedwithreason: "session_closed"when its session ends with a round still open. - That record MUST NOT carry a
card_hash. There is no card: the simulator is gone, and an engine composing one would be inventing a score. This is the oneround_endedthat stands alone, and a consumer MUST read it as the round stopped here, not as the round finished here.
This is the case the scorecard could never cover, and it is the reason the boundary is a record of its own rather than a field on the card. The engine outlives the round; the card does not.
What this makes of a practice swing#
§10.2 makes a swing a practice swing when nothing flight-critical ever binds its context — an air swing, or every device declining. The boundary supplies the other half:
- A swing measured outside any round is a practice swing, however complete its data. It MUST be journaled and MUST NOT be routed as a shot of any round.
That is the warm-up on the mat while the names are still being typed: a real swing, with real launch data, that is not golf. Nothing about the measurement distinguishes it — only where it falls.
9.7 Who is at the ball, as a record#
round.player_up is a standing field: the simulator asserts it and the engine stamps every stroke with it (§9.3). That answers whose stroke a shot was. It does not answer the question a member's phone asks between shots, whose turn is it now, because a standing value is journaled as a field like any other and a consumer would have to know which of the bay's fields matters and replay it. So the change of player is a record of its own.
{ "sga_kind": "player_up",
"round_ref": "r2",
"slot": 3,
"display_label": "Sam",
"at": "2026-09-16T01:14:40Z" }slot is the playing position, and it is what binds to a person: a consumer that knows which check-in holds that slot knows who is up. display_label is the typed name, for display only, and is not an identity (00 §6).
Rules#
- An engine MUST journal
player_upeach time the player at the ball changes: the first assertion of a round, and every assertion naming a different slot from the last one. It MUST NOT journal a repeat. A simulator restating the same player, or the same player playing two strokes in a row, is not a change of turn, and a consumer alerting a member on every record would alert them on every stroke. - A change of label with the slot unchanged is a correction to the name, not a new turn, and is not journaled as one.
- The record MUST carry the
round_refof the round it falls in. The firstplayer_upof a round is the assertion that opened it (§9.6), soround_startedcomes first and the two carry the same reference. - An engine SHOULD send
player_upto the cloud as soon as it is written, rather than at its next scheduled upload. Its only use is telling somebody it is their turn, and a turn announced after they have already hit is noise. - A consumer MUST read a stroke by the player who is up as the end of their turn: from that moment nobody in the round is up until the next
player_up. A stroke here is a shot record of anyshot_kindbutdropandpenalty(§7.1): those move the ball or the score, and the player still has to play. A consumer that tells a member it is their turn MUST take that back at their stroke and MUST NOT let one round's notices to one member accumulate.
10. Shot correlation and the flight window#
A shot record is composed from several devices that see different parts of the same swing and finish at different times (03 §2.2b). This section says how the engine decides that those reports describe one shot, and when it stops waiting.
The whole section exists to serve one design rule:
Make the wrong implementation impossible rather than merely discouraged.
A simulator will compute a ball flight from the first launch data it receives and put it on screen. It cannot retract that. So the engine MUST NOT hand a simulator a value it may later improve on — not because implementers are careless, but because a correction that arrives after the ball is airborne has nowhere to go.
10.1 Two classes of field#
| class | fields | why it matters |
|---|---|---|
| flight-critical | the launch condition — ball speed, launch and azimuth angles, spin — everything a ball-flight model needs to draw the shot | unrevisable once rendered |
| enrichment | club delivery, face and path, swing kinematics, pressure, media | changes nothing about where the ball goes; the ball data already encodes the outcome |
Only flight-critical fields hold the window open. Enrichment is routed the moment it arrives, tagged with the shot_id, and a consumer that wants it subscribes to it like anything else.
10.2 The pending context, and practice swings#
Enrichment routinely arrives before launch data, and that is not a race. Club delivery physically precedes ball flight: an RFID reader sees the club go past, a pressure mat records the weight shift during the backswing, a camera is recording before impact.
So the engine opens a pending context on the first evidence of any kind, and allocates the shot_id there:
- Flight-critical data, or an explicit no-swing stroke record (§7.1), binds the context. It is a shot. Everything held is released with it.
- A context that expires having seen neither was a practice swing. It MUST be journaled and MUST NOT be routed as a shot.
That second rule is the trap in holding early data at all. A practice swing produces club and pressure data and no ball, and releasing it on a timer emits a shot that never happened. Journaling it costs nothing and is good coaching data — the standard already records practice strokes with counted: false.
This test is necessary and not sufficient. It catches the swing that was never measured and says nothing about the one that was: a warm-up produces a complete, correct measurement and binds its context like any other shot. The round's boundaries decide that case (§9.6) — a swing measured outside any round is a practice swing however good its data.
10.3 record.swing_detected, and declining#
On opening a context the engine SHOULD notify every participant:
{
"type": "record.swing_detected",
"shot_id": "sga:shot:01J9X4...",
"t_ms": 842301,
"deadline_t_ms": 842481,
"window_ms": 180,
"pending": ["sga:device:lm-a", "sga:device:lm-b"]
}- The deadline is absolute, on the session clock. A duration alone is a bug waiting to happen: a device handed "180 ms" that receives the notice 60 ms late has 120 ms and no way to know it.
window_msis carried as a convenience, and the deadline governs. pendinglists only participants that declared a flight-critical field inprovides. Everyone else may receive the notice — it is useful for waking a radio or starting a buffer — but their silence MUST NOT hold the window.
A pending participant that has nothing SHOULD say so immediately rather than let the deadline expire:
{ "type": "request.shot_decline", "shot_id": "sga:shot:01J9X4...", "reason": "not_ready" }reason is one of not_ready, no_detection, blocked, out_of_range, declined. A boolean would be cheaper and worth much less. "I was armed and saw nothing" and "I was never ready" are different facts: the first is a monitor that may be failing, the second is a monitor that was not in play. Only the coded form can tell them apart afterwards.
A participant that saw the swing but needs longer than the budget MAY ask once:
{ "type": "request.shot_extend", "shot_id": "sga:shot:01J9X4...", "need_ms": 120 }The engine MUST cap the extension and MUST NOT grant more than one per shot. Otherwise a single slow device can stall every shot in the bay indefinitely.
10.4 When the window closes#
The window closes at the earliest of:
- every pending participant has reported or declined — the normal exit;
- the deadline passes;
- no pending participant could still change the result.
The third is what makes this affordable. The engine already knows each participant's declared provenance, resolution and latency_ms, so it can ask whether a pending contributor could still win the connection matrix. If the data in hand already beats every pending participant's declared capability on every flight-critical field, waiting cannot improve the answer and the engine MUST NOT wait.
The deadline is a backstop, never a delay. An engine MUST close the window the moment any of the three conditions is met and MUST NOT wait for the deadline once the pending set is satisfied. window_ms is how long the engine is willing to wait, not how long a shot takes.
In the ordinary bay this costs nothing. Where no participant other than the one reporting has declared a flight-critical field, the pending set is complete the instant that participant reports, and the engine MUST emit immediately: there is nobody left who could change the answer, so there is nothing to wait for. One launch monitor, one report, no added latency whatsoever. Latency is paid only in the case that buys a better number.
This is the common configuration and it is the one most easily got wrong: an implementation that starts a timer on every swing and emits when it expires is conformant to the letter of nothing here, and adds the full window to every shot in a bay that could never have benefited from it.
On expiry the engine emits what it has and records the absent participants as unmet for that shot, so a monitor that keeps missing swings appears in the data instead of quietly degrading the round.
10.5 What the engine emits#
One composed record per shot, marked complete:
{ "type": "record.shot", "field": "ball.speed_mps",
"shot_id": "sga:shot:01J9X4...",
"integrity": { "flight_complete": true, "window_closed": "complete" }, ... }- The engine MUST emit the flight-critical set exactly once per shot. A consumer that naively renders what it is given is therefore correct by construction.
- Enrichment arriving afterwards is routed as it comes, carrying the same
shot_id. A consumer MUST treat a later message bearing a knownshot_idas additional detail about that shot and MUST NOT treat it as a second shot.
10.6 Arming narrows the field; it does not decide it#
Where a device publishes an armed state, the engine SHOULD use it to choose whom to put in pending, and a device that is not armed SHOULD decline with not_ready.
Arming is not an election. Two monitors may legitimately arm on the same ball, and an engine that assumes exactly one will break the first time that happens. Arming narrows who is asked; the connection matrix still resolves whoever answers.
This is what makes several ordinary setups work with no special handling: a left-handed and a right-handed monitor either side of one mat, a putting sensor beside a full-swing unit, or four players outdoors each carrying a personal monitor where only the player at the ball has theirs placed. In each case the units that are not in play decline in milliseconds, the window collapses, and the shot goes out at full speed.
It is also a signal in its own right: which side of the ball armed is a handedness observation, and a monitor that can tell should publish it as player.lm_handedness (03 §3.4f). It costs nothing — the arming already happened — and it is the cheapest way a bay ever notices it has been set up for the wrong hand.
11. Correction after the fact#
Some fields are the best answer available at the time and a worse answer than one that arrives later. The clearest case is a resting position that no sensor watched: outdoors, or anywhere the ball leaves what the equipment can see, the simulator's model produces an estimate of where it finished, and the next stroke's launch monitor — placed at the ball, looking straight at it — produces a measurement of where it actually was.
This section says how the second replaces the first without rewriting history.
11.1 Append, never overwrite#
The journal is hash-chained and append-only (07). A correction is therefore a new record that references the original, never an edit of it:
{
"sga_kind": "shot-correction",
"corrects": "sga:shot:01J9X4...",
"field": "outcome.end_position",
"value": { "x_m": 214.7, "y_m": -31.2, "uncertainty_m": 0.15 },
"provenance": "calculated",
"from": ["sga:shot:01J9X7.../origin.position"],
"reason": "measured_at_next_stroke"
}- A correction MUST carry its own provenance and its own
from. The corrected value is not measured — it is calculated from a measurement taken on a different stroke, and saying so is the difference between a record that can be audited and one that merely looks tidy. - A consumer computing a rating MUST use the corrected value. Anything reconstructing what the bay believed at the time MUST use the original. Both are in the journal; neither is privileged by being written first.
- A correction MUST NOT change a field whose original provenance was
measured. A measurement is not improved by a later estimate, and a standard that allowed that would have no floor.
11.2 The two ends of a shot check each other#
Shot n's outcome and shot n+1's origin describe the same ball in the same place, and both are recorded (§3, §7). That overlap is not redundancy to be optimized away — it is the only cross-check in the record that costs nothing to collect.
- Where no relocating event intervened, shot n's
outcome.end_lie_surfaceandend_distance_to_pin_mSHOULD agree with shot n+1'sorigin.lie_surfaceanddistance_to_pin_m. - A relocating event — any
shot_kindofdrop,penalty,gimme,concession, or a hole boundary — suspends the expectation.droprecordsreplaced_positionfor exactly this reason (§7.1). - Disagreement is not an error. Where the two differ and one is
measuredwhile the other isinferred, the measured value is the correction and §11.1 is how it is recorded. Where both are measured, the disagreement is a finding worth surfacing: two sensors describing one ball in two places means one of them is wrong.
The last stroke of a hole has no successor, which is the reason outcome can never be dropped in favor of reading the next record's origin. Every holed putt, every conceded hole, every ball picked up — those outcomes exist nowhere else.
11.3 Estimates MUST carry their error#
A position whose provenance is inferred MUST carry uncertainty_m (§3). A point with no error bound reads as certainty, and a confident wrong answer is worse than an admitted vague one: it sends a player to the wrong trees with no reason to doubt it.
The bound is also what makes the estimate improve. Once corrections are recorded against estimates, the distance between the two is a measured error, and a model that compares its predicted radius against its actual error self-calibrates — by course, by club, by player. A system that has watched a season of rounds knows how wrong it usually is with a seven iron out of rough, which is a thing no amount of modeling gets you on day one.
11.4 What this makes possible#
Every stroke is a self-contained record carrying both endpoints, a trajectory and a corrected resting position. That is sufficient to draw an entire hole — each shot's arc, where it landed, where it finished, in order — without replaying a session or joining records together.
That is worth stating because it is the reason the overlap in §11.2 is deliberate. It was never duplication; it is what makes a round drawable.
12. Real-world coordinates#
Everything above works without this section. It exists so that a shot recorded indoors can be placed on the real course the file was digitized from, and a shot struck outdoors can be recorded at all. All of it is optional. A bay that ignores it behaves exactly as it does today.
12.1 Georeference the course, not every position#
course.georeference declares, once, how the course file maps onto the world: the datum and epoch, the real-world location of local (0, 0), and the compass bearing of the local +y axis. Anything that needs world coordinates applies that transform.
Positions are not required to carry world coordinates, and repeating them on every record would be worse than useless: two representations of one point can drift apart, and then a consumer has to decide which to believe. One transform cannot disagree with itself.
A course without a georeference simply cannot be overlaid. That is the honest failure, and it is better than a guess.
12.2 position.world, and which end is authoritative#
world is an optional sibling of x_m/y_m/z_m on every position in the record. It is carried when a position was genuinely observed in the world, or when a producer has already done the transform and wants to save consumers the work.
The local frame stays authoritative for everything the standard computes. Outdoors the local frame is the tangent plane the georeference defines, so x_m/y_m remain meaningful and required — the projection is simple planar arithmetic over a course-sized area, not geodesy, and a bay-local engine is not expected to do anything harder than that.
Provenance settles which end was measured. Indoors the simulator is the authority on its own world: the local position is measured and any world position is calculated from the georeference. Outdoors a GNSS fix is measured and the local position is calculated from it. Same fields, opposite directions, and a consumer already reads provenance to know which.
12.3 Precision is not optional just because the fields are#
A world position that does not say how it was obtained is not usable, only reassuring.
height_refis REQUIRED in practice wheneverheight_mis present. Ellipsoidal and orthometric height differ by −20 to −35 m across the continental US. Mixing them produces elevation changes larger than the hole.datumandepochare inherited from the georeference when omitted and MUST NOT be assumed otherwise. WGS84 and NAD83 differ by 1–2 m in the same region, and North America moves 2–7 cm a year, so an undated coordinate quietly stops being true.h_accuracy_mandv_accuracy_mare separate because vertical error is typically 1.5–3× horizontal, and one combined figure conceals exactly the axis that ruins an elevation calculation.fix_typedistinguishes a 3–5 m phone fix from a 1–2 cm RTK fix and fromestimated, which means there was no fix at all.
Accuracy should gate use, not admission. A 1 m error is irrelevant on a 150 m approach and disqualifying on a 2 m putt. A consumer that rates shots SHOULD decide from the reported accuracy what a position may be used for, the same way an event decides from min_attestation_tier what equipment it will accept. Rejecting an imprecise position outright throws away a value that was good enough for finding the ball.
12.4 Aim is supplied, not sensed#
Launch monitors measure relative to their own axis and do not report an absolute bearing; alignment is a physical act performed by the person setting the unit down. The standard therefore MUST NOT require a monitor to know which way it points.
What it needs is an aim reference, and the best source is usually not a sensor: given the ball's position and the target, the bearing is simply ball-to-target. course.pin_position is what makes that computable, and a derived bearing has none of the failure modes of a magnetometer sitting next to a bag of clubs and a motorised cart.
Aim is therefore an ordinary field like any other — something a participant provides, resolved by the connection matrix. Nothing new is required for a better source to appear later.
12.5 course.pin_position#
Worth having whether or not any of the above is ever used.
distance_to_pin_m is the input to nearly everything the standard computes, and until now nothing recorded where the pin actually was. Two rounds on the same course file, with the hole cut in different places, are not comparable — and comparing a professional's approach to a Sunday pin against an amateur's to a fixed one is not a comparison at all.
pin_sheet_ref carries the facility's own label for the day's setup, for humans. It is a companion to the position and never a substitute for it.
13. The journal's record kinds#
Normative. Every record the journal holds carries a kind, and a consumer switches on it: the association to decide what a record means, an auditor to find the ones that matter, the conformance kit to know which schema applies. The complete list is therefore part of the standard rather than an artifact of one engine.
These are not message types. The wire vocabulary is in 03 §1.2; this one is separate on purpose. A message is what crossed a wire, a record is what was written down, and several kinds here — a composed shot, a manifest, a superseded participant — correspond to no single message at all. An engine writes records nobody sent it.
A consumer MUST ignore a kind it does not recognize rather than refusing the batch: kinds are added as the engine learns to record more, and a reader that fails on an unfamiliar one turns every extension into an outage.
the session
| kind | written when |
|---|---|
session_start | the journal opens; names the bay, session and bundle |
engine_restarted | an existing session was reopened after the engine came back |
engine_stopped | the engine closed the journal on purpose |
going_away | a planned shutdown, with when to come back |
participants
| kind | written when |
|---|---|
participant_join | a device was admitted, with its descriptor hash |
participant_leave | a device's connection ended |
participant_superseded | the same unit reconnected and replaced its own stale connection |
admission_denied | a device was refused, and why |
manifest | the connection matrix in force from this point |
provenance_degraded | a field's best source got worse or went away |
the round
| kind | written when |
|---|---|
round_started | a round began; names the course |
round_ended | a round ended; names the card that closed it |
roster | who is playing, label to slot |
player_up | the player at the ball changed; names the slot (§9.7) |
course | the course document a card's references resolve against |
scorecard | a card, for a stated reason |
checked_in | who has scanned in at this bay |
shots
| kind | written when |
|---|---|
swing_detected | a shot_id was allocated on first evidence of a swing |
shot | the composed shot record |
data | one contribution to a field, as the device sent it |
shot_declined | a device said it would not contribute to this shot |
shot_extended | a device asked for more of the correlation window |
practice_swing | a swing that resolved to no stroke |
standing state
| kind | written when |
|---|---|
standing_asserted | a standing value was published |
standing_retracted | a standing value was withdrawn |
standing_ambiguous | two sources disagreed and neither wins |
standing_digest | a periodic digest of standing state, so a gap is detectable |
slots
| kind | written when |
|---|---|
slot_bound | a slot was bound to a check-in |
slot_rebind | a bound slot moved to a different check-in |
slot_released | a slot's binding ended |
requests
| kind | written when |
|---|---|
request | a request was made, with who asked |
request_refused | a request was refused, and why |
devices
| kind | written when |
|---|---|
device_enrolled | a device was enrolled at this bay |
device_unenrolled | a device was removed from this bay |
device_renamed | a device's local name changed |
device_policy_rejected | a device policy was refused as unusable |
device_notice | a notice was raised about a device |
update_fetch | a firmware transfer was attempted, with bytes and outcome |
notices
| kind | written when |
|---|---|
notice | a notice was raised |
notice_resolved | a notice was answered or expired |
policy_notified | a notice policy decided who to tell |
policy and certification
| kind | written when |
|---|---|
fetch_policy_loaded | an update budget policy came into force |
notice_policy_loaded | a notice policy came into force |
lapse_policy_loaded | a certification lapse policy came into force |
certificate_loaded | a bay certificate came into force |
certification_downgrade | the bay's certified tier dropped |
certification_lapsed | the bay's certification expired |
certification_shortfall | the bay no longer meets its certified tier |
event_bay_ineligible | the bay cannot host a sanctioned event |
Every record also carries seq, session_epoch, t_ms, ts, from, payload, prev_hash and hash. The chain is over the whole record, so a kind cannot be altered after the fact without breaking it.