{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/capability-descriptor.schema.json",
  "title": "SGA-OIS capability descriptor",
  "description": "What a participant sends in DESCRIBE. Answers 'what can you do' and 'what do you want in return'. Signed as one document together with identity — signing identity alone would let a device attest to being one thing and then claim capabilities it does not have.",
  "type": "object",
  "required": [
    "identity",
    "roles",
    "protocol_version",
    "provides"
  ],
  "additionalProperties": false,
  "properties": {
    "sga_kind": {
      "const": "capability-descriptor",
      "description": "Document type discriminator. Always capability-descriptor. 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."
    },
    "type": {
      "const": "session.describe",
      "description": "The wire frame's message type. Optional here, so the exact frame a device sends can be checked as it stands."
    },
    "identity": {
      "type": "object",
      "required": [
        "vendor",
        "model",
        "device_id",
        "attestation_tier"
      ],
      "additionalProperties": false,
      "properties": {
        "vendor": {
          "type": "string",
          "maxLength": 64,
          "description": "Manufacturer name."
        },
        "model": {
          "$ref": "common.schema.json#/$defs/modelName",
          "description": "Model name. Together with vendor, this is what deny rules, certificates and certified-model lists match on."
        },
        "serial": {
          "type": "string",
          "maxLength": 128,
          "description": "Travels to SGA for certification and audit. MUST NOT be redistributed to other participants in the bay, and never to the member-facing app."
        },
        "firmware": {
          "type": "string",
          "maxLength": 64,
          "description": "Firmware revision, as the vendor names it. A device that omits this cannot be matched by a firmware-specific rule -- and silence is never read as a claim to be a named build, so omitting it can only ever deny admission, never win it."
        },
        "firmware_digest": {
          "$ref": "common.schema.json#/$defs/digest",
          "description": "What makes 'approved software version' enforceable. A version string is a self-report; a digest can be checked against what the vendor published."
        },
        "hardware_revision": {
          "type": "string",
          "maxLength": 64,
          "description": "Hardware revision, for vendors who ship more than one under a single model name."
        },
        "device_id": {
          "$ref": "common.schema.json#/$defs/sgaId"
        },
        "attestation_tier": {
          "$ref": "common.schema.json#/$defs/attestationTier"
        },
        "attestation": {
          "$ref": "common.schema.json#/$defs/signature"
        },
        "sensors": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "$ref": "common.schema.json#/$defs/sensorType"
          },
          "description": "Every sensing modality present on this unit. Multiple entries are normal: real devices combine technologies -- dual radar with a camera, quadrascopic photometric cameras, overhead high-speed infrared. Declare all of them."
        },
        "calibrated_at": {
          "$ref": "common.schema.json#/$defs/timestamp"
        },
        "calibration_expires_at": {
          "$ref": "common.schema.json#/$defs/timestamp"
        },
        "declared_binary_path": {
          "type": "string",
          "maxLength": 1024,
          "description": "Where the software making this connection is installed, as the participant states it, so the engine can measure the running build against the vendor's published digest (02 section 2.2). A declaration, not a measurement."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "What the device is. Sent once at handshake, and used for admission policy, certificate matching and support."
    },
    "roles": {
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "common.schema.json#/$defs/role"
      },
      "description": "Additive. Roles determine defaults, not permissions; permissions come from session policy."
    },
    "own_vendor_only": {
      "type": "array",
      "uniqueItems": true,
      "maxItems": 16,
      "items": {
        "$ref": "common.schema.json#/$defs/role"
      },
      "description": "Roles this product accepts only from its own vendor's devices (03 section 2.1c). While it is in a session, a device from another vendor that holds one of these roles is admitted and recorded as usual, but is not a source for any field in that session's matrix, and the manifest lists it under excluded. All or nothing by governance: a product that accepts a role from any other vendor accepts it from every conforming device, and a product may list only roles in which its vendor ships a device of its own. Absent or empty means every conforming device is accepted, which is the default."
    },
    "protocol_version": {
      "$ref": "common.schema.json#/$defs/protocolVersion",
      "description": "The framing and handshake this device speaks. Load-bearing: the engine must be able to talk to it at all."
    },
    "built_against_bundle": {
      "$ref": "common.schema.json#/$defs/bundleVersion",
      "description": "OPTIONAL, and a different thing from the engine's bundle_version. A device does not LOAD bundles -- only an engine does. This is a build-time constant baked into firmware saying which generation of the field catalog the vendor consulted when they chose their field names. It exists for diagnosis: a device built against 2025.03 emitting a field renamed in 2026.01 is explicable rather than mysterious, and it distinguishes 'this device predates that field' from 'this device chose not to provide it'. It NEVER gates anything, and a device that does not track it simply omits it."
    },
    "discovered_via": {
      "$ref": "common.schema.json#/$defs/discoveryPath",
      "description": "OPTIONAL. How this device found the engine (03 section 5). A device that omits it MUST NOT be refused; a conforming device SHOULD send it. An engine records it as given, including a value it does not recognize."
    },
    "provides": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/providedField"
      },
      "description": "The fields this device can supply, each with the resolution, latency and rate it can actually achieve. A claim about capability, not a promise to send."
    },
    "wants": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "field"
        ],
        "additionalProperties": false,
        "properties": {
          "field": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^(x-[a-z0-9_-]+\\.(\\*|[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*(\\.\\*)?)|[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*(\\.\\*)?)$",
            "description": "The field being requested, or a namespace ending in '.*' to request every core field beneath it -- 'ball.*' asks for whatever ball data this bay happens to have.\n\nOnly a TRAILING '.*' is a wildcard. '*.speed_mps' and 'ball.*.rpm' are not patterns, they are typos, and the pattern here rejects them: matching is a prefix scan, so no participant needs a regular-expression engine to take part.\n\nA CORE wildcard never reaches into a vendor namespace: 'ball.*' will not subscribe you to 'x-acme.ball.whatever'. Otherwise one vendor's private extension becomes a broadcast to everyone who typed a wildcard. Naming a vendor field, or a vendor namespace, is a deliberate act and is allowed -- 'x-acme.spin_model' and 'x-acme.*' are both valid.\n\nNo wildcard, core or vendor, matches a field its provider marked restricted or entitlement-gated. Those exist because somebody's license governs them, and a wildcard must never be how that gets bypassed."
          },
          "importance": {
            "$ref": "common.schema.json#/$defs/wantImportance",
            "description": "OPTIONAL, and normally omitted -- the engine derives it. A named field defaults to 'requested'; a field matched by a wildcard defaults to 'opportunistic'.\n\nState it only to raise a wildcard to 'requested', or to mark a named field 'required'. A wildcard MUST NOT be 'required'."
          },
          "reason_code": {
            "$ref": "common.schema.json#/$defs/wantReason",
            "description": "Optional, countable. Nothing in the protocol branches on it."
          },
          "reason": {
            "type": "string",
            "maxLength": 200,
            "description": "Free text, deliberately unconstrained. Aggregated across the ecosystem this is how we learn which fields devices actually need, without surveying anyone -- and a vocabulary can only contain what we already thought of, which is the opposite of what this field is for."
          },
          "max_rate_hz": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "The fastest rate this consumer can usefully accept. The engine will not deliver faster, however fast the provider can produce -- a display that repaints at 10 Hz gains nothing from 240 Hz of data and may drown in it."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      },
      "description": "The fields this device would like delivered to it. Asking is free and unconditional; the engine decides who, if anyone, actually supplies each one."
    },
    "features": {
      "type": "object",
      "additionalProperties": false,
      "description": "Behaviors a participant supports, as distinct from data it provides. Declared so the engine never promises something the software cannot deliver.",
      "properties": {
        "round_resume": {
          "type": "boolean",
          "default": false,
          "description": "This simulator can save a round in progress and restore it in a later session. Many cannot -- widely used simulators offer no visible pause-and-resume for a round underway. Where false the engine must not offer resumption, and an interrupted round is recorded as partial rather than held open pretending it can be continued."
        },
        "settings_attestation": {
          "type": "boolean",
          "default": false,
          "description": "Can report the settings actually in force, not merely acknowledge a request. Required to host sanctioned events; see spec/08-competition.md."
        },
        "course_hashing": {
          "type": "boolean",
          "default": false,
          "description": "The device can compute the course digest used to prove two bays played the same layout."
        },
        "input_lockout": {
          "type": "boolean",
          "default": false,
          "description": "Can disable non-measured swing input on request and report it disabled."
        },
        "settings_lockout": {
          "type": "boolean",
          "default": false,
          "description": "Can hold the settings an event package pins for the whole round, and lists in round.settings.locked which ones it is holding (08 section 2.1a). Required where a package sets software_requirements.settings_lockout_required."
        },
        "scorecard_checkpoints": {
          "type": "boolean",
          "default": false,
          "description": "The device emits scorecard checkpoints, so a round survives a crash with per-hole completeness intact rather than being lost whole."
        },
        "state_retraction": {
          "type": "boolean",
          "default": false,
          "description": "Can withdraw a standing assertion rather than only replacing it."
        },
        "roster_add_in_round": {
          "type": "boolean",
          "default": false,
          "description": "A player may be added after the round has started (15 section 6). Absent is a refusal."
        },
        "roster_remove_in_round": {
          "type": "boolean",
          "default": false,
          "description": "A player may be removed after the round has started (15 section 6). Absent is a refusal."
        },
        "operator_end_round": {
          "type": "boolean",
          "default": false,
          "description": "The round may be ended from the portal, relayed by the cloud (05 section 4c). A bay declaring none of the operator flags cannot be operated remotely, and that is always a supported configuration."
        },
        "roster_update_in_round": {
          "type": "boolean",
          "default": false,
          "description": "A seated player's tee or display label may be changed with request.update_player, before or during the round (15 section 6). Absent means no."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "transports": {
      "type": "array",
      "items": {
        "enum": [
          "wss",
          "ws",
          "tcp",
          "ble",
          "usb",
          "serial",
          "loopback"
        ]
      },
      "description": "Every transport this device can speak, most preferred first."
    },
    "max_rate_hz": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "An overall ceiling across all fields together, for devices whose real limit is the link rather than any single sensor."
    },
    "scope": {
      "enum": [
        "object",
        "person"
      ],
      "default": "object",
      "description": "Whether this device describes an OBJECT or a PERSON, which decides how its data is attributed. Note there is deliberately no sharing policy here: whether a person-scoped device may change hands is set by its owner when they bind it, not declared by its maker, because the same hardware is a personal sensor on one wrist and a facility loaner on another and the vendor cannot know which. A rack describes a club, so its data follows whoever is swinging. A wearable describes its wearer, so its data follows them and nobody else. Roles sensor.swing, sensor.pressure and sensor.player imply person regardless of what is declared, because getting this wrong silently corrupts a coaching model rather than producing an obviously wrong value."
    },
    "bay_token": {
      "type": "string",
      "description": "The pairing token printed on the bay's placard: <bay_id>.<key_id>.<signature>, where the signature is SGA's Ed25519 signature over the bay_id, base64url without padding (02 section 4). Obtained from the physical bay directly, or handed to the device by a phone that scanned the placard on its behalf. The engine verifies it; a token that does not verify, or names another bay, leaves the device unbound."
    },
    "x": {
      "$ref": "common.schema.json#/$defs/vendorExtensions"
    },
    "media": {
      "type": "object",
      "additionalProperties": false,
      "description": "What a camera participant can capture (11 section 6).",
      "properties": {
        "source": {
          "enum": [
            "rtsp",
            "usb",
            "file",
            "sdk"
          ],
          "description": "Where the frames come from."
        },
        "exclusive_device": {
          "type": "boolean",
          "description": "true when opening the camera takes it from anything else using it."
        },
        "buffer_s": {
          "type": "number",
          "minimum": 0,
          "description": "Seconds of video held in the rolling buffer."
        },
        "pre_roll_s": {
          "type": "number",
          "minimum": 0,
          "description": "Seconds kept before the stroke."
        },
        "post_roll_s": {
          "type": "number",
          "minimum": 0,
          "description": "Seconds kept after the stroke."
        },
        "resolution": {
          "type": "string",
          "pattern": "^[0-9]+x[0-9]+$"
        },
        "frame_rate_hz": {
          "type": "number",
          "minimum": 0
        },
        "codec": {
          "type": "string",
          "maxLength": 32
        },
        "keyframe_interval_s": {
          "type": "number",
          "minimum": 0
        },
        "stream_copy_capable": {
          "type": "boolean",
          "description": "Can cut a clip without re-encoding."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "origin_handle": {
      "$ref": "common.schema.json#/$defs/deviceHandle",
      "description": "For a member's own device introduced by their phone (13 section 4.1), the handle the phone gave it. The engine records the device under this handle and never under its own identifier."
    },
    "relays": {
      "type": "array",
      "maxItems": 16,
      "description": "Present on a member's phone that composes values from the member's own devices (13 section 4.1): one entry per relayed device. The phone also declares the fields in its own provides, because it is the participant asserting them.",
      "items": {
        "type": "object",
        "required": [
          "origin_handle",
          "class",
          "fields"
        ],
        "additionalProperties": false,
        "properties": {
          "origin_handle": {
            "$ref": "common.schema.json#/$defs/deviceHandle"
          },
          "class": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,63}$",
            "description": "Equipment class, such as bag_sensor or kinematic_suit. Not identity, and recorded normally."
          },
          "fields": {
            "type": "array",
            "minItems": 1,
            "maxItems": 256,
            "items": {
              "type": "string"
            },
            "description": "The fields this phone sends for this device, each also in the phone's provides."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    }
  },
  "patternProperties": {
    "^x-[a-z0-9_-]+\\.": {}
  },
  "$defs": {
    "providedField": {
      "type": "object",
      "required": [
        "field",
        "provenance"
      ],
      "additionalProperties": false,
      "properties": {
        "field": {
          "type": "string",
          "pattern": "^([a-z][a-z0-9_]*\\.)*[a-z][a-z0-9_]*$",
          "description": "Dotted path into the shot record, e.g. ball.speed_mps."
        },
        "provenance": {
          "$ref": "common.schema.json#/$defs/provenance"
        },
        "from": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "REQUIRED when provenance is 'calculated': the fields this was computed from."
        },
        "model": {
          "$ref": "common.schema.json#/$defs/modelRef",
          "description": "REQUIRED when provenance is 'inferred'."
        },
        "confidence": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Vendor's own stated reliability for an inferred field. A claim, not a measurement."
        },
        "resolution": {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "Smallest distinguishable step, in the field's canonical unit. Published so consumers can round honestly instead of displaying invented precision."
        },
        "latency_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Typical delay between the physical event and the value reaching the engine. Used as a tie-break when provenance and resolution are equal; lower wins."
        },
        "rate_hz": {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "How often the device can produce this field. Absent, rather than zero, for values that occur once per stroke."
        },
        "timing": {
          "$ref": "common.schema.json#/$defs/fieldTiming",
          "default": "shot_synchronous",
          "description": "Whether this field is produced by the shot or asserted between shots. Standing fields MUST be retractable; see spec/03-handshake.md."
        },
        "max_age_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "For a standing field: how long an assertion stays meaningful without renewal. After this the engine treats it as absent rather than carrying a stale value into a shot. A club rack might use minutes; a moving platform, seconds."
        },
        "sensor": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/sensorType"
          },
          "description": "Which of the device's sensors produce THIS field. Optional, and more useful than the device-level list: on a multi-modality unit it says that spin came from the camera and speed from the radar, which is what makes cross-vendor trend analysis meaningful."
        },
        "conditions": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/fieldCondition"
          },
          "description": "What must hold for this field to be valid at its declared provenance. A device operating outside its declared conditions is operating outside its certification -- which is why this is a registered vocabulary rather than free text: certification cannot check a condition only one vendor understands."
        },
        "conditions_note": {
          "type": "string",
          "maxLength": 200,
          "description": "Free text detail alongside the coded conditions -- a specific temperature range, a ceiling height. Advisory; the codes are what certification reads."
        },
        "id_methods": {
          "type": "array",
          "items": {
            "$ref": "common.schema.json#/$defs/clubIdMethod"
          },
          "description": "For club.club_id only: the identification methods this device is capable of, best first. A capability, not a promise -- whether any of them succeeds is a per-shot question, answered per shot in club.identification."
        },
        "restricted": {
          "type": "boolean",
          "default": false,
          "description": "Declared but withheld under a contractual restriction, such as an OEM agreement forbidding redistribution. Declaring it is the requirement; hiding it is the anti-fragmentation violation."
        },
        "entitlement": {
          "enum": [
            "enabled",
            "not_licensed",
            "trial",
            "expired"
          ],
          "default": "enabled",
          "description": "Commercial availability of a field the hardware can physically produce. Vendors commonly sell club data as a paid add-on, bundle it in a higher package, or tier it by subscription -- so device-cannot and this-unit-is-not-licensed are different facts and were being conflated. Also lets an operator see before an event that their bays are inconsistently licensed."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "provenance": {
                "const": "calculated"
              }
            },
            "required": [
              "provenance"
            ]
          },
          "then": {
            "required": [
              "from"
            ]
          }
        },
        {
          "if": {
            "properties": {
              "provenance": {
                "const": "inferred"
              }
            },
            "required": [
              "provenance"
            ]
          },
          "then": {
            "required": [
              "model"
            ]
          }
        }
      ],
      "description": "One field a device offers, together with the quality it offers it at. The engine ranks competing providers on exactly these numbers, which is why they are claims a vendor should be willing to defend."
    }
  }
}
