SGA-OIS conformance kit
Public, runnable offline, no network access, no SGA account.
Know you pass before you ever contact us. Passing the kit is the technical floor for certification. Trust levels — A1 and up — come from the partner agreement and signing, not from the kit (trust levels).
What the kit reads, and what it doesn't. It checks documents: your capability descriptor and, for a simulator, scorecards, round boundaries and the course document. It cannot see the frames you send live (record.shot, record.state); watch those with sgapoke or sgaengine -livedata (testing your integration).
Run it#
The kit is Python 3.10 or newer, with no virtualenv required, so it runs the same on a build server as on your bench.
# from sga-ois/conformance/
python -m sgaconform your-descriptor.json # your device
python -m sgaconform your-scorecard.json --manifest m.json
python -m sgaconform --list-rules # every rule, by document kind
python -m pytest tests/ -q # the kit's own tests (maintainers)Optional, for shape checking as well as semantics:
pip install jsonschema referencingWithout it, JSON Schema validation is skipped with a notice and the semantic rules still run. The semantic rules are the ones that catch real interoperability bugs, so the kit is useful either way.
Two passes#
| pass | checks | needs |
|---|---|---|
| schema | shape — types, required fields, ranges, enums | jsonschema |
| rules | meaning — the things that are structurally valid and still wrong | stdlib |
What the rules cover#
Run python -m sgaconform --list-rules for the list your copy enforces.
| rule | applies to | checks |
|---|---|---|
| R001 | capability-descriptor | calculated must say what it was calculated from. |
| R002 | capability-descriptor | inferred must name and version a model. |
| R003 | capability-descriptor | A calculated field's inputs should be obtainable. |
| R004 | shot-record | Provenance may be downgraded at runtime, never upgraded. |
| R010 | every document | Presence of x- fields must never itself be an error. |
| R011 | shot-record | Every core numeric field name ends in its unit. |
| R012 | every document | 'draw' and 'fade' are handedness-dependent readings of one number. |
| R013 | every document | An absolute timestamp must carry its UTC offset. |
| R014 | every document | t_ms must stay inside the 24-hour epoch, because it rolls over. |
| R015 | shot-record | Wall clock orders across sessions; round_id joins them. |
| R016 | capability-descriptor | Informational. A wildcard is valid and works; it just says less. |
| R017 | capability-descriptor | required has to name a field. |
| R020 | shot-record | |
| R021 | shot-record | |
| R022 | shot-record | |
| R023 | shot-record | The three places elevation appears must agree. |
| R024 | shot-record | |
| R030 | shot-record | |
| R031 | shot-record | |
| R032 | shot-record | |
| R033 | shot-record | |
| R040 | capability-descriptor | A2 and A3 sign; a claim of either needs a signature over the session nonce. |
| R041 | capability-descriptor | A firmware version without a digest is a self-report. |
| R042 | capability-descriptor | With one sensor, attribution is obvious. With several, only the device knows. |
| R043 | capability-descriptor | A field the unit is not licensed for must not read as available. |
| R044 | shot-record | A putt block on a shot played from a fairway is a mapping error. |
| R045 | shot-record | Configured green speed and measured roll deceleration should broadly match. |
| R046 | shot-record | A club id with no stated method cannot support per-club statistics. |
| R047 | shot-record | A UI selection is a claim about the club, not an observation of it. |
| R048 | shot-record | A shot may not claim an identification method the device cannot perform. |
| R049 | shot-record | A standing value carried into a shot should say how old it was. |
| R050 | event-rules-package | A sanctioned event pins its course by digest, not by name. |
| R051 | event-rules-package | Camera capture an event requires is disclosed at registration. |
| R052 | event-rules-package | An event restricted to launch-monitor input requires a launch monitor. |
| R060 | scorecard | A scorecard must agree with its own arithmetic. |
| R061 | scorecard | Checkpoints are emitted per hole and must not skip one. |
| R062 | scorecard | The card and the shot stream must agree, exactly. |
| R063 | scorecard | A screen name must never look like a member identifier. |
| R064 | scorecard | A card with no scores must be declared as one, and vice versa. |
| R064 | shot-record | A stroke added without a swing still has to say where the ball is now. |
| R065 | capability-descriptor | A standing field with no max_age_ms can go stale silently. |
| R065 | course | The course must cover its holes, and rank each one exactly once. |
| R066 | course | A tee that skips a hole is a tee nobody can play a round from. |
| R066 | event-rules-package | A deadline checked against the bay clock is not a deadline. |
| R067 | course | Two tees with one name cannot be told apart. |
| R067 | session-manifest | After the fact, it must be answerable what time the engine believed. |
| R068 | capability-descriptor | A simulator that cannot restore a round must say so. |
| R068 | round-boundary | A round that somebody ended has a card; one that was cut off has none. |
| R069 | bay-profile | A bay cannot host a deadline it has no trusted clock for. |
| R069 | round-boundary | A round that does not say what it is being played over cannot be scored. |
| R070 | bay-profile | Equipment changes, calibration expires, connectivity degrades. |
| R070 | request | Ending a round is a decision about somebody's afternoon. |
| R071 | bay-profile | A rear view shows a person swinging, not which person. |
| R071 | request | A request that cannot say on whose authority it was made is a request |
| R072 | shot-record | A sim set for the wrong side mirrors the shot, silently, for a whole round. |
| R073 | bay-profile | The second camera earns its cost on every shot, not just for lefties. |
| R074 | capability-descriptor | An unrecognized condition cannot be checked by anyone but its author. |
| R075 | shot-record | Zero is a measurement. Absent is the truth. |
| R076 | shot-record | A cold ball is measurably slower, and it is seasonal and systematic. |
| R079 | capability-descriptor | A device that measures a person must say so. |
| R080 | bay-profile | Enrollment IS the binding, so a token alongside it means nothing extra. |
| R081 | bay-profile | An admission list is only as current as the bundle carrying it. |
| R082 | bay-profile | A tier's floor is meaningless if the profile cannot show who resolves what. |
The kit also refuses a descriptor that claims A2 or A3 without a signature (R040). It checks a claim; it cannot grant a level.
Severity is error, warning or info. Only error fails a run — warnings are for a human to look at, not a gate.
Some rules need context. R004 can only verify that a downgrade is a downgrade if it can see the declared provenance, so pass --manifest; without it the rule reports info rather than error instead of guessing.
Adding a rule#
One function with a decorator in sgaconform/rules.py (core) or rules_vendor.py (descriptor and vendor-specific rules):
@rule("R0NN", "shot-record")
def my_check(doc, ctx):
"""One line, shown by --list-rules."""
if bad:
yield Finding("R0NN", "error", "path.to.field", "what and why")Read rules.py as the list of things you will be tested on. That is the point of shipping it rather than describing it.
Fixtures#
fixtures/valid/ must all pass. fixtures/invalid/ each fail on a specific rule, asserted by name in tests/, so a rule cannot silently stop working.
Vendor extensions are exercised deliberately: the shot-record fixture carries unknown x- fields, and a run that rejects them fails. Tier-1 extensions ship without permission and must never break another implementation.
License#
The conformance kit is Apache 2.0 — see ../LICENSE-CODE.txt. That differs from the specification itself, which is CC BY 4.0; ../LICENSE says which is which and why.