{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/session-manifest.schema.json",
  "title": "SGA-OIS session manifest",
  "description": "The connection matrix, published by the engine to every participant. The set of manifests for a session is a complete history of the bay's technical configuration for that round.",
  "type": "object",
  "required": [
    "manifest_version",
    "manifest_hash",
    "session_id",
    "bundle_version",
    "protocol_version",
    "participants"
  ],
  "additionalProperties": false,
  "properties": {
    "sga_kind": {
      "const": "session-manifest",
      "description": "Document type discriminator. Always session-manifest. 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."
    },
    "manifest_version": {
      "type": "integer",
      "minimum": 1,
      "description": "Monotonic within a session. Incremented on any topology change."
    },
    "manifest_hash": {
      "$ref": "common.schema.json#/$defs/digest"
    },
    "previous_manifest_hash": {
      "$ref": "common.schema.json#/$defs/digest"
    },
    "session_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "round_id": {
      "$ref": "common.schema.json#/$defs/sgaId",
      "description": "Present when this session continues an existing round. A player returning the next evening opens a NEW session carrying the SAME round_id."
    },
    "session_epoch": {
      "$ref": "common.schema.json#/$defs/sessionEpoch"
    },
    "session_started_at": {
      "$ref": "common.schema.json#/$defs/timestamp"
    },
    "bay_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "facility_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "event_id": {
      "$ref": "common.schema.json#/$defs/sgaId"
    },
    "bundle_version": {
      "$ref": "common.schema.json#/$defs/bundleVersion"
    },
    "protocol_version": {
      "$ref": "common.schema.json#/$defs/protocolVersion"
    },
    "issued_at": {
      "$ref": "common.schema.json#/$defs/timestamp"
    },
    "participants": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "device_id",
          "roles",
          "descriptor_hash"
        ],
        "additionalProperties": false,
        "properties": {
          "device_id": {
            "$ref": "common.schema.json#/$defs/sgaId"
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The roles this device fills in this session -- launch monitor, simulator, display, and so on. Roles are descriptive only: routing is decided field by field, never by role."
          },
          "descriptor_hash": {
            "$ref": "common.schema.json#/$defs/digest",
            "description": "Hash of the signed capability descriptor. This is how a shot record's provenance resolves."
          },
          "attestation_tier": {
            "$ref": "common.schema.json#/$defs/attestationTier"
          },
          "bay_bound": {
            "type": "boolean",
            "description": "false means the device could not present a bay token. Recorded, not refused; the competition rules decide."
          },
          "joined_at": {
            "$ref": "common.schema.json#/$defs/timestamp"
          },
          "admitted_via": {
            "$ref": "common.schema.json#/$defs/admissionPath"
          },
          "relayed_by": {
            "$ref": "common.schema.json#/$defs/sgaId",
            "description": "Present when a phone composes this device's values from a vendor protocol (13 section 4.1): the participant that asserted them."
          },
          "origin_handle": {
            "$ref": "common.schema.json#/$defs/deviceHandle",
            "description": "For a member's own device, the session-scoped handle the engine records instead of any hardware identifier."
          },
          "degraded": {
            "type": "boolean",
            "description": "Present and true while a participant that heartbeats has not been heard from for three heartbeats (09 section 4). Its values still count; a consumer weighing one is owed the fact. Absent otherwise."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      },
      "description": "Every device in the session and what each one is contributing, after the connection matrix has resolved who supplies what."
    },
    "sources": {
      "type": "object",
      "description": "Resolved field -> authoritative provider. Selection order: provenance, then resolution, then latency, then operator priority, then deterministically by device_id so two identical devices resolve the same way on every engine.",
      "additionalProperties": {
        "type": "object",
        "required": [
          "from",
          "provenance"
        ],
        "additionalProperties": false,
        "properties": {
          "from": {
            "$ref": "common.schema.json#/$defs/sgaId"
          },
          "provenance": {
            "$ref": "common.schema.json#/$defs/provenance"
          },
          "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."
          },
          "latency_ms": {
            "type": "integer",
            "description": "Typical delay between the physical event and this source reporting it. Used as the final tiebreak when two sources offer the same field at the same provenance: lower wins."
          },
          "id_methods": {
            "type": "array",
            "items": {
              "$ref": "common.schema.json#/$defs/clubIdMethod"
            },
            "description": "For club.club_id: the methods the winning source can perform. Selection for this field ranks by method strength rather than by provenance, since every source of a club identity would otherwise claim measured."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "subscribers": {
      "type": "object",
      "description": "Field -> devices receiving it. Filters LIVE DISTRIBUTION ONLY. Recording is total: the engine journals every field every participant offered, subscribed or not. This must never become a retention policy.",
      "additionalProperties": {
        "type": "array",
        "items": {
          "$ref": "common.schema.json#/$defs/sgaId"
        }
      }
    },
    "unmet": {
      "type": "array",
      "description": "Wants nobody can satisfy. Published rather than hidden — this is how we learn which fields the ecosystem actually needs, straight from the devices.",
      "items": {
        "type": "object",
        "required": [
          "field",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "field": {
            "type": "string",
            "description": "The field nobody in the session is supplying."
          },
          "wanted_by": {
            "type": "array",
            "items": {
              "$ref": "common.schema.json#/$defs/sgaId"
            },
            "description": "The devices that asked for this field and will not be receiving it."
          },
          "reason": {
            "enum": [
              "no_provider",
              "restricted",
              "policy_denied",
              "rate_exceeded",
              "vendor_exclusive"
            ],
            "description": "Why the field is unmet. no_provider: nothing present offers it. restricted: a provider exists but withheld it. policy_denied: the bundle or event package forbids this pairing. rate_exceeded: the consumer asked faster than any provider can supply. vendor_exclusive: every device offering it is from another vendor, in a role a participant accepts only from its own vendor (see excluded). The distinction is the point -- no_provider is a hardware gap the operator can fix by plugging something in, restricted is a decision somebody made."
          },
          "required": {
            "type": "boolean",
            "description": "True if a consumer declared this field required rather than optional. A required field left unmet makes the session degraded, not merely incomplete."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "excluded": {
      "type": "array",
      "description": "Devices admitted to the session but not used as a source for any field, because a participant accepts their role only from its own vendor (03 section 2.1c). Published so the excluded device, the operator and anyone reading the record later can see why a connected device contributed nothing to it. Its data is still journaled.",
      "items": {
        "type": "object",
        "required": [
          "device_id",
          "role",
          "excluded_by",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "device_id": {
            "$ref": "common.schema.json#/$defs/sgaId",
            "description": "The device not used as a source."
          },
          "role": {
            "$ref": "common.schema.json#/$defs/role",
            "description": "The role it holds that the excluding participant accepts only from its own vendor."
          },
          "excluded_by": {
            "$ref": "common.schema.json#/$defs/sgaId",
            "description": "The participant whose own_vendor_only declaration excludes it."
          },
          "reason": {
            "const": "vendor_exclusive",
            "description": "Why. One value today; a field rather than an implication so a later reason does not change the shape."
          }
        },
        "patternProperties": {
          "^x-[a-z0-9_-]+\\.": {}
        }
      }
    },
    "time_anchor": {
      "$ref": "common.schema.json#/$defs/timeAnchor",
      "description": "Recorded per session so that, after the fact, it is answerable whether any deadline applied to this round was enforced against trusted time or merely against the bay's clock."
    },
    "clock_skew_ms": {
      "type": "object",
      "description": "Measured at handshake. A participant more than 2000 ms out of agreement is reported, not silently corrected — a bay whose clock is wrong is evidence.",
      "additionalProperties": {
        "type": "integer"
      }
    },
    "rules_applied": {
      "$ref": "event-rules-package.schema.json#/$defs/conformanceVector",
      "description": "Present when the session is bound to a sanctioned event."
    },
    "signature": {
      "$ref": "common.schema.json#/$defs/signature"
    },
    "x": {
      "$ref": "common.schema.json#/$defs/vendorExtensions"
    },
    "bundle_source": {
      "enum": [
        "cache",
        "sideload",
        "package",
        "peer",
        "online",
        "embedded"
      ],
      "description": "The path the bundle in force arrived by (04 section 3), so fleet reports show which distribution channels propagate."
    }
  },
  "patternProperties": {
    "^x-[a-z0-9_-]+\\.": {}
  }
}
