{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/bay-profile.schema.json",
  "title": "SGA-OIS bay capability profile",
  "description": "What a bay can actually prove, generated and signed by the engine rather than filled in by an inspector. Certification tiers are named bundles of requirements evaluated against this document, and an event's device_requirements are evaluated against it too -- the same mechanism, so a bay that certifies for a tier is by construction eligible for events at that tier.",
  "type": "object",
  "required": [
    "bay_id",
    "generated_at",
    "bundle_version",
    "participants"
  ],
  "additionalProperties": false,
  "properties": {
    "sga_kind": {
      "const": "bay-profile",
      "description": "Document type discriminator. Always bay-profile. 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."
    },
    "bay_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "facility_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "generated_at": {
      "$ref": "common.schema.json#/$defs/timestamp"
    },
    "valid_until": {
      "$ref": "common.schema.json#/$defs/timestamp",
      "description": "A profile goes stale. Equipment is added, firmware changes, calibration expires. Re-generated on any topology change and on a schedule."
    },
    "bundle_version": {
      "$ref": "common.schema.json#/$defs/bundleVersion"
    },
    "protocol_version": {
      "$ref": "common.schema.json#/$defs/protocolVersion"
    },
    "engine_version": {
      "type": "string",
      "maxLength": 64,
      "description": "Version of the SGA Engine that produced this profile."
    },
    "participants": {
      "type": "array",
      "description": "Every device the engine has seen at this bay, from its signed capability descriptor. This is the equipment inventory, and it is self-reported by the hardware rather than typed by a person.",
      "items": {
        "type": "object",
        "required": [
          "device_id",
          "roles",
          "attestation_tier"
        ],
        "additionalProperties": false,
        "properties": {
          "device_id": {
            "$ref": "common.schema.json#/$defs/sgaId"
          },
          "vendor": {
            "type": "string",
            "maxLength": 64,
            "description": "Manufacturer of the device, as it identified itself at handshake."
          },
          "model": {
            "$ref": "common.schema.json#/$defs/modelName",
            "description": "Model name, as it identified itself at handshake."
          },
          "firmware": {
            "type": "string",
            "maxLength": 64,
            "description": "Firmware revision the device was running when the profile was taken."
          },
          "firmware_digest": {
            "$ref": "common.schema.json#/$defs/digest"
          },
          "serial_present": {
            "type": "boolean",
            "description": "Whether a serial was supplied. The serial itself goes to SGA for certification and audit, never into a profile that circulates."
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this device did in the bay -- launch monitor, simulator, display, sensor. Descriptive; the profile records what was present, not what was routed."
          },
          "sensors": {
            "type": "array",
            "items": {
              "$ref": "common.schema.json#/$defs/sensorType"
            },
            "description": "The sensing this device performs, which is what determines the provenance of the values it contributes. Two devices can offer the same field and mean very different things by it."
          },
          "attestation_tier": {
            "$ref": "common.schema.json#/$defs/attestationTier"
          },
          "calibrated_at": {
            "$ref": "common.schema.json#/$defs/timestamp"
          },
          "calibration_expires_at": {
            "$ref": "common.schema.json#/$defs/timestamp"
          },
          "features": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "description": "Optional capabilities the device declared, such as course hashing or scorecard checkpoints."
          },
          "last_seen_at": {
            "$ref": "common.schema.json#/$defs/timestamp"
          },
          "enrolled": {
            "type": "object",
            "additionalProperties": false,
            "description": "Present when an operator attached this device to the bay permanently, rather than it pairing for a session. Bay infrastructure -- a ceiling camera, an environment sensor, a fixed rack -- is part of the room's equipment inventory, and certification should see it as such. An enrolled device presents no bay token: enrollment by an authenticated operator AT the machine is a stronger claim than a placard scan, not a weaker one. A player-owned device is never enrolled; it travels.",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 64,
                "description": "The operator's words -- \"Ceiling camera\", \"Left rack\" -- not a device id."
              },
              "added_at": {
                "$ref": "common.schema.json#/$defs/timestamp"
              },
              "denied": {
                "type": "boolean",
                "description": "Enrolled but currently refused admission by policy. Worth surfacing: a permanently attached device that cannot join is equipment the bay believes it has and does not."
              }
            },
            "patternProperties": {
              "^x-[a-z0-9_-]+\\.": {}
            }
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "fields_available": {
      "type": "object",
      "description": "The union of what this bay can produce, resolved the same way the connection matrix resolves a session: best provenance wins. This is the answer to 'can this bay gather everything a tier-3 event needs' and it is computed, not asserted.",
      "additionalProperties": {
        "type": "object",
        "required": [
          "provenance"
        ],
        "additionalProperties": false,
        "properties": {
          "provenance": {
            "$ref": "common.schema.json#/$defs/provenance"
          },
          "from": {
            "$ref": "common.schema.json#/$defs/sgaId"
          },
          "resolution": {
            "type": "number",
            "description": "The smallest change this source can actually distinguish, in the field's own unit. Declared so consumers can round honestly -- showing 71.3009 mph from a device with 0.1 mph resolution is a presentation bug publishing this value prevents."
          },
          "entitlement": {
            "enum": [
              "enabled",
              "not_licensed",
              "trial",
              "expired"
            ],
            "description": "Whether a field the hardware can produce is licensed on this particular unit. `not_licensed`, `trial` and `expired` all mean the value will not arrive even though the device is capable of producing it."
          },
          "conditions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Preconditions that must hold before this source actually produces the field, such as `requires_ball_marker`. A field whose conditions are unmet is declared but will not arrive."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "connectivity": {
      "type": "object",
      "additionalProperties": false,
      "description": "Measured by the engine over a sampling period, not declared by the operator. Internet access is a certification requirement at competition tiers and it is the requirement most likely to be true on inspection day and false on event day.",
      "properties": {
        "reachable": {
          "type": "boolean",
          "description": "Whether the bay could reach SGA at all during the assessment window."
        },
        "measured_over_days": {
          "type": "integer",
          "minimum": 0,
          "description": "How long the measurement window was. A single good day says nothing; this is what tells a reader how much the other connectivity numbers are worth."
        },
        "uptime_pct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Share of the measurement window in which the bay was reachable."
        },
        "median_latency_ms": {
          "type": "integer",
          "minimum": 0,
          "description": "Median round-trip latency to SGA. Median rather than mean, because one bad hour would otherwise dominate a month of good ones."
        },
        "downlink_mbps": {
          "type": "number",
          "minimum": 0,
          "description": "Measured download throughput. Relevant mainly to bundle and firmware relay."
        },
        "uplink_mbps": {
          "type": "number",
          "minimum": 0,
          "description": "Measured upload throughput. The direction that matters for shipping journals and media off the bay."
        },
        "redundant_path": {
          "type": "boolean",
          "description": "Whether a second route exists -- a cellular fallback behind wired broadband, typically. The difference between an outage and an inconvenience for a bay hosting a timed event."
        },
        "time_anchor_available": {
          "enum": [
            "signed_remote",
            "signed_cached",
            "roughtime",
            "system_clock",
            "none"
          ],
          "description": "The best time source this bay can reach. Determines whether it can host an event with an enforceable deadline."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "presence": {
      "type": "object",
      "additionalProperties": false,
      "description": "Hardware for verifying a player is at the bay. Placement matters as much as presence -- an adapter in a cupboard forty feet away technically scans and proves nothing -- so the engine reports observed signal strength rather than only the adapter's existence.",
      "properties": {
        "ble_scan_available": {
          "type": "boolean",
          "description": "The bay can scan for Bluetooth LE advertisements, so a player's phone can be observed nearby."
        },
        "ble_advertise_available": {
          "type": "boolean",
          "description": "The bay can advertise over Bluetooth LE, so a player's phone can observe the bay. Both directions are ordinary app capabilities and neither needs elevation."
        },
        "proximity_token_supported": {
          "type": "boolean",
          "description": "The bay can exchange a short-lived proximity token, which is stronger evidence than either party merely seeing the other."
        },
        "adapter_at_swing_position": {
          "type": "boolean",
          "description": "Declared at certification and corroborated by observed RSSI during commissioning. Declared alone is not evidence."
        },
        "median_rssi_dbm": {
          "type": "number",
          "description": "Observed during commissioning, from a device held at the swing position."
        },
        "nfc_available": {
          "type": "boolean",
          "description": "The bay has NFC, allowing a tap that is hard to perform from anywhere but the bay itself."
        },
        "qr_placard": {
          "type": "boolean",
          "description": "A printed placard is mounted at the bay. The weakest form of presence -- a photograph of a placard travels -- and it is recorded honestly as such."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "video": {
      "type": "object",
      "additionalProperties": false,
      "description": "Cameras available for human review of swings. Identity confirmation from video is deliberately absent: it is automated biometric identification and is not built.",
      "properties": {
        "cameras": {
          "type": "integer",
          "minimum": 0,
          "description": "How many cameras are installed."
        },
        "covers_swing_position": {
          "type": "boolean",
          "description": "Whether a camera actually frames the player at address and through the swing, as opposed to merely being present in the room."
        },
        "face_on_right_handed": {
          "type": "boolean",
          "description": "A face-on view of a right-handed player."
        },
        "face_on_left_handed": {
          "type": "boolean",
          "description": "A face-on view of a left-handed player. A single fixed camera cannot give both -- the two handednesses stand on opposite sides of the target line, so one position is face-on for one and behind for the other. Competition tiers that verify WHO swung need both, since a rear view shows a person swinging but not which person."
        },
        "covers_bay_entry": {
          "type": "boolean",
          "description": "Whether a camera sees who enters the bay."
        },
        "mirrored_pair": {
          "type": "boolean",
          "description": "Two cameras mounted at mirrored positions either side of the hitting area. Gives face-on coverage of both handednesses AND lets the engine determine handedness by observing which camera sees the player's front -- so the second camera earns its cost on every shot rather than only when a left-hander plays."
        },
        "handedness_detection": {
          "type": "boolean",
          "description": "Whether the bay can determine handedness from video rather than relying on what was entered."
        },
        "resolution_note": {
          "type": "string",
          "maxLength": 64,
          "description": "Free text about resolution and framing, for a human assessor. Deliberately not a structured field: what matters is whether the footage is usable, and no single number settles that."
        },
        "frame_rate_hz": {
          "type": "number",
          "minimum": 0,
          "description": "Capture frame rate. A swing is over in about a fifth of a second, so frame rate is the number that decides whether review footage shows anything."
        },
        "review_retention_days": {
          "type": "integer",
          "minimum": 0,
          "description": "How long swing footage is kept before deletion. Retention is a privacy commitment, so it is stated in the profile rather than left to whatever the recorder happens to be configured for."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "operations": {
      "type": "object",
      "additionalProperties": false,
      "description": "Facts the platform's booking and scheduling services need, gathered once at certification rather than surveyed separately.",
      "properties": {
        "bays_at_facility": {
          "type": "integer",
          "minimum": 1,
          "description": "How many bays the facility has. A multi-bay site can run a field simultaneously; a single bay runs it in shifts."
        },
        "operating_hours": {
          "type": "string",
          "maxLength": 200,
          "description": "When the facility is open, which bounds any deadline an event can set."
        },
        "bookable": {
          "type": "boolean",
          "description": "Whether bay time can be reserved in advance."
        },
        "booking_integration": {
          "type": "string",
          "maxLength": 64,
          "description": "The booking system in use, where the facility has one that could be integrated with."
        },
        "residential": {
          "type": "boolean",
          "description": "A bay in somebody's home, certified SGA Private Gold Certified for its owner, family and friends. Certified and numbered but never publicly listed: not on a map, a list or a search."
        },
        "staffed": {
          "type": "boolean",
          "description": "Whether staff are present during play. Bears directly on identity assurance: a staffed bay can witness who is standing in it."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "assessment": {
      "type": "object",
      "additionalProperties": false,
      "description": "The engine's own read of which tiers this bay satisfies, offered as a pre-check. It is not a certification: SGA certifies, a human reviews, and this only tells an operator what to expect before anyone travels.",
      "properties": {
        "tiers_satisfied": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Every certification tier the bay meets as assessed. A bay is not certified by this field -- a certificate is a signed statement from SGA, and this is the bay's own view of itself."
        },
        "unmet": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "tier",
              "requirement"
            ],
            "additionalProperties": false,
            "properties": {
              "tier": {
                "type": "string",
                "description": "The tier this shortfall relates to."
              },
              "requirement": {
                "type": "string",
                "description": "The requirement that was not met."
              },
              "observed": {
                "description": "What was actually observed instead, so the gap is measurable and the operator can tell how close they are."
              }
            },
            "patternProperties": {
              "^x-[a-z0-9_-]+\\.": {}
            }
          },
          "description": "The specific requirements standing between this bay and the tiers it does not satisfy. Written as a list of gaps rather than a verdict, so an operator is told what to fix rather than merely that they failed."
        }
      },
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      }
    },
    "signature": {
      "$ref": "common.schema.json#/$defs/signature"
    },
    "x": {
      "$ref": "common.schema.json#/$defs/vendorExtensions"
    }
  },
  "patternProperties": {
    "^x-[a-z0-9_-]+\\.": {}
  }
}
